transport.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * MCP Stdio Transport
  3. *
  4. * Handles JSON-RPC 2.0 communication over stdin/stdout for MCP protocol.
  5. */
  6. import * as readline from 'readline';
  7. import { captureException } from '../sentry';
  8. /**
  9. * JSON-RPC 2.0 Request
  10. */
  11. export interface JsonRpcRequest {
  12. jsonrpc: '2.0';
  13. id: string | number;
  14. method: string;
  15. params?: unknown;
  16. }
  17. /**
  18. * JSON-RPC 2.0 Response
  19. */
  20. export interface JsonRpcResponse {
  21. jsonrpc: '2.0';
  22. id: string | number | null;
  23. result?: unknown;
  24. error?: JsonRpcError;
  25. }
  26. /**
  27. * JSON-RPC 2.0 Error
  28. */
  29. export interface JsonRpcError {
  30. code: number;
  31. message: string;
  32. data?: unknown;
  33. }
  34. /**
  35. * JSON-RPC 2.0 Notification (no id, no response expected)
  36. */
  37. export interface JsonRpcNotification {
  38. jsonrpc: '2.0';
  39. method: string;
  40. params?: unknown;
  41. }
  42. // Standard JSON-RPC error codes
  43. export const ErrorCodes = {
  44. ParseError: -32700,
  45. InvalidRequest: -32600,
  46. MethodNotFound: -32601,
  47. InvalidParams: -32602,
  48. InternalError: -32603,
  49. } as const;
  50. export type MessageHandler = (message: JsonRpcRequest | JsonRpcNotification) => Promise<void>;
  51. /**
  52. * Stdio Transport for MCP
  53. *
  54. * Reads JSON-RPC messages from stdin and writes responses to stdout.
  55. */
  56. export class StdioTransport {
  57. private rl: readline.Interface | null = null;
  58. private messageHandler: MessageHandler | null = null;
  59. /**
  60. * Start listening for messages on stdin
  61. */
  62. start(handler: MessageHandler): void {
  63. this.messageHandler = handler;
  64. this.rl = readline.createInterface({
  65. input: process.stdin,
  66. output: process.stdout,
  67. terminal: false,
  68. });
  69. this.rl.on('line', async (line) => {
  70. await this.handleLine(line);
  71. });
  72. this.rl.on('close', () => {
  73. process.exit(0);
  74. });
  75. }
  76. /**
  77. * Stop listening
  78. */
  79. stop(): void {
  80. if (this.rl) {
  81. this.rl.close();
  82. this.rl = null;
  83. }
  84. }
  85. /**
  86. * Send a response
  87. */
  88. send(response: JsonRpcResponse): void {
  89. const json = JSON.stringify(response);
  90. process.stdout.write(json + '\n');
  91. }
  92. /**
  93. * Send a notification (no id)
  94. */
  95. notify(method: string, params?: unknown): void {
  96. const notification: JsonRpcNotification = {
  97. jsonrpc: '2.0',
  98. method,
  99. params,
  100. };
  101. process.stdout.write(JSON.stringify(notification) + '\n');
  102. }
  103. /**
  104. * Send a success response
  105. */
  106. sendResult(id: string | number, result: unknown): void {
  107. this.send({
  108. jsonrpc: '2.0',
  109. id,
  110. result,
  111. });
  112. }
  113. /**
  114. * Send an error response
  115. */
  116. sendError(id: string | number | null, code: number, message: string, data?: unknown): void {
  117. this.send({
  118. jsonrpc: '2.0',
  119. id,
  120. error: { code, message, data },
  121. });
  122. }
  123. /**
  124. * Handle an incoming line of JSON
  125. */
  126. private async handleLine(line: string): Promise<void> {
  127. const trimmed = line.trim();
  128. if (!trimmed) return;
  129. let parsed: unknown;
  130. try {
  131. parsed = JSON.parse(trimmed);
  132. } catch {
  133. this.sendError(null, ErrorCodes.ParseError, 'Parse error: invalid JSON');
  134. return;
  135. }
  136. // Validate basic JSON-RPC structure
  137. if (!this.isValidMessage(parsed)) {
  138. this.sendError(null, ErrorCodes.InvalidRequest, 'Invalid Request: not a valid JSON-RPC 2.0 message');
  139. return;
  140. }
  141. if (this.messageHandler) {
  142. try {
  143. await this.messageHandler(parsed as JsonRpcRequest | JsonRpcNotification);
  144. } catch (err) {
  145. captureException(err, { operation: 'mcp-message-handler' });
  146. const message = parsed as JsonRpcRequest;
  147. if ('id' in message) {
  148. this.sendError(
  149. message.id,
  150. ErrorCodes.InternalError,
  151. `Internal error: ${err instanceof Error ? err.message : String(err)}`
  152. );
  153. }
  154. }
  155. }
  156. }
  157. /**
  158. * Check if message is a valid JSON-RPC 2.0 message
  159. */
  160. private isValidMessage(msg: unknown): boolean {
  161. if (typeof msg !== 'object' || msg === null) return false;
  162. const obj = msg as Record<string, unknown>;
  163. if (obj.jsonrpc !== '2.0') return false;
  164. if (typeof obj.method !== 'string') return false;
  165. return true;
  166. }
  167. }