Skip to content

Conversation

bigH
Copy link

@bigH bigH commented Jul 31, 2025

See: #99

Currently, this implementation of MCP requires that sessions are held in memory for Server-Sent Events (SSEs). The session data maps an ID to a live IO stream. This doesn't work for Chime for a few reasons:

  • We run >1 replica, so in-memory storage of session state is not going to work
  • When contacting a backend with no knowledge of a given session, the server responds with an error, breaking downstream agent functionality
  • Chime's default configuration doesn't allow for long-lived connections and connections are terminated forcibly after a timeout

So, here we add tests and validate that stateless configuration behaves according to spec. In situations where the spec is unclear about something, we defer to the Python SDK.

@koic
Copy link
Member

koic commented Aug 1, 2025

Does handle_post method need to handle the case for stateless?

@bigH
Copy link
Author

bigH commented Aug 1, 2025

Does handle_post method need to handle the case for stateless?

My understanding is that it does not. handle_post calls handle_regular_request, which only uses SSE if a stream exists.

I've tested this against my own service and it works properly. What would it take to get this merged?

@koic
Copy link
Member

koic commented Aug 2, 2025

Ah, I see. Thank you for the explanation. Can you squash your commits into one?

@bigH
Copy link
Author

bigH commented Aug 5, 2025

Happy to.

@bigH bigH force-pushed the stateless branch 2 times, most recently from a2c47d2 to ca3537e Compare August 5, 2025 17:32
@bigH
Copy link
Author

bigH commented Aug 5, 2025

Rebased on to latest main and squashed my commits.

koic
koic previously approved these changes Aug 6, 2025
- Create failing tests for `stateless` mode - Implement `stateless` mode by turning of SSE in Streamable HTTP, making the interaction a standard HTTP Req/Resp
@bigH
Copy link
Author

bigH commented Aug 6, 2025

Bump @koic. I think this is good to review/merge.

bigH and others added 2 commits August 12, 2025 22:27
Co-authored-by: Koichi ITO <koic.ito@gmail.com>
Co-authored-by: Koichi ITO <koic.ito@gmail.com>

response = stateless_transport.handle_request(request)
assert_equal 202, response[0]
assert_equal({ "Content-Type" => "application/json" }, response[1])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this assertion is failing due to the changes in #105. Can you rebase with the latest main branch and update this assertion as follows?

Suggested change
assert_equal({ "Content-Type" => "application/json" }, response[1])
assert_empty(response[1])
@koic
Copy link
Member

koic commented Aug 20, 2025

@bigH This looks good overall to me. Can you rebase on the latest main branch, make sure the tests pass, and squash the commits into a single one?

@Ginja
Copy link
Contributor

Ginja commented Aug 21, 2025

I tested this change on an internal MCP server and it looks good 👍

@Ginja
Copy link
Contributor

Ginja commented Aug 21, 2025

On further testing I may have found a bug with this feature, but only when specifically using socketry/falcon (vs Puma).

{ "time": "2025-08-21T20:28:04+00:00", "severity": "error", "pid": 20, "oid": 1176, "fiber_id": 1192, "subject": "Protocol::Rack::Adapter::Rack31", "annotation": "Reading HTTP/1.1 requests for Async::HTTP::Protocol::HTTP1::Server.", "message": "undefined method 'bytesize' for nil", "event": { "type": "failure", "root": "/app", "class": "NoMethodError", "message": "undefined method 'bytesize' for nil", "backtrace": [ "/artifacts/bundle/ruby/3.4.0/gems/protocol-rack-0.16.0/lib/protocol/rack/body/enumerable.rb:27:in 'Array#sum'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-rack-0.16.0/lib/protocol/rack/body/enumerable.rb:27:in 'Protocol::Rack::Body::Enumerable.wrap'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-rack-0.16.0/lib/protocol/rack/body.rb:64:in 'Protocol::Rack::Body.wrap'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-rack-0.16.0/lib/protocol/rack/response.rb:53:in 'Protocol::Rack::Response.wrap'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-rack-0.16.0/lib/protocol/rack/adapter/generic.rb:160:in 'Protocol::Rack::Adapter::Generic#call'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-http-0.52.0/lib/protocol/http/middleware.rb:53:in 'Protocol::HTTP::Middleware#call'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-http-0.52.0/lib/protocol/http/content_encoding.rb:40:in 'Protocol::HTTP::ContentEncoding#call'", "/artifacts/bundle/ruby/3.4.0/gems/protocol-http-0.52.0/lib/protocol/http/middleware.rb:53:in 'Protocol::HTTP::Middleware#call'", "/artifacts/bundle/ruby/3.4.0/gems/falcon-0.52.3/lib/falcon/server.rb:66:in 'Falcon::Server#call'", "/artifacts/bundle/ruby/3.4.0/gems/async-http-0.91.0/lib/async/http/server.rb:57:in 'block in Async::HTTP::Server#accept'", "/artifacts/bundle/ruby/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http1/server.rb:72:in 'Async::HTTP::Protocol::HTTP1::Server#each'", "/artifacts/bundle/ruby/3.4.0/gems/async-http-0.91.0/lib/async/http/server.rb:49:in 'Async::HTTP::Server#accept'", "/artifacts/bundle/ruby/3.4.0/gems/falcon-0.52.3/lib/falcon/server.rb:57:in 'Falcon::Server#accept'", "/artifacts/bundle/ruby/3.4.0/gems/io-endpoint-0.15.2/lib/io/endpoint/wrapper.rb:216:in 'block (2 levels) in IO::Endpoint::Wrapper#accept'", "/artifacts/bundle/ruby/3.4.0/gems/async-2.27.4/lib/async/task.rb:183:in 'block in Async::Task#run'", "/artifacts/bundle/ruby/3.4.0/gems/async-2.27.4/lib/async/task.rb:427:in 'block in Async::Task#schedule'" ] } } 

Digging into it, it looks like nil is not a valid response with Falcon. Adding the following to streamable_http_transport.rb fixes the issue. Something we may want to consider for those using Falcon.

 def handle_regular_request(body_string, session_id) unless @stateless # If session ID is provided, but not in the sessions hash, return an error if session_id && !@sessions.key?(session_id) return [400, { "Content-Type" => "application/json" }, [{ error: "Invalid session ID" }.to_json]] end end # Ensure we always have a valid response body - convert nil to empty string response = @server.handle_json(body_string) || ""
@bigH
Copy link
Author

bigH commented Aug 23, 2025

I'll come back to this next week and handle what you shared @Ginja @koic

@koic
Copy link
Member

koic commented Aug 24, 2025

Can you add user-facing documentation to the README?

@Ginja
Copy link
Contributor

Ginja commented Oct 6, 2025

@bigH any chance you have time to revisit this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
3 participants