diff --git a/.env.example b/.env.example index aa63a12..fad7984 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,7 @@ MCP_SERVICE_ABOUT="MCP-enabled DVM providing AI and computational tools" MCP_CLIENT_NAME="DVM MCP Bridge Client" MCP_CLIENT_VERSION="1.0.0" MCP_SERVER_COMMAND=bun # The command to start the MCP server -MCP_SERVER_ARGS=run,src/external-mcp-server.ts # Comma-separated args to pass to the server command \ No newline at end of file +MCP_SERVER_ARGS=run,src/external-mcp-server.ts # Comma-separated args to pass to the server command + +# Optional: Comma-separated list of allowed pubkeys. If empty, allows all. +ALLOWED_PUBKEYS= \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index ea74c90..e9a9004 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,30 @@ import { config } from 'dotenv'; import { join } from 'path'; import { existsSync } from 'fs'; +interface NostrConfig { + privateKey: string; + relayUrls: string[]; +} + +interface MCPConfig { + name: string; + about: string; + clientName: string; + clientVersion: string; + serverCommand: string; + serverArgs: string[]; +} + +interface WhitelistConfig { + allowedPubkeys: Set | undefined; +} + +interface AppConfig { + nostr: NostrConfig; + mcp: MCPConfig; + whitelist: WhitelistConfig; +} + const envPath = join(process.cwd(), '.env'); if (!existsSync(envPath)) { throw new Error( @@ -31,7 +55,7 @@ function getEnvVar(name: string, defaultValue: string): string { return process.env[name] || defaultValue; } -export const CONFIG = { +export const CONFIG: AppConfig = { nostr: { privateKey: requireEnvVar('PRIVATE_KEY'), relayUrls: requireEnvVar('RELAY_URLS') @@ -49,6 +73,11 @@ export const CONFIG = { serverCommand: requireEnvVar('MCP_SERVER_COMMAND'), serverArgs: requireEnvVar('MCP_SERVER_ARGS').split(','), }, + whitelist: { + allowedPubkeys: process.env.ALLOWED_PUBKEYS + ? new Set(process.env.ALLOWED_PUBKEYS.split(',').map((pk) => pk.trim())) + : undefined, + }, }; if (!HEX_KEYS_REGEX.test(CONFIG.nostr.privateKey)) { diff --git a/src/dvm-bridge.ts b/src/dvm-bridge.ts index 131ff07..f0895ba 100644 --- a/src/dvm-bridge.ts +++ b/src/dvm-bridge.ts @@ -4,6 +4,7 @@ import { RelayHandler } from './nostr/relay'; import { keyManager } from './nostr/keys'; import relayHandler from './nostr/relay'; import type { Event } from 'nostr-tools/pure'; +import { CONFIG } from './config'; export class DVMBridge { private mcpClient: MCPClientHandler; @@ -18,13 +19,18 @@ export class DVMBridge { this.nostrAnnouncer = new NostrAnnouncer(this.mcpClient); } + private isWhitelisted(pubkey: string): boolean { + if (!CONFIG.whitelist.allowedPubkeys) { + return true; + } + return CONFIG.whitelist.allowedPubkeys.has(pubkey); + } + async start() { if (this.isRunning) { console.log('Bridge is already running'); return; } - - console.log('Starting DVM Bridge...'); try { console.log('Connecting to MCP server...'); await this.mcpClient.connect(); @@ -33,7 +39,7 @@ export class DVMBridge { console.log('Available MCP tools:', tools); console.log('Announcing service to Nostr network...'); - await this.nostrAnnouncer.announceService(); + await this.nostrAnnouncer.updateAnnouncement(); console.log('Setting up request handlers...'); this.relayHandler.subscribeToRequests(this.handleRequest.bind(this)); @@ -65,76 +71,89 @@ export class DVMBridge { private async handleRequest(event: Event) { try { - if (event.kind === 5910) { - const command = event.tags.find((tag) => tag[0] === 'c')?.[1]; + if (this.isWhitelisted(event.pubkey)) { + if (event.kind === 5910) { + const command = event.tags.find((tag) => tag[0] === 'c')?.[1]; - if (command === 'list-tools') { - const tools = await this.mcpClient.listTools(); - const response = keyManager.signEvent({ - ...keyManager.createEventTemplate(6910), - content: JSON.stringify({ - tools, - }), - tags: [ - ['request', JSON.stringify(event)], - ['e', event.id], - ['p', event.pubkey], - ], - }); - - await this.relayHandler.publishEvent(response); - } else { - const jobRequest = JSON.parse(event.content); - const processingStatus = keyManager.signEvent({ - ...keyManager.createEventTemplate(7000), - tags: [ - ['status', 'processing'], - ['e', event.id], - ['p', event.pubkey], - ], - }); - await this.relayHandler.publishEvent(processingStatus); - - try { - const result = await this.mcpClient.callTool( - jobRequest.name, - jobRequest.parameters - ); - const successStatus = keyManager.signEvent({ - ...keyManager.createEventTemplate(7000), - tags: [ - ['status', 'success'], - ['e', event.id], - ['p', event.pubkey], - ], - }); - await this.relayHandler.publishEvent(successStatus); + if (command === 'list-tools') { + const tools = await this.mcpClient.listTools(); const response = keyManager.signEvent({ ...keyManager.createEventTemplate(6910), - content: JSON.stringify(result), + content: JSON.stringify({ + tools, + }), tags: [ ['request', JSON.stringify(event)], ['e', event.id], ['p', event.pubkey], ], }); + await this.relayHandler.publishEvent(response); - } catch (error) { - const errorStatus = keyManager.signEvent({ + } else { + const jobRequest = JSON.parse(event.content); + const processingStatus = keyManager.signEvent({ ...keyManager.createEventTemplate(7000), tags: [ - [ - 'status', - 'error', - error instanceof Error ? error.message : 'Unknown error', - ], + ['status', 'processing'], ['e', event.id], ['p', event.pubkey], ], }); - await this.relayHandler.publishEvent(errorStatus); + await this.relayHandler.publishEvent(processingStatus); + + try { + const result = await this.mcpClient.callTool( + jobRequest.name, + jobRequest.parameters + ); + const successStatus = keyManager.signEvent({ + ...keyManager.createEventTemplate(7000), + tags: [ + ['status', 'success'], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(successStatus); + const response = keyManager.signEvent({ + ...keyManager.createEventTemplate(6910), + content: JSON.stringify(result), + tags: [ + ['request', JSON.stringify(event)], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(response); + } catch (error) { + const errorStatus = keyManager.signEvent({ + ...keyManager.createEventTemplate(7000), + tags: [ + [ + 'status', + 'error', + error instanceof Error ? error.message : 'Unknown error', + ], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(errorStatus); + } } } + } else { + const errorStatus = keyManager.signEvent({ + ...keyManager.createEventTemplate(7000), + content: 'Unauthorized: Pubkey not in whitelist', + tags: [ + ['status', 'error'], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(errorStatus); } } catch (error) { console.error('Error handling request:', error); @@ -143,7 +162,6 @@ export class DVMBridge { } if (import.meta.main) { - console.log('Starting DVM-MCP Bridge service...'); const bridge = new DVMBridge(); const shutdown = async () => { diff --git a/src/mcp-client.ts b/src/mcp-client.ts index ce095a2..f30e43f 100644 --- a/src/mcp-client.ts +++ b/src/mcp-client.ts @@ -1,7 +1,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { CONFIG } from './config'; - +// TODO: Add connection to multiple mcp servers and router export class MCPClientHandler { private client: Client; private transport: StdioClientTransport; diff --git a/src/nostr/announcer.ts b/src/nostr/announcer.ts index fb2d75e..97524b0 100644 --- a/src/nostr/announcer.ts +++ b/src/nostr/announcer.ts @@ -13,9 +13,19 @@ export class NostrAnnouncer { this.mcpClient = mcpClient; } + async announceRelayList() { + const event = keyManager.signEvent({ + ...keyManager.createEventTemplate(10002), + content: '', + tags: CONFIG.nostr.relayUrls.map((url) => ['r', url]), + }); + + await this.relayHandler.publishEvent(event); + console.log('Announced relay list metadata'); + } + async announceService() { const toolsResult = await this.mcpClient.listTools(); - const event = keyManager.signEvent({ ...keyManager.createEventTemplate(31990), content: JSON.stringify({ @@ -31,12 +41,11 @@ export class NostrAnnouncer { ...toolsResult.map((tool) => ['t', tool.name]), ], }); - await this.relayHandler.publishEvent(event); console.log(`Announced service with ${toolsResult.length} tools`); } async updateAnnouncement() { - await this.announceService(); + await Promise.all([this.announceService(), this.announceRelayList()]); } } diff --git a/src/nostr/relay.ts b/src/nostr/relay.ts index 4c31edd..1599d90 100644 --- a/src/nostr/relay.ts +++ b/src/nostr/relay.ts @@ -43,7 +43,9 @@ export class RelayHandler { async publishEvent(event: Event): Promise { try { await Promise.any(this.pool.publish(this.relayUrls, event)); - console.log(`Event published(${event.kind}):, ${event.id.slice(0, 12)}`); + console.log( + `Event published(${event.kind}), id: ${event.id.slice(0, 12)}` + ); } catch (error) { console.error('Failed to publish event:', error); throw error; @@ -60,6 +62,9 @@ export class RelayHandler { const sub = this.pool.subscribeMany(this.relayUrls, filters, { onevent(event) { + console.log( + `Event received(${event.kind}), id: ${event.id.slice(0, 12)}` + ); onRequest(event); }, oneose() {