diff --git a/docs/dvmcp-spec.md b/docs/dvmcp-spec.md index bda8f4c..4275b0f 100644 --- a/docs/dvmcp-spec.md +++ b/docs/dvmcp-spec.md @@ -66,7 +66,7 @@ Clients MAY use either method or both depending on their needs. Each method has ## Discovery via NIP-89 Announcements -DVMs SHOULD publish their tool listings using NIP-89 announcements. This enables immediate tool discovery without requiring a request/response cycle and allows clients to discover tools through relay queries. +You can query relays by creating a filter for events with kind `31990`, and `t` tag `mcp`. DVMs SHOULD include their available tools directly in their kind:31990 announcement events. This enables immediate tool discovery and execution without requiring an additional request/response cycle. Here's an example of a complete announcement: Example announcement: @@ -80,11 +80,16 @@ Example announcement: "tools": [ { "name": "summarize", - "description": "Summarizes text input" - }, - { - "name": "translate", - "description": "Translates text between languages" + "description": "Summarizes text input", + "inputSchema": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Text to summarize" + } + } + } } ] }, @@ -105,12 +110,19 @@ Each tool in the `tools` array MUST include: - `name`: The unique identifier for the tool - `description`: A brief description of the tool's functionality +- `tools`: The tools present in the MCP server -To maintain the announcement under control, the NIP-89 announcement SHOULD NOT include full input schemas. Since some apis might have an undefined amount of inputs and the event might end pretty verbose or lengthly. +### Required Tags + +- `d`: A unique identifier for this announcement that should be maintained consistently for announcement updates +- `k`: The event kind this DVM supports (5910 for MCP bridge requests) +- `capabilities`: Must include "mcp-1.0" to indicate MCP protocol support +- `t`: Should include "mcp", and also tool names, to aid in discovery ## Discovery via Direct Request Following NIP-90's model, clients MAY discover tools by publishing a request event and receiving responses from available DVMs. This method allows for discovery of DVMs that may not publish NIP-89 announcements. +Another way to do discovery using the previous list tools request is to query relays with a filter for events with type `5910` and `c` tag `list-tools-response`. ### List Tools Request @@ -210,7 +222,8 @@ Tools are executed through request/response pairs using kinds 5910/6910. }, "tags": [ ["c", "execute-tool"], - ["p", ""] + ["p", ""], + ["output", "application/json"] ] } ``` diff --git a/src/dvm-bridge.ts b/src/dvm-bridge.ts index a814df1..131ff07 100644 --- a/src/dvm-bridge.ts +++ b/src/dvm-bridge.ts @@ -2,7 +2,7 @@ import { MCPClientHandler } from './mcp-client'; import { NostrAnnouncer } from './nostr/announcer'; import { RelayHandler } from './nostr/relay'; import { keyManager } from './nostr/keys'; -import { CONFIG } from './config'; +import relayHandler from './nostr/relay'; import type { Event } from 'nostr-tools/pure'; export class DVMBridge { @@ -14,8 +14,8 @@ export class DVMBridge { constructor() { console.log('Initializing DVM Bridge...'); this.mcpClient = new MCPClientHandler(); + this.relayHandler = relayHandler; this.nostrAnnouncer = new NostrAnnouncer(this.mcpClient); - this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls); } async start() { diff --git a/src/mcp-client.ts b/src/mcp-client.ts index 3a99f5e..ce095a2 100644 --- a/src/mcp-client.ts +++ b/src/mcp-client.ts @@ -33,7 +33,7 @@ export class MCPClientHandler { } async listTools() { - return await this.client.listTools(); + return (await this.client.listTools()).tools; } async callTool(name: string, args: Record) { diff --git a/src/nostr/announcer.ts b/src/nostr/announcer.ts index 7c9534d..fb2d75e 100644 --- a/src/nostr/announcer.ts +++ b/src/nostr/announcer.ts @@ -2,45 +2,38 @@ import { CONFIG } from '../config'; import { RelayHandler } from './relay'; import { keyManager } from './keys'; import { MCPClientHandler } from '../mcp-client'; -import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'; +import relayHandler from './relay'; export class NostrAnnouncer { private relayHandler: RelayHandler; private mcpClient: MCPClientHandler; constructor(mcpClient: MCPClientHandler) { - this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls); + this.relayHandler = relayHandler; this.mcpClient = mcpClient; } async announceService() { - const toolsResult: ListToolsResult = await this.mcpClient.listTools(); - - const toolsListing = toolsResult.tools - .map((tool) => ({ - name: tool.name, - description: tool.description, - })) - .slice(0, 100); // Hard limit to 100 tools + const toolsResult = await this.mcpClient.listTools(); const event = keyManager.signEvent({ ...keyManager.createEventTemplate(31990), content: JSON.stringify({ name: CONFIG.mcp.name, about: CONFIG.mcp.about, - tools: toolsListing, + tools: toolsResult, }), tags: [ ['d', 'dvm-announcement'], ['k', '5910'], ['capabilities', 'mcp-1.0'], ['t', 'mcp'], - ...toolsListing.map((tool) => ['t', tool.name]), + ...toolsResult.map((tool) => ['t', tool.name]), ], }); await this.relayHandler.publishEvent(event); - console.log(`Announced service with ${toolsListing.length} tools`); + console.log(`Announced service with ${toolsResult.length} tools`); } async updateAnnouncement() { diff --git a/src/nostr/relay.ts b/src/nostr/relay.ts index f38ca6d..4c31edd 100644 --- a/src/nostr/relay.ts +++ b/src/nostr/relay.ts @@ -4,6 +4,7 @@ import type { SubCloser } from 'nostr-tools/pool'; import WebSocket from 'ws'; import { useWebSocketImplementation } from 'nostr-tools/pool'; import type { Filter } from 'nostr-tools'; +import { CONFIG } from '../config'; useWebSocketImplementation(WebSocket); @@ -11,10 +12,32 @@ export class RelayHandler { private pool: SimplePool; private relayUrls: string[]; private subscriptions: SubCloser[] = []; + private reconnectInterval?: ReturnType; constructor(relayUrls: string[]) { this.pool = new SimplePool(); this.relayUrls = relayUrls; + this.startReconnectLoop(); + } + + private startReconnectLoop() { + this.reconnectInterval = setInterval(() => { + this.relayUrls.forEach((url) => { + const normalizedUrl = new URL(url).href; + if (!this.getConnectionStatus().get(normalizedUrl)) { + this.ensureRelay(url); + } + }); + }, 10000); + } + + private async ensureRelay(url: string) { + try { + await this.pool.ensureRelay(url, { connectionTimeout: 5000 }); + console.log(`Connected to relay: ${url}`); + } catch (error) { + console.log(`Failed to connect to relay ${url}:`, error); + } } async publishEvent(event: Event): Promise { @@ -42,6 +65,9 @@ export class RelayHandler { oneose() { console.log('Reached end of stored events'); }, + onclose(reasons) { + console.log('Subscription closed:', reasons); + }, }); this.subscriptions.push(sub); @@ -53,8 +79,17 @@ export class RelayHandler { } cleanup() { + if (this.reconnectInterval) { + clearInterval(this.reconnectInterval); + } this.subscriptions.forEach((sub) => sub.close()); this.subscriptions = []; this.pool.close(this.relayUrls); } + + getConnectionStatus(): Map { + return this.pool.listConnectionStatus(); + } } + +export default new RelayHandler(CONFIG.nostr.relayUrls);