mirror of
https://github.com/aljazceru/dvmcp.git
synced 2025-12-17 05:14:24 +01:00
docs: clarify spec, feat: relay reconnection loop, and other improvements
This commit is contained in:
@@ -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"]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user