import type { NostrEvent } from 'nostr-tools';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import type { ContextMapping } from '../mapping/contexts.js';
import type { MCPTool } from '../mcp/mcp-types.js';
import type { DVMJobRequest, ParsedDVMEvent } from '../dvm/dvm-types.js';
import { parseDVMEvent, buildDVMJobRequest } from '../dvm/dvm-types.js';
/**
* Translator between MCP and DVM protocols
*/
export class MCPDVMTranslator {
constructor(private contexts: ContextMapping[]) {}
/**
* Get all DVM capabilities as MCP Tools
*/
async getDVMCapabilitiesAsTools(): Promise<MCPTool[]> {
const tools: MCPTool[] = [];
for (const context of this.contexts) {
// Create tool (creates new instance)
tools.push({
name: `${context.id}_create`,
description: `Create new ${context.name} instance`,
inputSchema: {
type: 'object',
properties: this.convertSchemaToJSONSchema(context.rootEntity.schema),
required: context.rootEntity.required,
},
});
// Transition tools
for (const transition of context.rootEntity.transitions) {
const properties: Record<string, any> = {
token_id: {
type: 'string',
description: 'ID of the token/instance',
},
};
if (transition.inputSchema) {
Object.assign(properties, this.convertSchemaToJSONSchema(transition.inputSchema));
}
tools.push({
name: `${context.id}_${transition.name}`,
description: `${transition.description} (${transition.from} → ${transition.to})`,
inputSchema: {
type: 'object',
properties,
required: ['token_id', ...(transition.inputSchema ? Object.keys(transition.inputSchema).filter(k =>
transition.inputSchema![k].required === true
) : [])],
},
});
}
// Query tool
if (context.queries?.enabled) {
tools.push({
name: `${context.id}_query`,
description: `Query ${context.name} state`,
inputSchema: {
type: 'object',
properties: {
token_id: {
type: 'string',
description: 'ID of the token to query',
},
filters: {
type: 'object',
description: 'Additional filters (optional)',
},
},
required: [],
},
});
}
}
return tools;
}
/**
* Translate MCP tool call to DVM job request
*/
async mcpToolToDVMRequest(toolName: string, args: any): Promise<DVMJobRequest> {
// Extract action (last part after last underscore)
const lastUnderscoreIndex = toolName.lastIndexOf('_');
const contextId = toolName.substring(0, lastUnderscoreIndex);
const action = toolName.substring(lastUnderscoreIndex + 1);
const context = this.contexts.find((c) => c.id === contextId);
if (!context) {
throw new Error(`Context ${contextId} not found`);
}
const tags: string[][] = [['L', context.namespace]];
if (action === 'create') {
// Create new instance
tags.push(
['l', context.rootEntity.initialState, context.namespace],
['token_tipo', context.rootEntity.type],
['source', 'mcp-bridge']
);
// Add inputs
for (const [key, value] of Object.entries(args)) {
tags.push(['input', key, String(value)]);
}
} else if (action === 'query') {
// Query is handled differently (not a job request)
throw new Error('Query operations should be handled separately');
} else {
// State transition
const transition = context.rootEntity.transitions.find((t) => t.name === action);
if (!transition) {
throw new Error(`Transition ${action} not found in context ${contextId}`);
}
if (!args.token_id) {
throw new Error('token_id is required for transitions');
}
tags.push(
['token_id', args.token_id],
['proceso_estado_inicial', transition.from],
['proceso_estado_final', transition.to],
['l', transition.from, context.namespace],
['source', 'mcp-bridge']
);
// Add transition inputs
for (const [key, value] of Object.entries(args)) {
if (key !== 'token_id') {
tags.push(['input', key, String(value)]);
}
}
}
return buildDVMJobRequest(context.jobRequestKind, tags, {
source: 'mcp-bridge',
timestamp: Date.now(),
toolName,
});
}
/**
* Translate DVM result to MCP response
*/
dvmResultToMCPResponse(dvmEvent: NostrEvent): CallToolResult {
const parsed = parseDVMEvent(dvmEvent);
// Check for errors
if (parsed.status === 'error' || parsed.error) {
return {
content: [
{
type: 'text' as const,
text: `❌ Error: ${parsed.error || 'Unknown error'}`,
},
],
isError: true,
};
}
// Format success response
const resultText = this.formatDVMResult(parsed);
return {
content: [
{
type: 'text' as const,
text: resultText,
},
],
};
}
/**
* Format DVM result as human-readable text
*/
private formatDVMResult(parsed: ParsedDVMEvent): string {
let text = '✅ Operation completed successfully\n\n';
if (parsed.tokenId) {
text += `📄 Token ID: ${parsed.tokenId}\n`;
}
if (parsed.state) {
text += `📊 Current State: ${parsed.state}\n`;
}
if (parsed.namespace) {
text += `🏷️ Context: ${parsed.namespace}\n`;
}
if (parsed.content?.data) {
text += '\n📦 Data:\n';
text += '```json\n';
text += JSON.stringify(parsed.content.data, null, 2);
text += '\n```\n';
}
text += `\n🔗 Event ID: ${parsed.eventId}`;
text += `\n🕐 Timestamp: ${new Date().toISOString()}`;
return text;
}
/**
* Convert internal schema format to JSON Schema
*/
private convertSchemaToJSONSchema(schema: Record<string, any>): Record<string, any> {
const jsonSchema: Record<string, any> = {};
for (const [key, value] of Object.entries(schema)) {
if (typeof value === 'object' && value !== null) {
jsonSchema[key] = { ...value };
} else {
jsonSchema[key] = { type: value };
}
}
return jsonSchema;
}
/**
* Find context by namespace
*/
findContextByNamespace(namespace: string): ContextMapping | undefined {
return this.contexts.find((c) => c.namespace === namespace);
}
/**
* Find context by job result kind
*/
findContextByResultKind(kind: number): ContextMapping | undefined {
return this.contexts.find((c) => c.jobResultKind === kind);
}
}