SdkControlTransport.ts
1 /** 2 * SDK MCP Transport Bridge 3 * 4 * This file implements a transport bridge that allows MCP servers running in the SDK process 5 * to communicate with the Claude Code CLI process through control messages. 6 * 7 * ## Architecture Overview 8 * 9 * Unlike regular MCP servers that run as separate processes, SDK MCP servers run in-process 10 * within the SDK. This requires a special transport mechanism to bridge communication between: 11 * - The CLI process (where the MCP client runs) 12 * - The SDK process (where the SDK MCP server runs) 13 * 14 * ## Message Flow 15 * 16 * ### CLI → SDK (via SdkControlClientTransport) 17 * 1. CLI's MCP Client calls a tool → sends JSONRPC request to SdkControlClientTransport 18 * 2. Transport wraps the message in a control request with server_name and request_id 19 * 3. Control request is sent via stdout to the SDK process 20 * 4. SDK's StructuredIO receives the control response and routes it back to the transport 21 * 5. Transport unwraps the response and returns it to the MCP Client 22 * 23 * ### SDK → CLI (via SdkControlServerTransport) 24 * 1. Query receives control request with MCP message and calls transport.onmessage 25 * 2. MCP server processes the message and calls transport.send() with response 26 * 3. Transport calls sendMcpMessage callback with the response 27 * 4. Query's callback resolves the pending promise with the response 28 * 5. Query returns the response to complete the control request 29 * 30 * ## Key Design Points 31 * 32 * - SdkControlClientTransport: StructuredIO tracks pending requests 33 * - SdkControlServerTransport: Query tracks pending requests 34 * - The control request wrapper includes server_name to route to the correct SDK server 35 * - The system supports multiple SDK MCP servers running simultaneously 36 * - Message IDs are preserved through the entire flow for proper correlation 37 */ 38 39 import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' 40 import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' 41 42 /** 43 * Callback function to send an MCP message and get the response 44 */ 45 export type SendMcpMessageCallback = ( 46 serverName: string, 47 message: JSONRPCMessage, 48 ) => Promise<JSONRPCMessage> 49 50 /** 51 * CLI-side transport for SDK MCP servers. 52 * 53 * This transport is used in the CLI process to bridge communication between: 54 * - The CLI's MCP Client (which wants to call tools on SDK MCP servers) 55 * - The SDK process (where the actual MCP server runs) 56 * 57 * It converts MCP protocol messages into control requests that can be sent 58 * through stdout/stdin to the SDK process. 59 */ 60 export class SdkControlClientTransport implements Transport { 61 private isClosed = false 62 63 onclose?: () => void 64 onerror?: (error: Error) => void 65 onmessage?: (message: JSONRPCMessage) => void 66 67 constructor( 68 private serverName: string, 69 private sendMcpMessage: SendMcpMessageCallback, 70 ) {} 71 72 async start(): Promise<void> {} 73 74 async send(message: JSONRPCMessage): Promise<void> { 75 if (this.isClosed) { 76 throw new Error('Transport is closed') 77 } 78 79 // Send the message and wait for the response 80 const response = await this.sendMcpMessage(this.serverName, message) 81 82 // Pass the response back to the MCP client 83 if (this.onmessage) { 84 this.onmessage(response) 85 } 86 } 87 88 async close(): Promise<void> { 89 if (this.isClosed) { 90 return 91 } 92 this.isClosed = true 93 this.onclose?.() 94 } 95 } 96 97 /** 98 * SDK-side transport for SDK MCP servers. 99 * 100 * This transport is used in the SDK process to bridge communication between: 101 * - Control requests coming from the CLI (via stdin) 102 * - The actual MCP server running in the SDK process 103 * 104 * It acts as a simple pass-through that forwards messages to the MCP server 105 * and sends responses back via a callback. 106 * 107 * Note: Query handles all request/response correlation and async flow. 108 */ 109 export class SdkControlServerTransport implements Transport { 110 private isClosed = false 111 112 constructor(private sendMcpMessage: (message: JSONRPCMessage) => void) {} 113 114 onclose?: () => void 115 onerror?: (error: Error) => void 116 onmessage?: (message: JSONRPCMessage) => void 117 118 async start(): Promise<void> {} 119 120 async send(message: JSONRPCMessage): Promise<void> { 121 if (this.isClosed) { 122 throw new Error('Transport is closed') 123 } 124 125 // Simply pass the response back through the callback 126 this.sendMcpMessage(message) 127 } 128 129 async close(): Promise<void> { 130 if (this.isClosed) { 131 return 132 } 133 this.isClosed = true 134 this.onclose?.() 135 } 136 }