Implement Streamable HTTP transport
Implements a rack-compatible Streamable HTTP transport with SSE support. As well as add notification support for the server with the following methods:
notify_tools_list_changed()- Send a notification when the tools list changesnotify_prompts_list_changed()- Send a notification when the prompts list changesnotify_resources_list_changed()- Send a notification when the resources list changes
Closes #4
Motivation and Context
To fulfill the Streamable HTTP transport specification as described here.
How Has This Been Tested?
Local tests only (see examples/). Working on implementing it with my MCP server that's a WIP.
Breaking Changes
Should introduce no breaking changes as using the StreamableHTTP transport is optional
Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [x] Documentation update
Checklist
- [x] I have read the MCP Documentation
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
Local Tests
Started with run options --seed 59936
132/132: [============================================================================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.21315s
132 tests, 405 assertions, 0 failures, 0 errors, 0 skips
I also thought a bit about a possible interface... and my idea so far was
- add a method like this to the
Server
def support_notifications(transport, tools_list_changed: false, resources_list_changed: false, resources_subscribe: false, prompts_list_changed: false, logging: false)
which would be called by the server implementer depending on the needs/capabilities of the application
-
Add a
send_notificationor maybe more genericsend_to_clientto the transport (since I think all transports use the same mechanism for notifications and server send requests) - which in case of STDIO would just send to stdout -
Add all protocol methods as public methods to the server which uses the transport internally to just send it
def notify_tools_list_changed
@transport.send_to_client(method: Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED)
end
def notify_resources_list_changed
# ...
def notify_resource_changed(uri)
# ...
def notify_prompts_list_changed
# ...
def log_to_client(data, level:, logger: nil)
# ...
Sending those notifications will be the server implementer's responsibility anyways so adding them easy to use to the server interface seems best....
I was planning to send a PoC Pull Request sooner or later when I got time... but if you like I could make one quite soon (maybe during the weekend) so it's maybe easier to talk about
I like what you've proposed for the notifications. I can look to add that to my PR next week.
Also about the current approach - I think this should be better marked as a StandaloneHTTPTransport - since I'm not sure that could be integrated into a Rails/Sinatra/Hanami/etc app as is.....
I prefer not to rename this. This is providing a barebone implementation of the StreamableHTTP transport. I think it's best to leave it up to implementers as to how they want to expose their MCP servers. Either directly with Rack & a web server (e.g. Puma, Unicorn, WEBrick, etc...) or contained within a web framework (e.g. Rails, Sinatra, etc...).
I prefer to keep this gem free of strong opinions (e.g. no nicities provided for wrapping this in a Rails controller, or including default/expected routes).
But I think the proper road ahead for those frameworks would be maybe to offer separate mcp-rails-transport mcp-sinatra-transport etc addon gems which provide their specific Transport implementation and the respective frameworks idiomatic Plugin boilerplate
💯 this. It should not be this gem's concern to facilitate the smooth integration into web frameworks. We can however, provide examples in the documentation (which could refer to the potential framework-specific gems).
Added 3 new commits:
- Converted
rackto a development dependency (00fb685c0007f3a5906a0f37503b0eb3369e9a59) - Removed transport-specific code from server (44a35b2cacc0e1b2df8584fb4dfa1d1999ae47d1)
- Added initial notification support based on @kfischer-okarin's suggestions (fb2b06b522b11565d1bd8f8a4b33e56e0727eac6)
@kfischer-okarin I added you as a co-author, I hope you don't mind. I figured it was the right thing to do as you proposed the design ❤️ If you prefer I can drop this commit and this support can be added in a dedicated PR. One method I skipped adding was this:
def support_notifications(transport, tools_list_changed: false, resources_list_changed: false, resources_subscribe: false, prompts_list_changed: false, logging: false)
I wasn't completely sure what the implementation looked like. I figured this can be added later.
Latest set of commits are just a rebase after some new conflicts.
Started with run options --seed 28896
132/132: [===========================================================================================================================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.22367s
132 tests, 405 assertions, 0 failures, 0 errors, 0 skips
$ bundle exec rubocop
Inspecting 49 files
.................................................
49 files inspected, no offenses detected
@Ginja
I added you as a co-author, I hope you don't mind. I figured it was the right thing to do as you proposed the design ❤️
Thanks very much appreciated :)
One method I skipped adding was this:
def support_notifications(transport, tools_list_changed: false, resources_list_changed: false, resources_subscribe: false, prompts_list_changed: false, logging: false)
That method is basicallt an explicit expression of the fact that notifications are optional, and since the Object doesn't need a Transport reference unless it supports notifications, I thought it would be ok if we only add it here and not in the constructor.
As for the flags - those would play into determining the capabilities hash
I submitted a little Capabilities related refactor as #42 - if that is adopted.... adding the respective capabilities dynamically depending on which flag is specified should be easy along the lines of
@capabilities.support_tools_list_changed if tools_list_changed
#...
Also as an additional note:
I'm half-way finished working on a little minimal version containing a subset of this branch that implements the general list changed notifications support for STDIO - (of course properly attributing @Ginja as Co-Author everywhere where I used any code from this PR) - since this PR seems to get quite big and if the discussion/review goes on it might still take a while until any value is provided to the main branch...
Unless you just want to go on with this branch as is which is also fine of course ;)
Documentation missing?