Skip to content
This repository was archived by the owner on Jul 21, 2025. It is now read-only.

Commit a5ae9b2

Browse files
committed
refactor: add encryption support detection for server communication
1 parent 43f20c2 commit a5ae9b2

13 files changed

Lines changed: 264 additions & 237 deletions

File tree

packages/dvmcp-bridge/src/announcer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
TAG_UNIQUE_IDENTIFIER,
1212
TAG_KIND,
1313
TAG_SERVER_IDENTIFIER,
14+
TAG_SUPPORT_ENCRYPTION,
1415
} from '@dvmcp/commons/core';
1516
import type { Event } from 'nostr-tools/pure';
1617
import { loggerBridge } from '@dvmcp/commons/core';
@@ -24,6 +25,7 @@ import {
2425
type ListResourceTemplatesResult,
2526
} from '@modelcontextprotocol/sdk/types.js';
2627
import { slugify } from '@dvmcp/commons/core';
28+
import { EncryptionMode } from '@dvmcp/commons/encryption';
2729

2830
function getNip89Tags(cfg: DvmcpBridgeConfig['mcp']): string[][] {
2931
const keys = ['name', 'about', 'picture', 'website', 'banner'] as const;
@@ -91,6 +93,11 @@ export class NostrAnnouncer {
9193
[TAG_KIND, `${REQUEST_KIND}`],
9294
...getNip89Tags(this.config.mcp),
9395
];
96+
97+
// Add encryption support tag conditionally
98+
if (this.config.encryption?.mode !== EncryptionMode.DISABLED) {
99+
tags.push([TAG_SUPPORT_ENCRYPTION, 'true']);
100+
}
94101
const event = this.keyManager.signEvent({
95102
...this.keyManager.createEventTemplate(SERVER_ANNOUNCEMENT_KIND),
96103
content: announcementContent,

packages/dvmcp-commons/src/config/utils.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,57 @@ import type { ValidationError } from './types';
66
* @returns Object with default values
77
*/
88
export function getDefaults(schema: any): any {
9-
if (!schema) return undefined;
9+
// Base case: if schema is null, undefined, or not an object, it has no defaults.
10+
if (!schema || typeof schema !== 'object') {
11+
return undefined;
12+
}
1013

11-
if (schema.type === 'object') {
12-
const result: any = {};
13-
if (schema.fields) {
14+
// Case 1: schema is a ConfigFieldMeta object (it has a 'type' property).
15+
if ('type' in schema) {
16+
// If the field itself has an explicit 'default' value, that takes precedence.
17+
if ('default' in schema) {
18+
return schema.default;
19+
}
20+
21+
// If it's an 'object' type with 'fields', recurse to build defaults for its children.
22+
if (schema.type === 'object' && schema.fields) {
23+
const subDefaults: Record<string, any> = {};
24+
let hasSubContent = false;
1425
for (const key in schema.fields) {
15-
const field = schema.fields[key];
16-
if ('default' in field) {
17-
result[key] = field.default;
18-
} else {
19-
const value = getDefaults(field);
20-
if (value !== undefined) {
21-
result[key] = value;
26+
if (Object.prototype.hasOwnProperty.call(schema.fields, key)) {
27+
const fieldValue = getDefaults(schema.fields[key]); // Recursive call
28+
if (fieldValue !== undefined) {
29+
subDefaults[key] = fieldValue;
30+
hasSubContent = true;
2231
}
2332
}
2433
}
34+
return hasSubContent ? subDefaults : undefined;
2535
}
26-
return result; // Always return the result object even if empty
36+
37+
// If it's an 'array' type (and no explicit 'default' was found above), default to an empty array.
38+
if (schema.type === 'array') {
39+
return [];
40+
}
41+
42+
// Other types (string, number, boolean) without an explicit 'default' have no default value.
43+
return undefined;
2744
}
28-
if (schema.type === 'array') {
29-
return [];
45+
// Case 2: schema is the root ConfigSchema (a plain object of ConfigFieldMeta, no 'type' property).
46+
else {
47+
const rootDefaults: Record<string, any> = {};
48+
let hasRootContent = false;
49+
for (const key in schema) {
50+
if (Object.prototype.hasOwnProperty.call(schema, key)) {
51+
const fieldValue = getDefaults(schema[key]); // Recursive call for each top-level field
52+
if (fieldValue !== undefined) {
53+
rootDefaults[key] = fieldValue;
54+
hasRootContent = true;
55+
}
56+
}
57+
}
58+
return hasRootContent ? rootDefaults : undefined;
3059
}
31-
if ('default' in schema) return schema.default;
32-
return undefined;
3360
}
3461

3562
/**

packages/dvmcp-commons/src/core/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const REQUEST_KIND = 25910; // Ephemeral: Client Requests
99
export const RESPONSE_KIND = 26910; // Ephemeral: Server Responses
1010
export const NOTIFICATION_KIND = 21316; // Ephemeral: Feedback/Notifications
1111
export const GIFT_WRAP_KIND = 1059; // Gift Wrap (NIP-59): Encrypted messages
12+
export const SEALED_DIRECT_MESSAGE_KIND = 13;
13+
export const PRIVATE_DIRECT_MESSAGE_KIND = 14;
1214

1315
// Common Tags for DVMCP Events
1416
export const TAG_UNIQUE_IDENTIFIER = 'd'; // Unique identifier (addressable events) or Server ID (init response)
@@ -21,6 +23,7 @@ export const TAG_KIND = 'k'; // Accepted request kind (server announcement)
2123
export const TAG_STATUS = 'status'; // Nostr-specific notification status (e.g., 'payment-required')
2224
export const TAG_AMOUNT = 'amount'; // Nostr-specific notification amount/invoice
2325
export const TAG_INVOICE = 'invoice'; // Nostr-specific notification invoice
26+
export const TAG_SUPPORT_ENCRYPTION = 'support_encryption';
2427
export const MCPMETHODS = {
2528
toolsList: 'tools/list',
2629
toolsCall: 'tools/call',

packages/dvmcp-commons/src/encryption/encryption-manager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { hexToBytes } from '@noble/hashes/utils';
66
import type { EncryptionConfig } from './types';
77
import { EncryptionMode } from './types';
88
import {
9-
SEALED_DIRECT_MESSAGE_KIND,
9+
GIFT_WRAP_KIND,
1010
PRIVATE_DIRECT_MESSAGE_KIND,
11-
} from './types';
12-
import { GIFT_WRAP_KIND } from '../core/constants';
11+
SEALED_DIRECT_MESSAGE_KIND,
12+
} from '../core/constants';
1313

1414
export interface DecryptedMessage {
1515
content: any;

packages/dvmcp-commons/src/encryption/types.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,3 @@ export interface EncryptionConfig {
2020
*/
2121
mode?: EncryptionMode;
2222
}
23-
24-
// TODO: move this kinds to constants
25-
/**
26-
* NIP-17 constants
27-
*/
28-
export const SEALED_DIRECT_MESSAGE_KIND = 13;
29-
export const PRIVATE_DIRECT_MESSAGE_KIND = 14;

packages/dvmcp-discovery/src/base-executor.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
NOTIFICATION_KIND,
77
GIFT_WRAP_KIND,
88
TAG_PUBKEY,
9+
loggerDiscovery,
910
} from '@dvmcp/commons/core';
1011
import type { RelayHandler } from '@dvmcp/commons/nostr';
1112
import type { KeyManager } from '@dvmcp/commons/nostr';
1213
import type { NWCPaymentHandler } from './nwc-payment';
1314
import { type EncryptionManager } from '@dvmcp/commons/encryption';
15+
import type { ServerRegistry } from './server-registry'; // Import ServerRegistry type
1416

1517
export abstract class BaseExecutor<T extends Capability, P, R> {
1618
protected executionSubscriptions: Map<string, () => void> = new Map();
@@ -22,6 +24,7 @@ export abstract class BaseExecutor<T extends Capability, P, R> {
2224
protected relayHandler: RelayHandler,
2325
protected keyManager: KeyManager,
2426
protected registry: BaseRegistry<T>,
27+
protected serverRegistry: ServerRegistry, // Add ServerRegistry to constructor
2528
encryptionManager?: EncryptionManager | null
2629
) {
2730
this.encryptionManager = encryptionManager || null;
@@ -153,27 +156,39 @@ export abstract class BaseExecutor<T extends Capability, P, R> {
153156
const recipientPubkey = request.tags.find(
154157
(tag) => tag[0] === TAG_PUBKEY
155158
)?.[1];
159+
156160
if (recipientPubkey) {
157-
try {
158-
const encryptedEvent =
159-
await this.encryptionManager.encryptMessage(
160-
this.keyManager.getPrivateKey(),
161-
recipientPubkey,
162-
{
163-
kind: request.kind,
164-
content: request.content,
165-
tags: request.tags,
166-
created_at: request.created_at,
167-
}
161+
const serverInfo =
162+
this.serverRegistry.getServerByPubkey(recipientPubkey);
163+
164+
// Check if the recipient server supports encryption
165+
if (serverInfo?.supportsEncryption) {
166+
try {
167+
const encryptedEvent =
168+
await this.encryptionManager.encryptMessage(
169+
this.keyManager.getPrivateKey(),
170+
recipientPubkey,
171+
{
172+
kind: request.kind,
173+
content: request.content,
174+
tags: request.tags,
175+
created_at: request.created_at,
176+
}
177+
);
178+
if (encryptedEvent) {
179+
eventToPublish = encryptedEvent;
180+
}
181+
} catch (encryptError) {
182+
// If encryption fails, send the original unencrypted request
183+
console.warn(
184+
'Failed to encrypt request, sending unencrypted:',
185+
encryptError
168186
);
169-
if (encryptedEvent) {
170-
eventToPublish = encryptedEvent;
171187
}
172-
} catch (encryptError) {
173-
// If encryption fails, send the original unencrypted request
174-
console.warn(
175-
'Failed to encrypt request, sending unencrypted:',
176-
encryptError
188+
} else {
189+
// If server does not support encryption, send unencrypted
190+
loggerDiscovery(
191+
`Recipient server ${recipientPubkey} does not support encryption or encryption mode disabled. Sending unencrypted.`
177192
);
178193
}
179194
}

packages/dvmcp-discovery/src/base-interfaces.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export interface Capability {
66
| 'resource'
77
| 'resourceTemplate'
88
| 'server'
9-
| 'completion';
9+
| 'completion'
10+
| 'ping';
1011
}
1112

1213
export interface DVMCPBridgeServer extends Capability {

packages/dvmcp-discovery/src/discovery-server.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
PROMPTS_LIST_KIND,
1515
TAG_SERVER_IDENTIFIER,
1616
TAG_UNIQUE_IDENTIFIER,
17+
TAG_SUPPORT_ENCRYPTION,
1718
} from '@dvmcp/commons/core';
1819
import {
1920
type Tool,
@@ -82,6 +83,7 @@ export class DiscoveryServer {
8283
this.relayHandler,
8384
this.keyManager,
8485
this.toolRegistry,
86+
this.serverRegistry,
8587
this.config,
8688
this.encryptionManager || undefined
8789
);
@@ -91,6 +93,7 @@ export class DiscoveryServer {
9193
this.relayHandler,
9294
this.keyManager,
9395
this.resourceRegistry,
96+
this.serverRegistry,
9497
this.config,
9598
this.encryptionManager || undefined
9699
);
@@ -100,6 +103,7 @@ export class DiscoveryServer {
100103
this.relayHandler,
101104
this.keyManager,
102105
this.promptRegistry,
106+
this.serverRegistry,
103107
this.config,
104108
this.encryptionManager || undefined
105109
);
@@ -116,6 +120,7 @@ export class DiscoveryServer {
116120
this.pingExecutor = new PingExecutor(
117121
this.relayHandler,
118122
this.keyManager,
123+
this.serverRegistry,
119124
this.encryptionManager || undefined
120125
);
121126

@@ -342,8 +347,24 @@ export class DiscoveryServer {
342347
loggerDiscovery('Server announcement missing server ID');
343348
return;
344349
}
345-
this.serverRegistry.registerServer(serverId, event.pubkey, event.content);
346-
loggerDiscovery(`Registered server: ${serverId} from ${event.pubkey}`);
350+
// Extract support_encryption tag
351+
const supportsEncryptionTag = event.tags.find(
352+
(tag) => tag[0] === TAG_SUPPORT_ENCRYPTION
353+
);
354+
const supportsEncryption =
355+
supportsEncryptionTag && supportsEncryptionTag[1] === 'true'
356+
? true
357+
: false;
358+
359+
this.serverRegistry.registerServer(
360+
serverId,
361+
event.pubkey,
362+
event.content,
363+
supportsEncryption
364+
);
365+
loggerDiscovery(
366+
`Registered server: ${serverId} from ${event.pubkey}, encryption support: ${supportsEncryption}`
367+
);
347368
} catch (error) {
348369
console.error('Error processing server announcement:', error);
349370
}
@@ -677,7 +698,12 @@ export class DiscoveryServer {
677698
capabilities: announcement.capabilities || {},
678699
serverInfo: announcement.serverInfo,
679700
instructions: announcement.instructions,
680-
})
701+
}),
702+
// For direct servers, assuming no explicit 'support_encryption' tag in InitializeResult.
703+
// If the MCP protocol or InitializeResult type is extended to include encryption info,
704+
// this logic would need to be updated to extract it.
705+
// For now, we default to false for direct servers unless explicitly handled.
706+
false // Defaulting to false for direct server encryption support
681707
);
682708
loggerDiscovery(
683709
`Registered direct server: ${announcement.serverInfo.name || serverId} (${serverId})`

0 commit comments

Comments
 (0)