docs: clarify spec, feat: relay reconnection loop, and other improvements

This commit is contained in:
gzuuus
2025-02-10 18:41:31 +01:00
parent 65eb5905cb
commit 48c45eab8d
5 changed files with 65 additions and 24 deletions

View File

@@ -66,7 +66,7 @@ Clients MAY use either method or both depending on their needs. Each method has
## Discovery via NIP-89 Announcements ## 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: Example announcement:
@@ -80,11 +80,16 @@ Example announcement:
"tools": [ "tools": [
{ {
"name": "summarize", "name": "summarize",
"description": "Summarizes text input" "description": "Summarizes text input",
}, "inputSchema": {
{ "type": "object",
"name": "translate", "properties": {
"description": "Translates text between languages" "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 - `name`: The unique identifier for the tool
- `description`: A brief description of the tool's functionality - `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 ## 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. 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 ### List Tools Request
@@ -210,7 +222,8 @@ Tools are executed through request/response pairs using kinds 5910/6910.
}, },
"tags": [ "tags": [
["c", "execute-tool"], ["c", "execute-tool"],
["p", "<provider-pubkey>"] ["p", "<provider-pubkey>"],
["output", "application/json"]
] ]
} }
``` ```

View File

@@ -2,7 +2,7 @@ import { MCPClientHandler } from './mcp-client';
import { NostrAnnouncer } from './nostr/announcer'; import { NostrAnnouncer } from './nostr/announcer';
import { RelayHandler } from './nostr/relay'; import { RelayHandler } from './nostr/relay';
import { keyManager } from './nostr/keys'; import { keyManager } from './nostr/keys';
import { CONFIG } from './config'; import relayHandler from './nostr/relay';
import type { Event } from 'nostr-tools/pure'; import type { Event } from 'nostr-tools/pure';
export class DVMBridge { export class DVMBridge {
@@ -14,8 +14,8 @@ export class DVMBridge {
constructor() { constructor() {
console.log('Initializing DVM Bridge...'); console.log('Initializing DVM Bridge...');
this.mcpClient = new MCPClientHandler(); this.mcpClient = new MCPClientHandler();
this.relayHandler = relayHandler;
this.nostrAnnouncer = new NostrAnnouncer(this.mcpClient); this.nostrAnnouncer = new NostrAnnouncer(this.mcpClient);
this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls);
} }
async start() { async start() {

View File

@@ -33,7 +33,7 @@ export class MCPClientHandler {
} }
async listTools() { async listTools() {
return await this.client.listTools(); return (await this.client.listTools()).tools;
} }
async callTool(name: string, args: Record<string, any>) { async callTool(name: string, args: Record<string, any>) {

View File

@@ -2,45 +2,38 @@ import { CONFIG } from '../config';
import { RelayHandler } from './relay'; import { RelayHandler } from './relay';
import { keyManager } from './keys'; import { keyManager } from './keys';
import { MCPClientHandler } from '../mcp-client'; import { MCPClientHandler } from '../mcp-client';
import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'; import relayHandler from './relay';
export class NostrAnnouncer { export class NostrAnnouncer {
private relayHandler: RelayHandler; private relayHandler: RelayHandler;
private mcpClient: MCPClientHandler; private mcpClient: MCPClientHandler;
constructor(mcpClient: MCPClientHandler) { constructor(mcpClient: MCPClientHandler) {
this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls); this.relayHandler = relayHandler;
this.mcpClient = mcpClient; this.mcpClient = mcpClient;
} }
async announceService() { async announceService() {
const toolsResult: ListToolsResult = await this.mcpClient.listTools(); const toolsResult = 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 event = keyManager.signEvent({ const event = keyManager.signEvent({
...keyManager.createEventTemplate(31990), ...keyManager.createEventTemplate(31990),
content: JSON.stringify({ content: JSON.stringify({
name: CONFIG.mcp.name, name: CONFIG.mcp.name,
about: CONFIG.mcp.about, about: CONFIG.mcp.about,
tools: toolsListing, tools: toolsResult,
}), }),
tags: [ tags: [
['d', 'dvm-announcement'], ['d', 'dvm-announcement'],
['k', '5910'], ['k', '5910'],
['capabilities', 'mcp-1.0'], ['capabilities', 'mcp-1.0'],
['t', 'mcp'], ['t', 'mcp'],
...toolsListing.map((tool) => ['t', tool.name]), ...toolsResult.map((tool) => ['t', tool.name]),
], ],
}); });
await this.relayHandler.publishEvent(event); await this.relayHandler.publishEvent(event);
console.log(`Announced service with ${toolsListing.length} tools`); console.log(`Announced service with ${toolsResult.length} tools`);
} }
async updateAnnouncement() { async updateAnnouncement() {

View File

@@ -4,6 +4,7 @@ import type { SubCloser } from 'nostr-tools/pool';
import WebSocket from 'ws'; import WebSocket from 'ws';
import { useWebSocketImplementation } from 'nostr-tools/pool'; import { useWebSocketImplementation } from 'nostr-tools/pool';
import type { Filter } from 'nostr-tools'; import type { Filter } from 'nostr-tools';
import { CONFIG } from '../config';
useWebSocketImplementation(WebSocket); useWebSocketImplementation(WebSocket);
@@ -11,10 +12,32 @@ export class RelayHandler {
private pool: SimplePool; private pool: SimplePool;
private relayUrls: string[]; private relayUrls: string[];
private subscriptions: SubCloser[] = []; private subscriptions: SubCloser[] = [];
private reconnectInterval?: ReturnType<typeof setTimeout>;
constructor(relayUrls: string[]) { constructor(relayUrls: string[]) {
this.pool = new SimplePool(); this.pool = new SimplePool();
this.relayUrls = relayUrls; 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<void> { async publishEvent(event: Event): Promise<void> {
@@ -42,6 +65,9 @@ export class RelayHandler {
oneose() { oneose() {
console.log('Reached end of stored events'); console.log('Reached end of stored events');
}, },
onclose(reasons) {
console.log('Subscription closed:', reasons);
},
}); });
this.subscriptions.push(sub); this.subscriptions.push(sub);
@@ -53,8 +79,17 @@ export class RelayHandler {
} }
cleanup() { cleanup() {
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
this.subscriptions.forEach((sub) => sub.close()); this.subscriptions.forEach((sub) => sub.close());
this.subscriptions = []; this.subscriptions = [];
this.pool.close(this.relayUrls); this.pool.close(this.relayUrls);
} }
getConnectionStatus(): Map<string, boolean> {
return this.pool.listConnectionStatus();
}
} }
export default new RelayHandler(CONFIG.nostr.relayUrls);