/ services / mcp / SdkControlTransport.ts
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  }