ruby-sdk icon indicating copy to clipboard operation
ruby-sdk copied to clipboard

Implement Streamable HTTP transport

Open Ginja opened this issue 7 months ago • 5 comments

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 changes
  • notify_prompts_list_changed() - Send a notification when the prompts list changes
  • notify_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

Ginja avatar May 30 '25 18:05 Ginja

I also thought a bit about a possible interface... and my idea so far was

  1. 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

  1. Add a send_notification or maybe more generic send_to_client to 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

  2. 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

kfischer-okarin avatar May 31 '25 01:05 kfischer-okarin

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).

Ginja avatar May 31 '25 18:05 Ginja

Added 3 new commits:

  • Converted rack to 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.

Ginja avatar Jun 02 '25 21:06 Ginja

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 avatar Jun 04 '25 05:06 Ginja

@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
#...

kfischer-okarin avatar Jun 04 '25 14:06 kfischer-okarin

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 ;)

kfischer-okarin avatar Jun 11 '25 01:06 kfischer-okarin

Documentation missing?

Animeshz avatar Jul 27 '25 14:07 Animeshz