/** * MCPlet Server base — wraps MCP SDK's low-level Server to include * _meta.mcpletType in tools/list responses as required by the MCPlet spec. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; export type MCPletType = 'read' | 'prepare' | 'action'; export type Visibility = 'model' | 'app'; export interface MCPletToolAuth { required: 'passkey'; enforcement: 'strict' | 'workflow' | 'host-only'; promptMessage?: string; } export interface MCPletToolDef { name: string; description: string; inputSchema: Record; mcpletType: MCPletType; pool?: string; visibility: Visibility[]; auth?: MCPletToolAuth; handler: (args: Record, authPayload?: Record) => Promise; } export class MCPletServer { private readonly server: Server; private readonly tools = new Map(); constructor(private readonly serverName: string, version = '0.1.0') { this.server = new Server( { name: serverName, version }, { capabilities: { tools: {} } }, ); this.setupHandlers(); } registerTool(def: MCPletToolDef): this { this.tools.set(def.name, def); return this; } async listen(): Promise { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`[mcplet-server] ${this.serverName} listening on stdio`); } private setupHandlers(): void { // tools/list — include _meta for MCPlet discovery (Spec Section 5.3.1) this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...this.tools.values()].map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema, _meta: { mcpletType: t.mcpletType, pool: t.pool, visibility: t.visibility, auth: t.auth ?? null, }, })), })); // tools/call this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; const tool = this.tools.get(toolName); if (!tool) { return { content: [{ type: 'text', text: this.errorEnvelope(toolName, `Tool "${toolName}" not found`, 'NOT_FOUND') }], isError: true, }; } // Extract auth payload injected by Host (Spec Section 7.3.1) const requestMeta = request.params._meta as Record | undefined; const authPayload = requestMeta?.mcplet_auth as Record | undefined; // Enforce auth requirement ('strict' = per-call ceremony; 'workflow' = cached ceremony per contextId) if (tool.auth?.required === 'passkey' && (tool.auth.enforcement === 'strict' || tool.auth.enforcement === 'workflow') && !authPayload) { return { content: [{ type: 'text', text: this.errorEnvelope(toolName, 'Authentication required', 'AUTH_REQUIRED', tool.mcpletType) }], isError: true, }; } try { const args = (request.params.arguments ?? {}) as Record; const result = await tool.handler(args, authPayload); const envelope = { result, _meta: { timestamp: new Date().toISOString(), toolId: toolName, mcpletType: tool.mcpletType, visibility: tool.visibility, }, }; return { content: [{ type: 'text', text: JSON.stringify(envelope) }] }; } catch (err) { return { content: [{ type: 'text', text: this.errorEnvelope(toolName, (err as Error).message, 'UNKNOWN_ERROR', tool.mcpletType) }], isError: true, }; } }); } private errorEnvelope(toolId: string, message: string, code: string, mcpletType: MCPletType = 'read'): string { return JSON.stringify({ error: { message, code }, _meta: { timestamp: new Date().toISOString(), toolId, mcpletType, visibility: ['model'] }, }); } }