import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { NostrServerTransport, PrivateKeySigner, ApplesauceRelayPool } from '@contextvm/sdk';
import { SimplePool, type NostrEvent, type Filter, finalizeEvent, getPublicKey, nip19 } from 'nostr-tools';
import pino from 'pino';
import type { Config } from '../config/index.js';
import type { ContextMapping } from '../mapping/contexts.js';
import { MCPDVMTranslator } from './translator.js';
import {
isToolCallRequest,
isToolListRequest,
isInitializeRequest,
createErrorResponse,
createSuccessResponse,
} from '../mcp/mcp-types.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
type JSONRPCMessage,
type CallToolResult
} from '@modelcontextprotocol/sdk/types.js';
/**
* Main bridge class connecting MCP and DVM protocols
*/
export class ContextVMMCPBridge {
private mcpServer: Server;
private nostrTransport!: NostrServerTransport;
private pool: SimplePool;
private translator: MCPDVMTranslator;
private logger: pino.Logger;
private pendingRequests: Map<string, {
resolve: (value: NostrEvent) => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}>;
private isRunning: boolean = false;
constructor(
private config: Config,
private contexts: ContextMapping[]
) {
this.logger = pino({
level: config.bridge.logLevel,
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
},
},
});
this.translator = new MCPDVMTranslator(contexts);
this.pool = new SimplePool();
this.pendingRequests = new Map();
this.mcpServer = this.createMCPServer();
}
/**
* Start the bridge
*/
async start(): Promise<void> {
if (this.isRunning) {
throw new Error('Bridge is already running');
}
try {
this.logger.info('Starting ContextVM MCP Bridge...');
// 1. Subscribe to DVM results
await this.subscribeToDVMResults();
// 2. Create and start Nostr Server Transport
const privKeyBytes = new Uint8Array(Buffer.from(this.config.bridge.privateKey, 'hex'));
const pubKey = getPublicKey(privKeyBytes);
const npub = nip19.npubEncode(pubKey);
this.nostrTransport = new NostrServerTransport({
signer: new PrivateKeySigner(this.config.bridge.privateKey),
relayHandler: new ApplesauceRelayPool([this.config.relay.url]),
encryptionMode: this.config.mcp.encryptionMode as any,
isPublicServer: this.config.mcp.publicAnnouncement,
serverInfo: {
name: 'ContextVM MCP Bridge',
about: 'Bridge between MCP protocol and ContextVM DVM services',
},
logLevel: this.config.bridge.logLevel as any,
});
// Connect the transport to the server
await this.mcpServer.connect(this.nostrTransport);
await this.nostrTransport.start();
this.isRunning = true;
this.logger.info({
pubkey: pubKey,
npub,
relay: this.config.relay.url,
contexts: this.contexts.length,
tools: (await this.translator.getDVMCapabilitiesAsTools()).length,
}, '🌉 MCP Bridge started successfully');
} catch (error) {
this.logger.error({ error }, 'Failed to start bridge');
throw error;
}
}
/**
* Stop the bridge
*/
async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
this.logger.info('Stopping MCP Bridge...');
// Cancel pending requests
for (const [eventId, pending] of this.pendingRequests.entries()) {
clearTimeout(pending.timeout);
pending.reject(new Error('Bridge shutting down'));
this.pendingRequests.delete(eventId);
}
// Close nostr transport
if (this.nostrTransport) {
await this.nostrTransport.close();
}
// Close pool connections
this.pool.close([this.config.relay.url]);
this.isRunning = false;
this.logger.info('MCP Bridge stopped');
}
/**
* Create MCP Server with handlers
*/
private createMCPServer(): Server {
const server = new Server(
{
name: 'ContextVM MCP Bridge',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
// Tools list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
this.logger.debug('Handling tools/list request');
const tools = await this.translator.getDVMCapabilitiesAsTools();
return { tools };
});
// Tools call handler
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const { name, arguments: args } = request.params;
this.logger.info({ tool: name, args }, 'Handling tools/call request');
try {
// Check if it's a query operation
if (name.endsWith('_query')) {
return await this.handleQueryOperation(name, args);
}
// Translate MCP call to DVM request
const dvmRequest = await this.translator.mcpToolToDVMRequest(name, args);
// Publish DVM request
const requestEvent = await this.publishDVMRequest(dvmRequest);
// Wait for DVM result
const resultEvent = await this.waitForDVMResult(requestEvent.id);
// Translate DVM result to MCP response
const mcpResult = this.translator.dvmResultToMCPResponse(resultEvent);
return mcpResult;
} catch (error) {
this.logger.error({ error, tool: name }, 'Error executing tool');
return {
content: [
{
type: 'text' as const,
text: `❌ Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
} as CallToolResult;
}
});
return server;
}
/**
* Subscribe to DVM result events
*/
private async subscribeToDVMResults(): Promise<void> {
const resultKinds = this.contexts.map((c) => c.jobResultKind);
const filter: Filter = {
kinds: resultKinds,
};
this.logger.debug({ filter }, 'Subscribing to DVM results');
this.pool.subscribeMany(
[this.config.relay.url],
filter,
{
onevent: (event) => {
this.handleDVMResult(event);
},
oneose: () => {
this.logger.debug('DVM subscription EOSE received');
},
}
);
}
/**
* Handle incoming DVM result
*/
private handleDVMResult(event: NostrEvent): void {
// Find the request event ID this is responding to
const requestEventId = event.tags.find(([tag]) => tag === 'e')?.[1];
if (!requestEventId) {
this.logger.warn({ eventId: event.id }, 'DVM result without request reference');
return;
}
const pending = this.pendingRequests.get(requestEventId);
if (!pending) {
this.logger.debug({ requestEventId }, 'No pending request found for DVM result');
return;
}
// Clear timeout and resolve
clearTimeout(pending.timeout);
this.pendingRequests.delete(requestEventId);
this.logger.info(
{ requestEventId, resultEventId: event.id },
'DVM result received'
);
pending.resolve(event);
}
/**
* Publish DVM request
*/
private async publishDVMRequest(dvmRequest: any): Promise<NostrEvent> {
this.logger.debug({ kind: dvmRequest.kind }, 'Publishing DVM request');
// Finalize and sign the event
const privKeyBytes = new Uint8Array(Buffer.from(this.config.bridge.privateKey, 'hex'));
const signedEvent = finalizeEvent(dvmRequest, privKeyBytes);
// Publish to relay
await Promise.any(
this.pool.publish([this.config.relay.url], signedEvent)
);
this.logger.info({ eventId: signedEvent.id, kind: signedEvent.kind }, 'DVM request published');
return signedEvent;
}
/**
* Wait for DVM result with timeout
*/
private waitForDVMResult(requestEventId: string): Promise<NostrEvent> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestEventId);
reject(new Error(`Timeout waiting for DVM result (${this.config.dvm.responseTimeout}ms)`));
}, this.config.dvm.responseTimeout);
this.pendingRequests.set(requestEventId, {
resolve,
reject,
timeout,
});
});
}
/**
* Handle query operations (read-only, no DVM events)
*/
private async handleQueryOperation(toolName: string, args: any): Promise<CallToolResult> {
const lastUnderscoreIndex = toolName.lastIndexOf('_');
const contextId = toolName.substring(0, lastUnderscoreIndex);
const context = this.contexts.find((c) => c.id === contextId);
if (!context) {
throw new Error(`Context ${contextId} not found`);
}
// TODO: Implement actual query logic
// For now, return a placeholder
return {
content: [
{
type: 'text' as const,
text: `🔍 Query operation for ${context.name}\n\nArgs: ${JSON.stringify(args, null, 2)}\n\n⚠️ Query implementation pending`,
},
],
};
}
/**
* Get bridge statistics
*/
getStats() {
return {
isRunning: this.isRunning,
pendingRequests: this.pendingRequests.size,
contexts: this.contexts.length,
relay: this.config.relay.url,
};
}
}