@@ -72,7 +72,9 @@ def call(env)
7272 when [ "POST" , [ TIDEWAVE_ROUTE , MCP_ROUTE ] ]
7373 mcp_endpoint ( request )
7474 else
75- not_found
75+ # The MCP Streamable HTTP transport requires the MCP endpoint to answer
76+ # non-POST methods with 405 (GET without SSE support, DELETE, etc.)
77+ path == [ TIDEWAVE_ROUTE , MCP_ROUTE ] ? method_not_allowed ( ) : not_found ( )
7678 end
7779 else
7880 strip_x_frame_options ( @app . call ( env ) )
@@ -83,7 +85,7 @@ def call(env)
8385
8486 def strip_x_frame_options ( response )
8587 status , headers , body = response
86- headers . delete ( "X-Frame-Options " )
88+ headers . delete ( "x-frame-options " )
8789 [ status , headers , body ]
8890 end
8991
@@ -105,27 +107,46 @@ def home_endpoint(_request)
105107 end
106108
107109 def config_endpoint ( request )
108- json_response ( config_data ( request ) , headers : { "Access-Control-Allow-Origin " => "*" } )
110+ json_response ( config_data ( request ) , headers : { "access-control-allow-origin " => "*" } )
109111 end
110112
111113 def mcp_endpoint ( request )
112- body = request . body . read
114+ message = JSON . parse ( request . body . read )
113115
114- message = JSON . parse ( body )
115- validation_error = validate_jsonrpc_message ( message )
116- return jsonrpc_error_response ( nil , -32600 , validation_error ) if validation_error
117-
118- response = handle_mcp_message ( message )
119- return json_response ( { "status" => "ok" } , status : 202 ) if response . nil?
120-
121- json_response ( response )
116+ if message . is_a? ( Array )
117+ handle_mcp_batch ( message )
118+ else
119+ handle_mcp_single ( message )
120+ end
122121 rescue JSON ::ParserError
123- jsonrpc_error_response ( nil , -32700 , "Parse error" )
122+ jsonrpc_error_response ( nil , -32700 , "Parse error" , status : 400 )
124123 rescue StandardError => error
125124 @logger &.error ( "Error handling MCP request: #{ error . message } " )
126125 jsonrpc_error_response ( nil , -32603 , "Internal error" )
127126 end
128127
128+ def handle_mcp_single ( message )
129+ validation_error = validate_jsonrpc_message ( message )
130+ return jsonrpc_error_response ( nil , -32600 , validation_error , status : 400 ) if validation_error
131+
132+ response = handle_mcp_message ( message )
133+ response . nil? ? accepted_response : json_response ( response )
134+ end
135+
136+ def handle_mcp_batch ( messages )
137+ return jsonrpc_error_response ( nil , -32600 , "Invalid Request" , status : 400 ) if messages . empty?
138+
139+ responses = messages . map { |message | handle_mcp_batch_message ( message ) } . compact
140+ responses . empty? ? accepted_response : json_response ( responses )
141+ end
142+
143+ def handle_mcp_batch_message ( message )
144+ validation_error = validate_jsonrpc_message ( message )
145+ return jsonrpc_error_response_body ( nil , -32600 , validation_error ) if validation_error
146+
147+ handle_mcp_message ( message )
148+ end
149+
129150 def config_data ( request )
130151 {
131152 "project_name" => @options [ :project_name ] ,
@@ -151,14 +172,27 @@ def not_found
151172 text_response ( 404 , "Not Found" )
152173 end
153174
175+ def method_not_allowed
176+ status , headers , body = text_response ( 405 , "Method Not Allowed" )
177+ [ status , headers . merge ( "allow" => "POST" ) , body ]
178+ end
179+
180+ def accepted_response
181+ [ 202 , { "content-length" => "0" } , [ ] ]
182+ end
183+
154184 def text_response ( status , message )
155185 [ status , response_headers ( "text/plain; charset=utf-8" , message ) , [ message ] ]
156186 end
157187
188+ # Rack 3 requires response header keys to be lowercase. Capitalized keys break
189+ # case-sensitive middleware such as Rack::Deflater, which strips "content-length"
190+ # before gzipping; a surviving "Content-Length" leaves a stale (uncompressed)
191+ # length on the compressed body and hangs spec-compliant HTTP clients.
158192 def response_headers ( content_type , body )
159193 {
160- "Content-Type " => content_type ,
161- "Content-Length " => body . bytesize . to_s
194+ "content-type " => content_type ,
195+ "content-length " => body . bytesize . to_s
162196 }
163197 end
164198
@@ -192,22 +226,25 @@ def validate_jsonrpc_message(message)
192226
193227 has_id = message . key? ( "id" )
194228 has_method = message . key? ( "method" )
195- has_result = message . key? ( "result" )
229+ has_result = message . key? ( "result" ) || message . key? ( "error" )
196230
197231 return nil if has_method
198232 return nil if has_id && has_result
199233
200234 "Invalid JSON-RPC message structure"
201235 end
202236
237+ # Returns the JSON-RPC response for a request, or nil for messages that
238+ # must not be replied to (notifications and client-sent responses), which
239+ # the transport acknowledges with 202 Accepted.
203240 def handle_mcp_message ( message )
241+ return nil unless message . key? ( "method" ) && message . key? ( "id" )
242+
204243 method = message [ "method" ]
205244 request_id = message [ "id" ]
206245 params = message [ "params" ] . is_a? ( Hash ) ? message [ "params" ] : { }
207246
208247 case method
209- when "notifications/initialized" , "notifications/cancelled"
210- nil
211248 when "ping"
212249 jsonrpc_success_response_body ( request_id , { } )
213250 when "initialize"
@@ -216,6 +253,12 @@ def handle_mcp_message(message)
216253 jsonrpc_success_response_body ( request_id , { "tools" => tool_definitions } )
217254 when "tools/call"
218255 handle_tool_call ( request_id , params )
256+ when "prompts/list"
257+ jsonrpc_success_response_body ( request_id , { "prompts" => [ ] } )
258+ when "resources/list"
259+ jsonrpc_success_response_body ( request_id , { "resources" => [ ] } )
260+ when "resources/templates/list"
261+ jsonrpc_success_response_body ( request_id , { "resourceTemplates" => [ ] } )
219262 else
220263 {
221264 "jsonrpc" => "2.0" ,
@@ -233,14 +276,9 @@ def handle_initialize(request_id, params)
233276 client_version = params [ "protocolVersion" ]
234277 return jsonrpc_error_response_body ( request_id , -32602 , "Protocol version is required" ) if client_version . nil? || client_version . empty?
235278
236- if client_version < PROTOCOL_VERSION
237- return jsonrpc_error_response_body (
238- request_id ,
239- -32602 ,
240- "Unsupported protocol version. Server supports #{ PROTOCOL_VERSION } or later"
241- )
242- end
243-
279+ # Version negotiation: when the client requests a version we don't
280+ # support, we respond with the version we do support and the client
281+ # decides whether to continue or disconnect.
244282 jsonrpc_success_response_body ( request_id , {
245283 "protocolVersion" => PROTOCOL_VERSION ,
246284 "capabilities" => { "tools" => { "listChanged" => false } } ,
@@ -276,8 +314,8 @@ def jsonrpc_success_response_body(request_id, result)
276314 }
277315 end
278316
279- def jsonrpc_error_response ( request_id , code , message )
280- json_response ( jsonrpc_error_response_body ( request_id , code , message ) )
317+ def jsonrpc_error_response ( request_id , code , message , status : 200 )
318+ json_response ( jsonrpc_error_response_body ( request_id , code , message ) , status : status )
281319 end
282320
283321 def jsonrpc_error_response_body ( request_id , code , message )
0 commit comments