137 lines
4.4 KiB
TypeScript
137 lines
4.4 KiB
TypeScript
/**
|
|
* SDK MCP Transport Bridge
|
|
*
|
|
* This file implements a transport bridge that allows MCP servers running in the SDK process
|
|
* to communicate with the Claude Code CLI process through control messages.
|
|
*
|
|
* ## Architecture Overview
|
|
*
|
|
* Unlike regular MCP servers that run as separate processes, SDK MCP servers run in-process
|
|
* within the SDK. This requires a special transport mechanism to bridge communication between:
|
|
* - The CLI process (where the MCP client runs)
|
|
* - The SDK process (where the SDK MCP server runs)
|
|
*
|
|
* ## Message Flow
|
|
*
|
|
* ### CLI → SDK (via SdkControlClientTransport)
|
|
* 1. CLI's MCP Client calls a tool → sends JSONRPC request to SdkControlClientTransport
|
|
* 2. Transport wraps the message in a control request with server_name and request_id
|
|
* 3. Control request is sent via stdout to the SDK process
|
|
* 4. SDK's StructuredIO receives the control response and routes it back to the transport
|
|
* 5. Transport unwraps the response and returns it to the MCP Client
|
|
*
|
|
* ### SDK → CLI (via SdkControlServerTransport)
|
|
* 1. Query receives control request with MCP message and calls transport.onmessage
|
|
* 2. MCP server processes the message and calls transport.send() with response
|
|
* 3. Transport calls sendMcpMessage callback with the response
|
|
* 4. Query's callback resolves the pending promise with the response
|
|
* 5. Query returns the response to complete the control request
|
|
*
|
|
* ## Key Design Points
|
|
*
|
|
* - SdkControlClientTransport: StructuredIO tracks pending requests
|
|
* - SdkControlServerTransport: Query tracks pending requests
|
|
* - The control request wrapper includes server_name to route to the correct SDK server
|
|
* - The system supports multiple SDK MCP servers running simultaneously
|
|
* - Message IDs are preserved through the entire flow for proper correlation
|
|
*/
|
|
|
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
|
|
|
|
/**
|
|
* Callback function to send an MCP message and get the response
|
|
*/
|
|
export type SendMcpMessageCallback = (
|
|
serverName: string,
|
|
message: JSONRPCMessage,
|
|
) => Promise<JSONRPCMessage>
|
|
|
|
/**
|
|
* CLI-side transport for SDK MCP servers.
|
|
*
|
|
* This transport is used in the CLI process to bridge communication between:
|
|
* - The CLI's MCP Client (which wants to call tools on SDK MCP servers)
|
|
* - The SDK process (where the actual MCP server runs)
|
|
*
|
|
* It converts MCP protocol messages into control requests that can be sent
|
|
* through stdout/stdin to the SDK process.
|
|
*/
|
|
export class SdkControlClientTransport implements Transport {
|
|
private isClosed = false
|
|
|
|
onclose?: () => void
|
|
onerror?: (error: Error) => void
|
|
onmessage?: (message: JSONRPCMessage) => void
|
|
|
|
constructor(
|
|
private serverName: string,
|
|
private sendMcpMessage: SendMcpMessageCallback,
|
|
) {}
|
|
|
|
async start(): Promise<void> {}
|
|
|
|
async send(message: JSONRPCMessage): Promise<void> {
|
|
if (this.isClosed) {
|
|
throw new Error('Transport is closed')
|
|
}
|
|
|
|
// Send the message and wait for the response
|
|
const response = await this.sendMcpMessage(this.serverName, message)
|
|
|
|
// Pass the response back to the MCP client
|
|
if (this.onmessage) {
|
|
this.onmessage(response)
|
|
}
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
if (this.isClosed) {
|
|
return
|
|
}
|
|
this.isClosed = true
|
|
this.onclose?.()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SDK-side transport for SDK MCP servers.
|
|
*
|
|
* This transport is used in the SDK process to bridge communication between:
|
|
* - Control requests coming from the CLI (via stdin)
|
|
* - The actual MCP server running in the SDK process
|
|
*
|
|
* It acts as a simple pass-through that forwards messages to the MCP server
|
|
* and sends responses back via a callback.
|
|
*
|
|
* Note: Query handles all request/response correlation and async flow.
|
|
*/
|
|
export class SdkControlServerTransport implements Transport {
|
|
private isClosed = false
|
|
|
|
constructor(private sendMcpMessage: (message: JSONRPCMessage) => void) {}
|
|
|
|
onclose?: () => void
|
|
onerror?: (error: Error) => void
|
|
onmessage?: (message: JSONRPCMessage) => void
|
|
|
|
async start(): Promise<void> {}
|
|
|
|
async send(message: JSONRPCMessage): Promise<void> {
|
|
if (this.isClosed) {
|
|
throw new Error('Transport is closed')
|
|
}
|
|
|
|
// Simply pass the response back through the callback
|
|
this.sendMcpMessage(message)
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
if (this.isClosed) {
|
|
return
|
|
}
|
|
this.isClosed = true
|
|
this.onclose?.()
|
|
}
|
|
}
|