mcp 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +174 -5
  3. data/lib/mcp/client/stdio.rb +222 -0
  4. data/lib/mcp/client.rb +12 -2
  5. data/lib/mcp/progress.rb +21 -0
  6. data/lib/mcp/prompt.rb +4 -0
  7. data/lib/mcp/resource.rb +3 -0
  8. data/lib/mcp/server/transports/stdio_transport.rb +1 -1
  9. data/lib/mcp/server/transports/streamable_http_transport.rb +7 -19
  10. data/lib/mcp/server/transports.rb +10 -0
  11. data/lib/mcp/server.rb +40 -4
  12. data/lib/mcp/server_context.rb +26 -0
  13. data/lib/mcp/tool.rb +5 -0
  14. data/lib/mcp/version.rb +1 -1
  15. data/lib/mcp.rb +10 -24
  16. metadata +7 -36
  17. data/.gitattributes +0 -4
  18. data/.github/dependabot.yml +0 -6
  19. data/.github/workflows/ci.yml +0 -54
  20. data/.github/workflows/conformance.yml +0 -29
  21. data/.github/workflows/release.yml +0 -57
  22. data/.gitignore +0 -11
  23. data/.rubocop.yml +0 -15
  24. data/AGENTS.md +0 -107
  25. data/CHANGELOG.md +0 -168
  26. data/CODE_OF_CONDUCT.md +0 -74
  27. data/Gemfile +0 -29
  28. data/RELEASE.md +0 -12
  29. data/Rakefile +0 -56
  30. data/SECURITY.md +0 -21
  31. data/bin/console +0 -15
  32. data/bin/generate-gh-pages.sh +0 -119
  33. data/bin/rake +0 -31
  34. data/bin/setup +0 -8
  35. data/conformance/README.md +0 -103
  36. data/conformance/expected_failures.yml +0 -9
  37. data/conformance/runner.rb +0 -101
  38. data/conformance/server.rb +0 -547
  39. data/dev.yml +0 -30
  40. data/docs/_config.yml +0 -6
  41. data/docs/index.md +0 -7
  42. data/docs/latest/index.html +0 -19
  43. data/examples/README.md +0 -197
  44. data/examples/http_client.rb +0 -184
  45. data/examples/http_server.rb +0 -169
  46. data/examples/stdio_server.rb +0 -94
  47. data/examples/streamable_http_client.rb +0 -207
  48. data/examples/streamable_http_server.rb +0 -172
  49. data/mcp.gemspec +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 149cbf6f8809235111648ffae6163abb1994cb5c2a4bd2240eb696f17197e421
4
- data.tar.gz: 7c526f405b013d868032122484079ce715ef8db9629d78dc5c8635fccaf2fc6f
3
+ metadata.gz: d5cd1f7d23be518ff8bff1b1710ff8e8c4ba86ba3d55417e991f921057c0841d
4
+ data.tar.gz: 8e6bba0111698a39ff5aeaa0b4a34e51822018ac943b13b828f9bd2965062ddb
5
5
  SHA512:
6
- metadata.gz: b7dcdebe8faa024fe784a894ae6383d2bcd812f41a3a492c75429034ed8f062a8da49963a94a159ce2af25928cccfbb92cb629be98ead1d751d54389e3dfc9a3
7
- data.tar.gz: 9c6f1fb122e3d636da0a7585fe38bd3ea567ba98c2899d91ef87136b69e8d0c93007c3f031915b44c835d16bbcd1465875decc459e5a34aada517541b91d369f
6
+ metadata.gz: 1f9689d2ecb0a2b4e5ba6888e515d577a12d1f41cd48be8459c2d90ad3b7e4cbe7f557c23aac134a8426bf7bfaba2c3579ab496817ade2ff0fd24056286e52cd
7
+ data.tar.gz: 03601ddc6bf751a75ec6bbadc67458ab9d49367feac7b834d90072028e41a2d1046ed3da6551832482884f435c8779620071f1aa5df66753cf9afade090eb220
data/README.md CHANGED
@@ -108,11 +108,12 @@ The server supports sending notifications to clients when lists of tools, prompt
108
108
 
109
109
  #### Notification Methods
110
110
 
111
- The server provides three notification methods:
111
+ The server provides the following notification methods:
112
112
 
113
113
  - `notify_tools_list_changed` - Send a notification when the tools list changes
114
114
  - `notify_prompts_list_changed` - Send a notification when the prompts list changes
115
115
  - `notify_resources_list_changed` - Send a notification when the resources list changes
116
+ - `notify_progress` - Send a progress notification for long-running operations
116
117
  - `notify_log_message` - Send a structured logging notification message
117
118
 
118
119
  #### Notification Format
@@ -122,8 +123,72 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
122
123
  - `notifications/tools/list_changed`
123
124
  - `notifications/prompts/list_changed`
124
125
  - `notifications/resources/list_changed`
126
+ - `notifications/progress`
125
127
  - `notifications/message`
126
128
 
129
+ ### Progress
130
+
131
+ The MCP Ruby SDK supports progress tracking for long-running tool operations,
132
+ following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
133
+
134
+ #### How Progress Works
135
+
136
+ 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
137
+ 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
138
+ 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
139
+
140
+ #### Server-Side: Tool with Progress
141
+
142
+ Tools that accept a `server_context:` parameter can call `report_progress` on it.
143
+ The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
144
+
145
+ ```ruby
146
+ class LongRunningTool < MCP::Tool
147
+ description "A tool that reports progress during execution"
148
+ input_schema(
149
+ properties: {
150
+ count: { type: "integer" },
151
+ },
152
+ required: ["count"]
153
+ )
154
+
155
+ def self.call(count:, server_context:)
156
+ count.times do |i|
157
+ # Do work here.
158
+ server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
159
+ end
160
+
161
+ MCP::Tool::Response.new([{ type: "text", text: "Done" }])
162
+ end
163
+ end
164
+ ```
165
+
166
+ The `server_context.report_progress` method accepts:
167
+
168
+ - `progress` (required) — current progress value (numeric)
169
+ - `total:` (optional) — total expected value, so clients can display a percentage
170
+ - `message:` (optional) — human-readable status message
171
+
172
+ #### Server-Side: Direct `notify_progress` Usage
173
+
174
+ You can also call `notify_progress` directly on the server instance:
175
+
176
+ ```ruby
177
+ server.notify_progress(
178
+ progress_token: "token-123",
179
+ progress: 50,
180
+ total: 100, # optional
181
+ message: "halfway" # optional
182
+ )
183
+ ```
184
+
185
+ **Key Features:**
186
+
187
+ - Tools report progress via `server_context.report_progress`
188
+ - `report_progress` is a no-op when no `progressToken` was provided by the client
189
+ - `notify_progress` is a no-op when no transport is configured
190
+ - Supports both numeric and string progress tokens
191
+
127
192
  ### Logging
128
193
 
129
194
  The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
@@ -242,11 +307,12 @@ When added to a Rails controller on a route that handles POST requests, your ser
242
307
  [Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
243
308
  requests.
244
309
 
245
- You can use the `Server#handle_json` method to handle requests.
310
+ You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
311
+ status codes (e.g., 202 Accepted for notifications).
246
312
 
247
313
  ```ruby
248
- class ApplicationController < ActionController::Base
249
- def index
314
+ class McpController < ActionController::Base
315
+ def create
250
316
  server = MCP::Server.new(
251
317
  name: "my_server",
252
318
  title: "Example Server Display Name",
@@ -256,7 +322,11 @@ class ApplicationController < ActionController::Base
256
322
  prompts: [MyPrompt],
257
323
  server_context: { user_id: current_user.id },
258
324
  )
259
- render(json: server.handle_json(request.body.read))
325
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
326
+ server.transport = transport
327
+ status, headers, body = transport.handle_request(request)
328
+
329
+ render(json: body.first, status: status, headers: headers)
260
330
  end
261
331
  end
262
332
  ```
@@ -375,6 +445,50 @@ server = MCP::Server.new(
375
445
 
376
446
  This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
377
447
 
448
+ #### Request-specific `_meta` Parameter
449
+
450
+ The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`.
451
+
452
+ **Access Pattern:**
453
+
454
+ When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
455
+
456
+ ```ruby
457
+ class MyTool < MCP::Tool
458
+ def self.call(message:, server_context:)
459
+ # Access provider-specific metadata
460
+ session_id = server_context.dig(:_meta, :session_id)
461
+ request_id = server_context.dig(:_meta, :request_id)
462
+
463
+ # Access server's original context
464
+ user_id = server_context.dig(:user_id)
465
+
466
+ MCP::Tool::Response.new([{
467
+ type: "text",
468
+ text: "Processing for user #{user_id} in session #{session_id}"
469
+ }])
470
+ end
471
+ end
472
+ ```
473
+
474
+ **Client Request Example:**
475
+
476
+ ```json
477
+ {
478
+ "jsonrpc": "2.0",
479
+ "id": 1,
480
+ "method": "tools/call",
481
+ "params": {
482
+ "name": "my_tool",
483
+ "arguments": { "message": "Hello" },
484
+ "_meta": {
485
+ "session_id": "abc123",
486
+ "request_id": "req_456"
487
+ }
488
+ }
489
+ }
490
+ ```
491
+
378
492
  #### Configuration Block Data
379
493
 
380
494
  ##### Exception Reporter
@@ -968,6 +1082,52 @@ class CustomTransport
968
1082
  end
969
1083
  ```
970
1084
 
1085
+ ### Stdio Transport Layer
1086
+
1087
+ Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
1088
+
1089
+ `MCP::Client::Stdio.new` accepts the following keyword arguments:
1090
+
1091
+ | Parameter | Required | Description |
1092
+ |---|---|---|
1093
+ | `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
1094
+ | `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
1095
+ | `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
1096
+ | `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
1097
+
1098
+ Example usage:
1099
+
1100
+ ```ruby
1101
+ stdio_transport = MCP::Client::Stdio.new(
1102
+ command: "bundle",
1103
+ args: ["exec", "ruby", "path/to/server.rb"],
1104
+ env: { "API_KEY" => "my_secret_key" },
1105
+ read_timeout: 30
1106
+ )
1107
+ client = MCP::Client.new(transport: stdio_transport)
1108
+
1109
+ # List available tools.
1110
+ tools = client.tools
1111
+ tools.each do |tool|
1112
+ puts "Tool: #{tool.name} - #{tool.description}"
1113
+ end
1114
+
1115
+ # Call a specific tool.
1116
+ response = client.call_tool(
1117
+ tool: tools.first,
1118
+ arguments: { message: "Hello, world!" }
1119
+ )
1120
+
1121
+ # Close the transport when done.
1122
+ stdio_transport.close
1123
+ ```
1124
+
1125
+ The stdio transport automatically handles:
1126
+
1127
+ - Spawning the server process with `Open3.popen3`
1128
+ - MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
1129
+ - JSON-RPC 2.0 message framing over newline-delimited JSON
1130
+
971
1131
  ### HTTP Transport Layer
972
1132
 
973
1133
  Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
@@ -1000,8 +1160,17 @@ response = client.call_tool(
1000
1160
  tool: tools.first,
1001
1161
  arguments: { message: "Hello, world!" }
1002
1162
  )
1163
+
1164
+ # Call a tool with progress tracking.
1165
+ response = client.call_tool(
1166
+ tool: tools.first,
1167
+ arguments: { count: 10 },
1168
+ progress_token: "my-progress-token"
1169
+ )
1003
1170
  ```
1004
1171
 
1172
+ The server will send `notifications/progress` back to the client during execution.
1173
+
1005
1174
  #### HTTP Authorization
1006
1175
 
1007
1176
  By default, the HTTP transport layer provides no authentication to the server, but you can provide custom headers if you need authentication. For example, to use Bearer token authentication:
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "securerandom"
6
+ require "timeout"
7
+ require_relative "../../json_rpc_handler"
8
+ require_relative "../configuration"
9
+ require_relative "../methods"
10
+ require_relative "../version"
11
+
12
+ module MCP
13
+ class Client
14
+ class Stdio
15
+ # Seconds to wait for the server process to exit before sending SIGTERM.
16
+ # Matches the Python and TypeScript SDKs' shutdown timeout:
17
+ # https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/stdio/__init__.py#L48
18
+ # https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/stdio.ts#L221
19
+ CLOSE_TIMEOUT = 2
20
+ STDERR_READ_SIZE = 4096
21
+
22
+ attr_reader :command, :args, :env
23
+
24
+ def initialize(command:, args: [], env: nil, read_timeout: nil)
25
+ @command = command
26
+ @args = args
27
+ @env = env
28
+ @read_timeout = read_timeout
29
+ @stdin = nil
30
+ @stdout = nil
31
+ @stderr = nil
32
+ @wait_thread = nil
33
+ @stderr_thread = nil
34
+ @started = false
35
+ @initialized = false
36
+ end
37
+
38
+ def send_request(request:)
39
+ start unless @started
40
+ initialize_session unless @initialized
41
+
42
+ write_message(request)
43
+ read_response(request)
44
+ end
45
+
46
+ def start
47
+ raise "MCP::Client::Stdio already started" if @started
48
+
49
+ spawn_env = @env || {}
50
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(spawn_env, @command, *@args)
51
+ @stdout.set_encoding("UTF-8")
52
+ @stdin.set_encoding("UTF-8")
53
+
54
+ # Drain stderr in the background to prevent the pipe buffer from filling up,
55
+ # which would cause the server process to block and deadlock.
56
+ @stderr_thread = Thread.new do
57
+ loop do
58
+ @stderr.readpartial(STDERR_READ_SIZE)
59
+ end
60
+ rescue IOError
61
+ nil
62
+ end
63
+
64
+ @started = true
65
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOEXEC => e
66
+ raise RequestHandlerError.new(
67
+ "Failed to spawn server process: #{e.message}",
68
+ {},
69
+ error_type: :internal_error,
70
+ original_error: e,
71
+ )
72
+ end
73
+
74
+ def close
75
+ return unless @started
76
+
77
+ @stdin.close
78
+ @stdout.close
79
+ @stderr.close
80
+
81
+ begin
82
+ Timeout.timeout(CLOSE_TIMEOUT) { @wait_thread.value }
83
+ rescue Timeout::Error
84
+ begin
85
+ Process.kill("TERM", @wait_thread.pid)
86
+ Timeout.timeout(CLOSE_TIMEOUT) { @wait_thread.value }
87
+ rescue Timeout::Error
88
+ begin
89
+ Process.kill("KILL", @wait_thread.pid)
90
+ rescue Errno::ESRCH
91
+ nil
92
+ end
93
+ rescue Errno::ESRCH
94
+ nil
95
+ end
96
+ end
97
+
98
+ @stderr_thread.join(CLOSE_TIMEOUT)
99
+ @started = false
100
+ @initialized = false
101
+ end
102
+
103
+ private
104
+
105
+ # The client MUST send a protocol version it supports. This SHOULD be the latest version.
106
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation
107
+ #
108
+ # Always sends `LATEST_STABLE_PROTOCOL_VERSION`, matching the Python and TypeScript SDKs:
109
+ # https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/session.py#L175
110
+ # https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/index.ts#L495
111
+ def initialize_session
112
+ init_request = {
113
+ jsonrpc: JsonRpcHandler::Version::V2_0,
114
+ id: SecureRandom.uuid,
115
+ method: MCP::Methods::INITIALIZE,
116
+ params: {
117
+ protocolVersion: MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION,
118
+ capabilities: {},
119
+ clientInfo: { name: "mcp-ruby-client", version: MCP::VERSION },
120
+ },
121
+ }
122
+
123
+ write_message(init_request)
124
+ response = read_response(init_request)
125
+
126
+ if response.key?("error")
127
+ error = response["error"]
128
+ raise RequestHandlerError.new(
129
+ "Server initialization failed: #{error["message"]}",
130
+ { method: MCP::Methods::INITIALIZE },
131
+ error_type: :internal_error,
132
+ )
133
+ end
134
+
135
+ unless response.key?("result")
136
+ raise RequestHandlerError.new(
137
+ "Server initialization failed: missing result in response",
138
+ { method: MCP::Methods::INITIALIZE },
139
+ error_type: :internal_error,
140
+ )
141
+ end
142
+
143
+ notification = {
144
+ jsonrpc: JsonRpcHandler::Version::V2_0,
145
+ method: MCP::Methods::NOTIFICATIONS_INITIALIZED,
146
+ }
147
+ write_message(notification)
148
+
149
+ @initialized = true
150
+ end
151
+
152
+ def write_message(message)
153
+ ensure_running!
154
+ json = JSON.generate(message)
155
+ @stdin.puts(json)
156
+ @stdin.flush
157
+ rescue IOError, Errno::EPIPE => e
158
+ raise RequestHandlerError.new(
159
+ "Failed to write to server process",
160
+ {},
161
+ error_type: :internal_error,
162
+ original_error: e,
163
+ )
164
+ end
165
+
166
+ def read_response(request)
167
+ request_id = request[:id] || request["id"]
168
+ method = request[:method] || request["method"]
169
+ params = request[:params] || request["params"]
170
+
171
+ loop do
172
+ ensure_running!
173
+ wait_for_readable!(method, params) if @read_timeout
174
+ line = @stdout.gets
175
+ raise_connection_error!(method, params) if line.nil?
176
+
177
+ parsed = JSON.parse(line.strip)
178
+
179
+ next unless parsed.key?("id")
180
+
181
+ return parsed if parsed["id"] == request_id
182
+ end
183
+ rescue JSON::ParserError => e
184
+ raise RequestHandlerError.new(
185
+ "Failed to parse server response",
186
+ { method: method, params: params },
187
+ error_type: :internal_error,
188
+ original_error: e,
189
+ )
190
+ end
191
+
192
+ def ensure_running!
193
+ return if @wait_thread.alive?
194
+
195
+ raise RequestHandlerError.new(
196
+ "Server process has exited",
197
+ {},
198
+ error_type: :internal_error,
199
+ )
200
+ end
201
+
202
+ def wait_for_readable!(method, params)
203
+ ready = @stdout.wait_readable(@read_timeout)
204
+ return if ready
205
+
206
+ raise RequestHandlerError.new(
207
+ "Timed out waiting for server response",
208
+ { method: method, params: params },
209
+ error_type: :internal_error,
210
+ )
211
+ end
212
+
213
+ def raise_connection_error!(method, params)
214
+ raise RequestHandlerError.new(
215
+ "Server process closed stdout unexpectedly",
216
+ { method: method, params: params },
217
+ error_type: :internal_error,
218
+ )
219
+ end
220
+ end
221
+ end
222
+ end
data/lib/mcp/client.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "client/stdio"
4
+ require_relative "client/http"
5
+ require_relative "client/tool"
6
+
3
7
  module MCP
4
8
  class Client
5
9
  # Initializes a new MCP::Client instance.
@@ -90,6 +94,7 @@ module MCP
90
94
  #
91
95
  # @param tool [MCP::Client::Tool] The tool to be called.
92
96
  # @param arguments [Object, nil] The arguments to pass to the tool.
97
+ # @param progress_token [String, Integer, nil] A token to request progress notifications from the server during tool execution.
93
98
  # @return [Hash] The full JSON-RPC response from the transport.
94
99
  #
95
100
  # @example
@@ -100,12 +105,17 @@ module MCP
100
105
  # @note
101
106
  # The exact requirements for `arguments` are determined by the transport layer in use.
102
107
  # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
103
- def call_tool(tool:, arguments: nil)
108
+ def call_tool(tool:, arguments: nil, progress_token: nil)
109
+ params = { name: tool.name, arguments: arguments }
110
+ if progress_token
111
+ params[:_meta] = { progressToken: progress_token }
112
+ end
113
+
104
114
  transport.send_request(request: {
105
115
  jsonrpc: JsonRpcHandler::Version::V2_0,
106
116
  id: request_id,
107
117
  method: "tools/call",
108
- params: { name: tool.name, arguments: arguments },
118
+ params: params,
109
119
  })
110
120
  end
111
121
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Progress
5
+ def initialize(server:, progress_token:)
6
+ @server = server
7
+ @progress_token = progress_token
8
+ end
9
+
10
+ def report(progress, total: nil, message: nil)
11
+ return unless @progress_token
12
+
13
+ @server.notify_progress(
14
+ progress_token: @progress_token,
15
+ progress: progress,
16
+ total: total,
17
+ message: message,
18
+ )
19
+ end
20
+ end
21
+ end
data/lib/mcp/prompt.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "prompt/argument"
4
+ require_relative "prompt/message"
5
+ require_relative "prompt/result"
6
+
3
7
  module MCP
4
8
  class Prompt
5
9
  class << self
data/lib/mcp/resource.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "resource/contents"
4
+ require_relative "resource/embedded"
5
+
3
6
  module MCP
4
7
  class Resource
5
8
  attr_reader :uri, :name, :title, :description, :icons, :mime_type
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../transport"
4
3
  require "json"
4
+ require_relative "../../transport"
5
5
 
6
6
  module MCP
7
7
  class Server
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../transport"
4
3
  require "json"
5
4
  require "securerandom"
5
+ require_relative "../../transport"
6
6
 
7
7
  module MCP
8
8
  class Server
@@ -154,13 +154,7 @@ module MCP
154
154
  return success_response
155
155
  end
156
156
 
157
- session_id = request.env["HTTP_MCP_SESSION_ID"]
158
-
159
- return [
160
- 400,
161
- { "Content-Type" => "application/json" },
162
- [{ error: "Missing session ID" }.to_json],
163
- ] unless session_id
157
+ return missing_session_id_response unless (session_id = request.env["HTTP_MCP_SESSION_ID"])
164
158
 
165
159
  cleanup_session(session_id)
166
160
  success_response
@@ -193,6 +187,8 @@ module MCP
193
187
  return not_acceptable_response(required_types) unless accept_header
194
188
 
195
189
  accepted_types = parse_accept_header(accept_header)
190
+ return if accepted_types.include?("*/*")
191
+
196
192
  missing_types = required_types - accepted_types
197
193
  return not_acceptable_response(required_types) unless missing_types.empty?
198
194
 
@@ -257,31 +253,23 @@ module MCP
257
253
 
258
254
  def handle_regular_request(body_string, session_id)
259
255
  unless @stateless
260
- # If session ID is provided, but not in the sessions hash, return an error
261
- if session_id && !@sessions.key?(session_id)
262
- return [400, { "Content-Type" => "application/json" }, [{ error: "Invalid session ID" }.to_json]]
256
+ if session_id && !session_exists?(session_id)
257
+ return session_not_found_response
263
258
  end
264
259
  end
265
260
 
266
- response = @server.handle_json(body_string) || ""
261
+ response = @server.handle_json(body_string)
267
262
 
268
263
  # Stream can be nil since stateless mode doesn't retain streams
269
264
  stream = get_session_stream(session_id) if session_id
270
265
 
271
266
  if stream
272
267
  send_response_to_stream(stream, response, session_id)
273
- elsif response.nil? && notification_request?(body_string)
274
- [202, { "Content-Type" => "application/json" }, [response]]
275
268
  else
276
269
  [200, { "Content-Type" => "application/json" }, [response]]
277
270
  end
278
271
  end
279
272
 
280
- def notification_request?(body_string)
281
- body = parse_request_body(body_string)
282
- body.is_a?(Hash) && body["method"].start_with?("notifications/")
283
- end
284
-
285
273
  def get_session_stream(session_id)
286
274
  @mutex.synchronize { @sessions[session_id]&.fetch(:stream, nil) }
287
275
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Server
5
+ module Transports
6
+ autoload :StdioTransport, "mcp/server/transports/stdio_transport"
7
+ autoload :StreamableHTTPTransport, "mcp/server/transports/streamable_http_transport"
8
+ end
9
+ end
10
+ end