feat: white list, announce relay list

This commit is contained in:
gzuuus
2025-02-11 19:09:57 +01:00
parent 48c45eab8d
commit 1e3f22647b
6 changed files with 127 additions and 63 deletions

View File

@@ -11,3 +11,6 @@ MCP_CLIENT_NAME="DVM MCP Bridge Client"
MCP_CLIENT_VERSION="1.0.0" MCP_CLIENT_VERSION="1.0.0"
MCP_SERVER_COMMAND=bun # The command to start the MCP server 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 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=

View File

@@ -2,6 +2,30 @@ import { config } from 'dotenv';
import { join } from 'path'; import { join } from 'path';
import { existsSync } from 'fs'; 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<string> | undefined;
}
interface AppConfig {
nostr: NostrConfig;
mcp: MCPConfig;
whitelist: WhitelistConfig;
}
const envPath = join(process.cwd(), '.env'); const envPath = join(process.cwd(), '.env');
if (!existsSync(envPath)) { if (!existsSync(envPath)) {
throw new Error( throw new Error(
@@ -31,7 +55,7 @@ function getEnvVar(name: string, defaultValue: string): string {
return process.env[name] || defaultValue; return process.env[name] || defaultValue;
} }
export const CONFIG = { export const CONFIG: AppConfig = {
nostr: { nostr: {
privateKey: requireEnvVar('PRIVATE_KEY'), privateKey: requireEnvVar('PRIVATE_KEY'),
relayUrls: requireEnvVar('RELAY_URLS') relayUrls: requireEnvVar('RELAY_URLS')
@@ -49,6 +73,11 @@ export const CONFIG = {
serverCommand: requireEnvVar('MCP_SERVER_COMMAND'), serverCommand: requireEnvVar('MCP_SERVER_COMMAND'),
serverArgs: requireEnvVar('MCP_SERVER_ARGS').split(','), 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)) { if (!HEX_KEYS_REGEX.test(CONFIG.nostr.privateKey)) {

View File

@@ -4,6 +4,7 @@ import { RelayHandler } from './nostr/relay';
import { keyManager } from './nostr/keys'; import { keyManager } from './nostr/keys';
import relayHandler from './nostr/relay'; import relayHandler from './nostr/relay';
import type { Event } from 'nostr-tools/pure'; import type { Event } from 'nostr-tools/pure';
import { CONFIG } from './config';
export class DVMBridge { export class DVMBridge {
private mcpClient: MCPClientHandler; private mcpClient: MCPClientHandler;
@@ -18,13 +19,18 @@ export class DVMBridge {
this.nostrAnnouncer = new NostrAnnouncer(this.mcpClient); 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() { async start() {
if (this.isRunning) { if (this.isRunning) {
console.log('Bridge is already running'); console.log('Bridge is already running');
return; return;
} }
console.log('Starting DVM Bridge...');
try { try {
console.log('Connecting to MCP server...'); console.log('Connecting to MCP server...');
await this.mcpClient.connect(); await this.mcpClient.connect();
@@ -33,7 +39,7 @@ export class DVMBridge {
console.log('Available MCP tools:', tools); console.log('Available MCP tools:', tools);
console.log('Announcing service to Nostr network...'); console.log('Announcing service to Nostr network...');
await this.nostrAnnouncer.announceService(); await this.nostrAnnouncer.updateAnnouncement();
console.log('Setting up request handlers...'); console.log('Setting up request handlers...');
this.relayHandler.subscribeToRequests(this.handleRequest.bind(this)); this.relayHandler.subscribeToRequests(this.handleRequest.bind(this));
@@ -65,6 +71,7 @@ export class DVMBridge {
private async handleRequest(event: Event) { private async handleRequest(event: Event) {
try { try {
if (this.isWhitelisted(event.pubkey)) {
if (event.kind === 5910) { if (event.kind === 5910) {
const command = event.tags.find((tag) => tag[0] === 'c')?.[1]; const command = event.tags.find((tag) => tag[0] === 'c')?.[1];
@@ -136,6 +143,18 @@ export class DVMBridge {
} }
} }
} }
} 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) { } catch (error) {
console.error('Error handling request:', error); console.error('Error handling request:', error);
} }
@@ -143,7 +162,6 @@ export class DVMBridge {
} }
if (import.meta.main) { if (import.meta.main) {
console.log('Starting DVM-MCP Bridge service...');
const bridge = new DVMBridge(); const bridge = new DVMBridge();
const shutdown = async () => { const shutdown = async () => {

View File

@@ -1,7 +1,7 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { CONFIG } from './config'; import { CONFIG } from './config';
// TODO: Add connection to multiple mcp servers and router
export class MCPClientHandler { export class MCPClientHandler {
private client: Client; private client: Client;
private transport: StdioClientTransport; private transport: StdioClientTransport;

View File

@@ -13,9 +13,19 @@ export class NostrAnnouncer {
this.mcpClient = mcpClient; 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() { async announceService() {
const toolsResult = await this.mcpClient.listTools(); const toolsResult = await this.mcpClient.listTools();
const event = keyManager.signEvent({ const event = keyManager.signEvent({
...keyManager.createEventTemplate(31990), ...keyManager.createEventTemplate(31990),
content: JSON.stringify({ content: JSON.stringify({
@@ -31,12 +41,11 @@ export class NostrAnnouncer {
...toolsResult.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 ${toolsResult.length} tools`); console.log(`Announced service with ${toolsResult.length} tools`);
} }
async updateAnnouncement() { async updateAnnouncement() {
await this.announceService(); await Promise.all([this.announceService(), this.announceRelayList()]);
} }
} }

View File

@@ -43,7 +43,9 @@ export class RelayHandler {
async publishEvent(event: Event): Promise<void> { async publishEvent(event: Event): Promise<void> {
try { try {
await Promise.any(this.pool.publish(this.relayUrls, event)); 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) { } catch (error) {
console.error('Failed to publish event:', error); console.error('Failed to publish event:', error);
throw error; throw error;
@@ -60,6 +62,9 @@ export class RelayHandler {
const sub = this.pool.subscribeMany(this.relayUrls, filters, { const sub = this.pool.subscribeMany(this.relayUrls, filters, {
onevent(event) { onevent(event) {
console.log(
`Event received(${event.kind}), id: ${event.id.slice(0, 12)}`
);
onRequest(event); onRequest(event);
}, },
oneose() { oneose() {