Class: MCP::Server::Transports::StdioTransport
- Defined in:
- lib/mcp/server/transports/stdio_transport.rb
Constant Summary collapse
- STATUS_INTERRUPTED =
Signal.list["INT"] + 128
- MAX_LINE_BYTES =
Default upper bound on a single newline-delimited frame. CRuby's
IO#getswithout a limit accumulates bytes until a newline arrives, so a peer that never emits one can grow a single String until the process is OOM-killed. 4 MiB is large enough for any realistic JSON-RPC frame, including base64-embedded images. 4 * 1024 * 1024
Instance Method Summary collapse
- #close ⇒ Object
-
#initialize(server, max_line_bytes: MAX_LINE_BYTES) ⇒ StdioTransport
constructor
A new instance of StdioTransport.
- #open ⇒ Object
- #send_notification(method, params = nil) ⇒ Object
-
#send_request(method, params = nil) ⇒ Object
NOTE: This signature deliberately matches the abstract
Transport#send_requestcontract (method, params = nil) without the cancellation kwargs thatStreamableHTTPTransport#send_requestaccepts. - #send_response(message) ⇒ Object
Methods inherited from Transport
#handle_json_request, #handle_request
Constructor Details
#initialize(server, max_line_bytes: MAX_LINE_BYTES) ⇒ StdioTransport
Returns a new instance of StdioTransport.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/mcp/server/transports/stdio_transport.rb', line 19 def initialize(server, max_line_bytes: MAX_LINE_BYTES) super(server) @open = false @session = nil # Reject `nil` or non-positive values: `IO#gets("\n", nil)` and a negative # limit read without an upper bound, which would silently disable the # protection this option exists to provide. unless max_line_bytes.is_a?(Integer) && max_line_bytes > 0 raise ArgumentError, "max_line_bytes must be a positive Integer" end @max_line_bytes = max_line_bytes $stdin.set_encoding("UTF-8") $stdout.set_encoding("UTF-8") end |
Instance Method Details
#close ⇒ Object
61 62 63 |
# File 'lib/mcp/server/transports/stdio_transport.rb', line 61 def close @open = false end |
#open ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/mcp/server/transports/stdio_transport.rb', line 35 def open @open = true @session = ServerSession.new(server: @server, transport: self) while @open begin line = read_line($stdin) rescue RequestHandlerError => e # Stop accumulating and end the connection gracefully rather than # letting an unbounded read exhaust memory or escape as an uncaught # backtrace. Scoped to the read so genuine request errors raised while # handling a frame are not swallowed here. @open = false MCP.configuration.exception_reporter.call(e, { error: "stdio frame exceeds limit" }) break end break if line.nil? response = @session.handle_json(line.strip) send_response(response) if response end rescue Interrupt warn("\nExiting...") exit(STATUS_INTERRUPTED) end |
#send_notification(method, params = nil) ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/mcp/server/transports/stdio_transport.rb', line 71 def send_notification(method, params = nil) notification = { jsonrpc: "2.0", method: method, } notification[:params] = params if params send_response(notification) true rescue => e MCP.configuration.exception_reporter.call(e, { error: "Failed to send notification" }) false end |
#send_request(method, params = nil) ⇒ Object
NOTE: This signature deliberately matches the abstract Transport#send_request contract
(method, params = nil) without the cancellation kwargs that StreamableHTTPTransport#send_request accepts.
On Ruby 2.7 the project's supported minimum a method that mixes a positional params Hash with
explicit keyword arguments cannot be called as send_request(method, { ... }) - the trailing Hash would be
auto-promoted to keyword arguments. Stdio is single-threaded and blocks on $stdin.gets, so nested-request
cancellation has very limited value here regardless; servers that need cancellation propagation for nested
server-to-client requests should use StreamableHTTPTransport.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/mcp/server/transports/stdio_transport.rb', line 92 def send_request(method, params = nil) request_id = generate_request_id request = { jsonrpc: "2.0", id: request_id, method: method } request[:params] = params if params begin send_response(request) rescue => e MCP.configuration.exception_reporter.call(e, { error: "Failed to send request" }) raise end while @open && (line = read_line($stdin)) begin parsed = JSON.parse(line.strip, symbolize_names: true) rescue JSON::ParserError => e MCP.configuration.exception_reporter.call(e, { error: "Failed to parse response" }) raise end if parsed[:id] == request_id && !parsed.key?(:method) if parsed[:error] raise StandardError, "Client returned an error for #{method} request (code: #{parsed[:error][:code]}): #{parsed[:error][:message]}" end return parsed[:result] else response = @session ? @session.handle(parsed) : @server.handle(parsed) send_response(response) if response end end raise "Transport closed while waiting for response to #{method} request." end |
#send_response(message) ⇒ Object
65 66 67 68 69 |
# File 'lib/mcp/server/transports/stdio_transport.rb', line 65 def send_response() = .is_a?(String) ? : JSON.generate() $stdout.puts() $stdout.flush end |