From 09debf68071b49f7a2af9b883301aeb1ae751f50 Mon Sep 17 00:00:00 2001 From: gzuuus <116975404+gzuuus@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:31:33 +0100 Subject: [PATCH] Refactor/clis (#6) --- .gitignore | 2 +- docs/dvmcp-spec.md | 6 +- packages/dvmcp-bridge/.npmignore | 2 +- packages/dvmcp-bridge/README.md | 6 +- packages/dvmcp-bridge/cli.ts | 2 +- packages/dvmcp-bridge/config.example.yml | 4 +- packages/dvmcp-bridge/config.yml | 18 +++ packages/dvmcp-bridge/package.json | 4 +- packages/dvmcp-bridge/src/announcer.ts | 3 + packages/dvmcp-bridge/src/config.ts | 7 +- packages/dvmcp-bridge/src/dvm-bridge.ts | 19 ++- packages/dvmcp-bridge/src/keys.test.ts | 5 + packages/dvmcp-bridge/src/mcp-pool.test.ts | 5 +- packages/dvmcp-bridge/src/mock-server.ts | 30 ----- packages/dvmcp-bridge/src/types.ts | 3 + packages/dvmcp-commons/config-generator.ts | 113 ++++++++++++++---- packages/dvmcp-commons/nostr/key-manager.ts | 3 + packages/dvmcp-commons/package.json | 2 +- packages/dvmcp-discovery/.npmignore | 2 +- packages/dvmcp-discovery/README.md | 6 +- packages/dvmcp-discovery/cli.ts | 2 +- packages/dvmcp-discovery/config.yml | 10 ++ packages/dvmcp-discovery/package.json | 4 +- packages/dvmcp-discovery/src/config.ts | 4 +- .../dvmcp-discovery/src/discovery-server.ts | 4 +- .../dvmcp-discovery/src/discovery.test.ts | 2 +- packages/dvmcp-discovery/src/tool-executor.ts | 10 +- packages/dvmcp-discovery/src/tool-registry.ts | 45 ++++--- 28 files changed, 219 insertions(+), 104 deletions(-) create mode 100644 packages/dvmcp-bridge/config.yml delete mode 100644 packages/dvmcp-bridge/src/mock-server.ts create mode 100644 packages/dvmcp-discovery/config.yml diff --git a/.gitignore b/.gitignore index ff780c8..811e447 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,4 @@ dist codebase* .aider* .prettierrc -config.yml \ No newline at end of file +config.dvmcp.yml \ No newline at end of file diff --git a/docs/dvmcp-spec.md b/docs/dvmcp-spec.md index 4275b0f..224b000 100644 --- a/docs/dvmcp-spec.md +++ b/docs/dvmcp-spec.md @@ -42,8 +42,8 @@ This specification defines these event kinds: | Kind | Description | | ----- | ------------------------------------- | | 31990 | DVM Service Announcement (via NIP-89) | -| 5910 | DVM-MCP Bridge Requests | -| 6910 | DVM-MCP Bridge Responses | +| 5910 | DVMCP Bridge Requests | +| 6910 | DVMCP Bridge Responses | | 7000 | Job Feedback | Operations are differentiated using the `c` tag, which specifies the command being executed: @@ -339,7 +339,7 @@ For any error, DVMs MUST: sequenceDiagram participant Client as Nostr Client participant Relay as Nostr Relay - participant DVM as MCP-DVM Bridge + participant DVM as DVMCP-Bridge participant Server as MCP Server rect rgb(240, 240, 240) diff --git a/packages/dvmcp-bridge/.npmignore b/packages/dvmcp-bridge/.npmignore index 5cdffbc..726e767 100644 --- a/packages/dvmcp-bridge/.npmignore +++ b/packages/dvmcp-bridge/.npmignore @@ -4,4 +4,4 @@ node_modules .DS_Store *.test.ts *.test.js -config.yml \ No newline at end of file +config.dvmcp.yml \ No newline at end of file diff --git a/packages/dvmcp-bridge/README.md b/packages/dvmcp-bridge/README.md index 23f82a5..52f44da 100644 --- a/packages/dvmcp-bridge/README.md +++ b/packages/dvmcp-bridge/README.md @@ -12,11 +12,11 @@ A bridge implementation that connects Model Context Protocol (MCP) servers to No ## Configuration -When the package is run for the first time, it will detect if the `config.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields +When the package is run for the first time, it will detect if the `'config.dvmcp.yml'` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields ```bash -cp config.example.yml config.yml -nano config.yml +cp config.example.yml config.dvmcp.yml +nano config.dvmcp.yml ``` ## Usage diff --git a/packages/dvmcp-bridge/cli.ts b/packages/dvmcp-bridge/cli.ts index eb18bef..0225032 100755 --- a/packages/dvmcp-bridge/cli.ts +++ b/packages/dvmcp-bridge/cli.ts @@ -14,7 +14,7 @@ import { import { argv } from 'process'; import type { Config } from './src/types'; -const configPath = join(process.cwd(), 'config.yml'); +const configPath = join(process.cwd(), 'config.dvmcp.yml'); const configFields: Record = { nostr: { diff --git a/packages/dvmcp-bridge/config.example.yml b/packages/dvmcp-bridge/config.example.yml index 1d35841..a2afea5 100644 --- a/packages/dvmcp-bridge/config.example.yml +++ b/packages/dvmcp-bridge/config.example.yml @@ -15,7 +15,9 @@ mcp: # Required client information clientName: "DVM MCP Bridge Client" clientVersion: "1.0.0" - + # optional metadata + picture: "https://image.nostr.build/5bf2e2eb3b858bf72c23e53ed1f41ed0f65b2c8a805eaa48dd506b7cfec4ab88.png" + website: "https://github.com/gzuuus/dvmcp" # MCP Servers Configuration accepts multiple servers servers: - name: "server1" diff --git a/packages/dvmcp-bridge/config.yml b/packages/dvmcp-bridge/config.yml new file mode 100644 index 0000000..6d124ad --- /dev/null +++ b/packages/dvmcp-bridge/config.yml @@ -0,0 +1,18 @@ +nostr: + privateKey: 0e79e5e90754cb23275477a32bd1c42bee28dc78a0b10693dfd8cac6852363fd + relayUrls: + - ws://localhost:10547 +mcp: + name: DVM MCP Bridge + about: MCP-enabled DVM providing AI and computational tools + clientName: DVM MCP Bridge Client + clientVersion: 1.0.0 + servers: + - name: hello + command: bun + args: + - --cwd + - /home/user/Documents/dev/gz/js-ts/bun/mcp-echo + - start:mcp +whitelist: + allowedPubkeys: [] diff --git a/packages/dvmcp-bridge/package.json b/packages/dvmcp-bridge/package.json index 8d338f0..3079b38 100644 --- a/packages/dvmcp-bridge/package.json +++ b/packages/dvmcp-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@dvmcp/bridge", - "version": "0.1.6", + "version": "0.1.11", "description": "Bridge connecting MCP servers to Nostr's DVM ecosystem", "module": "index.ts", "type": "module", @@ -35,7 +35,7 @@ "dotenv": "^16.4.7", "nostr-tools": "^2.10.4", "yaml": "^2.7.0", - "@dvmcp/commons": "^0.1.1" + "@dvmcp/commons": "^0.1.2" }, "publishConfig": { "access": "public" diff --git a/packages/dvmcp-bridge/src/announcer.ts b/packages/dvmcp-bridge/src/announcer.ts index 5044428..9dc32e1 100644 --- a/packages/dvmcp-bridge/src/announcer.ts +++ b/packages/dvmcp-bridge/src/announcer.ts @@ -37,6 +37,9 @@ export class NostrAnnouncer { content: JSON.stringify({ name: CONFIG.mcp.name, about: CONFIG.mcp.about, + picture: CONFIG.mcp.picture, + website: CONFIG.mcp.website, + banner: CONFIG.mcp.banner, tools: tools, }), tags: [ diff --git a/packages/dvmcp-bridge/src/config.ts b/packages/dvmcp-bridge/src/config.ts index b507aab..2b36e96 100644 --- a/packages/dvmcp-bridge/src/config.ts +++ b/packages/dvmcp-bridge/src/config.ts @@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs'; import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants'; import type { Config, MCPServerConfig } from './types'; -const CONFIG_PATH = join(process.cwd(), 'config.yml'); +const CONFIG_PATH = join(process.cwd(), 'config.dvmcp.yml'); const TEST_CONFIG: Config = { nostr: { @@ -87,7 +87,7 @@ function loadConfig(): Config { if (!existsSync(CONFIG_PATH)) { throw new Error( - 'No config.yml file found. Please create one based on config.example.yml' + 'No config.dvmcp.yml file found. Please create one based on config.example.yml' ); } @@ -117,6 +117,9 @@ function loadConfig(): Config { rawConfig.mcp?.clientVersion, 'mcp.clientVersion' ), + picture: rawConfig.mcp?.picture, + website: rawConfig.mcp?.website, + banner: rawConfig.mcp?.banner, servers: validateMCPServers(rawConfig.mcp?.servers), }, whitelist: { diff --git a/packages/dvmcp-bridge/src/dvm-bridge.ts b/packages/dvmcp-bridge/src/dvm-bridge.ts index 576ae81..877320a 100644 --- a/packages/dvmcp-bridge/src/dvm-bridge.ts +++ b/packages/dvmcp-bridge/src/dvm-bridge.ts @@ -24,7 +24,10 @@ export class DVMBridge { } private isWhitelisted(pubkey: string): boolean { - if (!CONFIG.whitelist.allowedPubkeys) { + if ( + !CONFIG.whitelist.allowedPubkeys || + CONFIG.whitelist.allowedPubkeys.size == 0 + ) { return true; } return CONFIG.whitelist.allowedPubkeys.has(pubkey); @@ -47,7 +50,12 @@ export class DVMBridge { await this.nostrAnnouncer.updateAnnouncement(); console.log('Setting up request handlers...'); - this.relayHandler.subscribeToRequests(this.handleRequest.bind(this)); + const publicKey = keyManager.getPublicKey(); + this.relayHandler.subscribeToRequests(this.handleRequest.bind(this), { + kinds: [TOOL_REQUEST_KIND], + '#p': [publicKey], + since: Math.floor(Date.now() / 1000), + }); this.isRunning = true; console.log('DVM Bridge is now running and ready to handle requests'); @@ -88,14 +96,14 @@ export class DVMBridge { tools, }), tags: [ - ['request', JSON.stringify(event)], + ['c', 'list-tools-response'], ['e', event.id], ['p', event.pubkey], ], }); await this.relayHandler.publishEvent(response); - } else { + } else if (command === 'execute-tool') { const jobRequest = JSON.parse(event.content); const processingStatus = keyManager.signEvent({ ...keyManager.createEventTemplate(DVM_NOTICE_KIND), @@ -125,7 +133,7 @@ export class DVMBridge { ...keyManager.createEventTemplate(TOOL_RESPONSE_KIND), content: JSON.stringify(result), tags: [ - ['request', JSON.stringify(event)], + ['c', 'execute-tool-response'], ['e', event.id], ['p', event.pubkey], ], @@ -159,6 +167,7 @@ export class DVMBridge { ], }); await this.relayHandler.publishEvent(errorStatus); + return; } } catch (error) { console.error('Error handling request:', error); diff --git a/packages/dvmcp-bridge/src/keys.test.ts b/packages/dvmcp-bridge/src/keys.test.ts index 3d5d1d6..ac38b0b 100644 --- a/packages/dvmcp-bridge/src/keys.test.ts +++ b/packages/dvmcp-bridge/src/keys.test.ts @@ -22,4 +22,9 @@ describe('KeyManager', () => { expect(signedEvent.sig).toBeDefined(); expect(signedEvent.pubkey).toBe(keyManager.pubkey); }); + + test('should get public key', () => { + const publicKey = keyManager.getPublicKey(); + expect(publicKey).toBeDefined(); + }); }); diff --git a/packages/dvmcp-bridge/src/mcp-pool.test.ts b/packages/dvmcp-bridge/src/mcp-pool.test.ts index 55ab2e1..8a58821 100644 --- a/packages/dvmcp-bridge/src/mcp-pool.test.ts +++ b/packages/dvmcp-bridge/src/mcp-pool.test.ts @@ -9,7 +9,10 @@ describe('MCPPool', () => { let transports: any[] = []; const serverNames = ['server1', 'server2', 'server3', 'server4']; beforeAll(async () => { - const mockServerPath = join(import.meta.dir, 'mock-server.ts'); + const mockServerPath = join( + import.meta.dir, + '../../dvmcp-commons/mock-server.ts' + ); const serverConfigs = serverNames.map((name) => ({ name, diff --git a/packages/dvmcp-bridge/src/mock-server.ts b/packages/dvmcp-bridge/src/mock-server.ts deleted file mode 100644 index 4e2d300..0000000 --- a/packages/dvmcp-bridge/src/mock-server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { z } from 'zod'; - -export const createMockServer = async (name: string) => { - const server = new McpServer({ - name: `Mock ${name}`, - version: '1.0.0', - }); - - server.tool( - `${name}-echo`, - `Echo tool for ${name}`, - { - text: z.string(), - }, - async ({ text }) => ({ - content: [{ type: 'text' as const, text: `[${name}] ${text}` }], - }) - ); - - const transport = new StdioServerTransport(); - await server.connect(transport); - - return { server, transport }; -}; - -if (import.meta.path === Bun.main) { - await createMockServer(process.argv[2] || 'default'); -} diff --git a/packages/dvmcp-bridge/src/types.ts b/packages/dvmcp-bridge/src/types.ts index 9a47840..846c537 100644 --- a/packages/dvmcp-bridge/src/types.ts +++ b/packages/dvmcp-bridge/src/types.ts @@ -8,6 +8,9 @@ export interface MCPConfig { about: string; clientName: string; clientVersion: string; + picture?: string; + website?: string; + banner?: string; servers: MCPServerConfig[]; } diff --git a/packages/dvmcp-commons/config-generator.ts b/packages/dvmcp-commons/config-generator.ts index 23187e9..37dd95c 100644 --- a/packages/dvmcp-commons/config-generator.ts +++ b/packages/dvmcp-commons/config-generator.ts @@ -1,6 +1,6 @@ import { createInterface } from 'node:readline'; -import { stringify } from 'yaml'; -import { writeFileSync, existsSync } from 'node:fs'; +import { parse, stringify } from 'yaml'; +import { writeFileSync, existsSync, readFileSync } from 'node:fs'; import { randomBytes } from 'node:crypto'; import { HEX_KEYS_REGEX } from './constants'; @@ -47,10 +47,21 @@ export interface FieldConfig { } export class ConfigGenerator> { + private currentConfig: T | null = null; constructor( private configPath: string, private fields: Record - ) {} + ) { + if (existsSync(configPath)) { + try { + this.currentConfig = parse(readFileSync(configPath, 'utf8')); + } catch (error) { + console.warn( + `${CONFIG_EMOJIS.INFO} Could not parse existing configuration` + ); + } + } + } private async prompt(question: string, defaultValue = ''): Promise { const rl = createInterface({ @@ -112,6 +123,27 @@ export class ConfigGenerator> { ): Promise { const emoji = config.emoji || CONFIG_EMOJIS.PROMPT; + if (currentValue !== undefined) { + if (config.type !== 'nested' && config.type !== 'object-array') { + console.log( + `${CONFIG_EMOJIS.INFO} Current value: ${ + typeof currentValue === 'object' + ? JSON.stringify(currentValue) + : currentValue + }` + ); + } + + const keepCurrent = await this.promptYesNo( + `${emoji} Keep current ${fieldName}?`, + true + ); + + if (keepCurrent) { + return currentValue; + } + } + if (config.type === 'nested') { console.log(`\n${emoji} ${config.description || fieldName}`); } @@ -151,7 +183,20 @@ export class ConfigGenerator> { array.forEach((item: string, index: number) => { console.log(`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item}`); }); - console.log(''); + + if (await this.promptYesNo(`${emoji} Remove any items?`, false)) { + while (true) { + const index = + parseInt( + await this.prompt('Enter index to remove (0 to finish):') + ) - 1; + if (isNaN(index) || index < 0) break; + if (index < array.length) { + array.splice(index, 1); + console.log('Item removed'); + } + } + } } if (await this.promptYesNo(`${emoji} Add ${fieldName}?`, true)) { @@ -187,7 +232,7 @@ export class ConfigGenerator> { }, 'object-array': async () => { - const array: ArrayItem[] = currentValue || []; + let array: ArrayItem[] = currentValue || []; if (array.length > 0) { console.log('\nCurrent servers:'); array.forEach((item: ArrayItem, index: number) => { @@ -196,19 +241,49 @@ export class ConfigGenerator> { ); }); console.log(''); + const keepCurrent = await this.promptYesNo( + `${emoji} Keep current ${fieldName}?`, + true + ); + if (!keepCurrent) { + array = []; + console.log(`${CONFIG_EMOJIS.INFO} Cleared existing servers.`); + } else { + if ( + await this.promptYesNo( + `${emoji} Remove any existing servers?`, + false + ) + ) { + while (true) { + const index = + parseInt( + await this.prompt( + 'Enter server number to remove (0 to finish):' + ) + ) - 1; + if (isNaN(index) || index < 0) break; + if (index < array.length) { + console.log( + `${CONFIG_EMOJIS.INFO} Removed server: ${array[index].name}` + ); + array.splice(index, 1); + } + } + } + } } - if ( - (await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) && - config.fields - ) { + if (await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) { while (true) { - const item = await this.handleObjectArrayItem(config.fields); + const item = await this.handleObjectArrayItem(config.fields!); if (!item) break; array.push(item as ArrayItem); + console.log(`${CONFIG_EMOJIS.INFO} Added new server: ${item.name}`); console.log(''); } } + return array; }, @@ -269,19 +344,15 @@ export class ConfigGenerator> { } async generate(): Promise { - console.log(`\n${CONFIG_EMOJIS.SETUP} Configuration Setup\n`); - - if (existsSync(this.configPath)) { - const reconfigure = await this.promptYesNo( - `${CONFIG_EMOJIS.PROMPT} Configuration exists. Reconfigure?`, - true - ); - if (!reconfigure) process.exit(0); - } - const config: Record = {}; for (const [fieldName, fieldConfig] of Object.entries(this.fields)) { - config[fieldName] = await this.handleField(fieldName, fieldConfig); + const currentValue = this.currentConfig?.[fieldName]; + config[fieldName] = await this.handleField( + fieldName, + fieldConfig, + true, + currentValue + ); } writeFileSync(this.configPath, stringify(config)); diff --git a/packages/dvmcp-commons/nostr/key-manager.ts b/packages/dvmcp-commons/nostr/key-manager.ts index a8da004..7c189b8 100644 --- a/packages/dvmcp-commons/nostr/key-manager.ts +++ b/packages/dvmcp-commons/nostr/key-manager.ts @@ -22,6 +22,9 @@ export const createKeyManager = (privateKeyHex: string) => { content: '', }; } + getPublicKey(): string { + return this.pubkey; + } } return new Manager(); diff --git a/packages/dvmcp-commons/package.json b/packages/dvmcp-commons/package.json index 22d4b69..77e9f03 100644 --- a/packages/dvmcp-commons/package.json +++ b/packages/dvmcp-commons/package.json @@ -1,6 +1,6 @@ { "name": "@dvmcp/commons", - "version": "0.1.1", + "version": "0.1.2", "description": "Shared utilities for DVMCP packages", "type": "module", "exports": { diff --git a/packages/dvmcp-discovery/.npmignore b/packages/dvmcp-discovery/.npmignore index 5cdffbc..726e767 100644 --- a/packages/dvmcp-discovery/.npmignore +++ b/packages/dvmcp-discovery/.npmignore @@ -4,4 +4,4 @@ node_modules .DS_Store *.test.ts *.test.js -config.yml \ No newline at end of file +config.dvmcp.yml \ No newline at end of file diff --git a/packages/dvmcp-discovery/README.md b/packages/dvmcp-discovery/README.md index c22eab9..52f82d3 100644 --- a/packages/dvmcp-discovery/README.md +++ b/packages/dvmcp-discovery/README.md @@ -11,11 +11,11 @@ A MCP server implementation that aggregates tools from DVMs across the Nostr net ## Configuration -When the package is run for the first time, it will detect if the `config.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields +When the package is run for the first time, it will detect if the `config.dvmcp.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields ```bash -cp config.example.yml config.yml -nano config.yml +cp config.example.yml config.dvmcp.yml +nano config.dvmcp.yml ``` ## Usage diff --git a/packages/dvmcp-discovery/cli.ts b/packages/dvmcp-discovery/cli.ts index 3838f41..f19c53a 100755 --- a/packages/dvmcp-discovery/cli.ts +++ b/packages/dvmcp-discovery/cli.ts @@ -12,7 +12,7 @@ import type { Config } from './src/config.js'; import { argv } from 'process'; import { existsSync } from 'fs'; -const configPath = join(process.cwd(), 'config.yml'); +const configPath = join(process.cwd(), 'config.dvmcp.yml'); const configFields: Record = { nostr: { diff --git a/packages/dvmcp-discovery/config.yml b/packages/dvmcp-discovery/config.yml new file mode 100644 index 0000000..4514896 --- /dev/null +++ b/packages/dvmcp-discovery/config.yml @@ -0,0 +1,10 @@ +nostr: + privateKey: c5af6a2588250d3dfe82a9cc1c054697d7483b53850d7e6552620ff8e565630c + relayUrls: + - wss://relay.dvmcp.fun +mcp: + name: DVMCP Discovery + version: 1.0.0 + about: DVMCP Discovery Server for aggregating MCP tools from DVMs +whitelist: + allowedDVMs: [] diff --git a/packages/dvmcp-discovery/package.json b/packages/dvmcp-discovery/package.json index 7da7bb9..ed2508c 100644 --- a/packages/dvmcp-discovery/package.json +++ b/packages/dvmcp-discovery/package.json @@ -1,6 +1,6 @@ { "name": "@dvmcp/discovery", - "version": "0.1.7", + "version": "0.1.11", "description": "Discovery service for MCP tools in the Nostr DVM ecosystem", "module": "index.ts", "type": "module", @@ -33,7 +33,7 @@ "@modelcontextprotocol/sdk": "^1.5.0", "nostr-tools": "^2.10.4", "yaml": "^2.7.0", - "@dvmcp/commons": "^0.1.1" + "@dvmcp/commons": "^0.1.2" }, "publishConfig": { "access": "public" diff --git a/packages/dvmcp-discovery/src/config.ts b/packages/dvmcp-discovery/src/config.ts index ea28794..1f2bd4c 100644 --- a/packages/dvmcp-discovery/src/config.ts +++ b/packages/dvmcp-discovery/src/config.ts @@ -18,7 +18,7 @@ export interface Config { }; } -const CONFIG_PATH = join(process.cwd(), 'config.yml'); +const CONFIG_PATH = join(process.cwd(), 'config.dvmcp.yml'); const TEST_CONFIG: Config = { nostr: { @@ -79,7 +79,7 @@ function loadConfig(): Config { if (!existsSync(CONFIG_PATH)) { throw new Error( - 'No config.yml file found. Please create one based on config.example.yml' + 'No config.dvmcp.yml file found. Please create one based on config.example.yml' ); } diff --git a/packages/dvmcp-discovery/src/discovery-server.ts b/packages/dvmcp-discovery/src/discovery-server.ts index 38a0715..fc48b68 100644 --- a/packages/dvmcp-discovery/src/discovery-server.ts +++ b/packages/dvmcp-discovery/src/discovery-server.ts @@ -61,7 +61,7 @@ export class DiscoveryServer { if (!announcement?.tools) return; for (const tool of announcement.tools) { - const toolId = `${event.pubkey.slice(0, 12)}:${tool.name}`; + const toolId = `${tool.name}:${event.pubkey.slice(0, 4)}`; this.toolRegistry.registerTool(toolId, tool); } } catch (error) { @@ -72,7 +72,7 @@ export class DiscoveryServer { private isAllowedDVM(pubkey: string): boolean { if ( !CONFIG.whitelist?.allowedDVMs || - CONFIG.whitelist.allowedDVMs.size === 0 + CONFIG.whitelist.allowedDVMs.size == 0 ) { return true; } diff --git a/packages/dvmcp-discovery/src/discovery.test.ts b/packages/dvmcp-discovery/src/discovery.test.ts index 715c727..75ddb2a 100644 --- a/packages/dvmcp-discovery/src/discovery.test.ts +++ b/packages/dvmcp-discovery/src/discovery.test.ts @@ -56,7 +56,7 @@ describe('DiscoveryServer E2E', () => { const toolIds = Array.from(toolRegistry['discoveredTools'].keys()); console.log('Available tool IDs:', toolIds); - const toolId = toolIds.find((id) => id.endsWith(`:${mockTool.name}`)); + const toolId = toolIds.find((id) => id.startsWith(`${mockTool.name}`)); expect(toolId).toBeDefined(); console.log('Selected tool ID:', toolId); diff --git a/packages/dvmcp-discovery/src/tool-executor.ts b/packages/dvmcp-discovery/src/tool-executor.ts index 16f0997..5fa24cc 100644 --- a/packages/dvmcp-discovery/src/tool-executor.ts +++ b/packages/dvmcp-discovery/src/tool-executor.ts @@ -104,10 +104,18 @@ export class ToolExecutor { private createToolRequest(tool: Tool, params: unknown): Event { const request = this.keyManager.createEventTemplate(TOOL_REQUEST_KIND); + + const parameters = + !tool.inputSchema.properties || + Object.keys(tool.inputSchema.properties).length === 0 + ? {} + : params; + request.content = JSON.stringify({ name: tool.name, - parameters: params, + parameters, }); + request.tags.push(['c', 'execute-tool']); return this.keyManager.signEvent(request); } diff --git a/packages/dvmcp-discovery/src/tool-registry.ts b/packages/dvmcp-discovery/src/tool-registry.ts index 5dc9b97..15a5b8b 100644 --- a/packages/dvmcp-discovery/src/tool-registry.ts +++ b/packages/dvmcp-discovery/src/tool-registry.ts @@ -81,27 +81,34 @@ export class ToolRegistry { } private mapJsonSchemaToZod(schema: Tool['inputSchema']): z.ZodRawShape { + if (!schema.properties || Object.keys(schema.properties).length === 0) { + return { _: z.object({}).optional() }; + } + const properties: z.ZodRawShape = {}; - if (schema.properties) { - for (const [key, prop] of Object.entries(schema.properties)) { - if (typeof prop === 'object' && prop && 'type' in prop) { - switch (prop.type) { - case 'string': - properties[key] = z.string(); - break; - case 'number': - properties[key] = z.number(); - break; - case 'integer': - properties[key] = z.number().int(); - break; - case 'boolean': - properties[key] = z.boolean(); - break; - default: - properties[key] = z.any(); - } + for (const [key, prop] of Object.entries(schema.properties)) { + if (typeof prop === 'object' && prop && 'type' in prop) { + let zodType: z.ZodType; + switch (prop.type) { + case 'string': + zodType = z.string(); + break; + case 'number': + zodType = z.number(); + break; + case 'integer': + zodType = z.number().int(); + break; + case 'boolean': + zodType = z.boolean(); + break; + default: + zodType = z.any(); } + properties[key] = + Array.isArray(schema.required) && schema.required.includes(key) + ? zodType + : zodType.optional(); } } return properties;