Compare commits

..
233 changed files with 2862 additions and 55049 deletions

View file

@ -4,22 +4,16 @@ on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
entry:
- { ruby: '2.7.0', allowed-failure: false } # Minimum supported Ruby version.
- { ruby: '3.0', allowed-failure: false }
- { ruby: '3.1', allowed-failure: false }
- { ruby: '3.2', allowed-failure: false }
- { ruby: '3.3', allowed-failure: false }
- { ruby: '3.4', allowed-failure: false }
- { ruby: '4.0', allowed-failure: false }
- { ruby: 'head', allowed-failure: true }
- { ruby: 3.2, allowed-failure: false }
- { ruby: 3.3, allowed-failure: false }
- { ruby: 3.4, allowed-failure: false }
- { ruby: head, allowed-failure: true }
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
@ -29,26 +23,11 @@ jobs:
rubocop:
runs-on: ubuntu-latest
permissions:
contents: read
name: RuboCop
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0 # Specify the latest supported Ruby version.
ruby-version: 3.2 # Specify the oldest supported Ruby version.
bundler-cache: true
- run: bundle exec rake rubocop
yard:
runs-on: ubuntu-latest
permissions:
contents: read
name: YARD Documentation
steps:
- uses: actions/checkout@v7
- uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0 # Specify the latest supported Ruby version.
bundler-cache: true
- run: bundle exec yard --no-output

View file

@ -1,29 +0,0 @@
name: Conformance Tests
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: conformance-${{ github.ref }}-${{ github.event_name == 'push' && github.sha || '' }}
cancel-in-progress: true
permissions:
contents: read
jobs:
server-conformance:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v7
- uses: ruby/setup-ruby@v1
with:
ruby-version: '4.0' # Specify the latest supported Ruby version.
bundler-cache: true
- uses: actions/setup-node@v7
with:
node-version: '24' # Specify the latest Node.js version.
- run: bundle exec rake conformance:test

View file

@ -16,42 +16,10 @@ jobs:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: 4.0
ruby-version: 3.4
- uses: rubygems/release-gem@v1
publish_gh_pages:
if: github.repository_owner == 'modelcontextprotocol'
name: Publish Documentation to GitHub Pages
runs-on: ubuntu-latest
needs: [publish_gem]
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Get version tag
id: version
run: |
git fetch --tags
TAG=$(git describe --tags --exact-match HEAD)
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Generate GitHub Pages
run: ./bin/generate-gh-pages.sh ${{ steps.version.outputs.tag }}
- name: Push to gh-pages
run: git push origin gh-pages

5
.gitignore vendored
View file

@ -1,5 +1,4 @@
.ruby-version
/*.gem
/.bundle/
/.yardoc
/_yardoc/
@ -9,7 +8,3 @@
/spec/reports/
/tmp/
Gemfile.lock
docs/_site/
docs/.jekyll-cache/
docs/.jekyll-metadata
docs/_data/versions.yml

View file

@ -5,19 +5,8 @@ plugins:
- rubocop-minitest
- rubocop-rake
AllCops:
TargetRubyVersion: 2.7
SuggestExtensions: false
Gemspec/DevelopmentDependencies:
Enabled: true
Lint/IncompatibleIoSelectWithFiberScheduler:
Enabled: true
Minitest/LiteralAsActualArgument:
Enabled: true
Naming/MethodName:
AllowedPatterns:
- '\Atest_'

View file

@ -6,18 +6,16 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
## Dev environment setup
- Ruby 3.2.0+ required to run the full test suite, including all Sorbet-related features
- Ruby 3.2.0+ required
- Run `bundle install` to install dependencies
- Dependencies: `json-schema` >= 4.1 - Schema validation
- Dependencies: `json_rpc_handler` ~> 0.1, `json-schema` >= 4.1
## Build and test commands
- `bundle install` - Install dependencies
- `rake test` - Run all tests
- `rake rubocop` - Run linter
- `rake` - Run linting, tests, and the conformance suite (default task)
- `bundle exec rake conformance:test` - Run the MCP conformance suite (see conformance/README.md)
- `bundle exec rake docs:preview` - Serve the documentation site locally at http://localhost:4000 (PORT to override)
- `rake` - Run tests and linting (default task)
- `ruby -I lib -I test test/path/to/specific_test.rb` - Run single test file
- `gem build mcp.gemspec` - Build the gem
@ -34,15 +32,6 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
- Use frozen string literals
- Follow Ruby community conventions
- Keep dependencies minimal
- Use lowercase HTTP response header names (e.g. `mcp-session-id`); the Rack 3 SPEC requires this, and the MCP spec's `Mcp-Session-Id` casing is prose convention only
## Documentation
- User-facing documentation lives in `docs/`, one page per topic, published at https://ruby.sdk.modelcontextprotocol.io (deploys only when a release is published)
- Pages live in the `docs/_server/`, `docs/_client/`, and `docs/_extensions/` collections
- Keep README.md slim: quick start and pointers only; document features on the relevant docs page
- Internal links are absolute and extensionless (e.g. `/server/tools/`); front matter is followed by a blank line before the h1
- Callout tiers: `.note` (supplementary), `.important` (spec constraints), `.warning` (deprecated features)
## Commit message conventions
@ -67,18 +56,12 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
**MCP::Server** (`lib/mcp/server.rb`):
- Main server class handling JSON-RPC requests and holding shared configuration (tools, prompts, resources, handlers, capabilities)
- Main server class handling JSON-RPC requests
- Implements MCP protocol methods: initialize, ping, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/read
- Supports custom method registration via `define_custom_method`
- Handles instrumentation, exception reporting, and notifications
- Uses JsonRpcHandler for request processing
**MCP::ServerSession** (`lib/mcp/server_session.rb`):
- Per-connection state: client info, logging level
- Created by the transport layer for each client connection
- Delegates request handling to the shared `Server`
**MCP::Client** (`lib/mcp/client.rb`):
- Client interface for communicating with MCP servers
@ -112,21 +95,25 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
- Protocol version 2025-03-26+ supports tool annotations (destructive_hint, idempotent_hint, etc.)
- Validation is configurable via `configuration.validate_tool_call_arguments`
**Session Architecture**:
- `Server` holds shared configuration (tools, prompts, resources, handlers)
- `ServerSession` holds per-connection state (client info, logging level)
- Both `StdioTransport` and `StreamableHTTPTransport` create a `ServerSession` per connection, making the session model transparent across transports
- Session-scoped notifications (`notify_progress`, `notify_log_message`) are sent only to the originating client via `ServerSession`
- Server-wide notifications (`notify_tools_list_changed`, etc.) broadcast to all sessions via `Server`
**Context Passing**:
- `server_context` hash passed through tool/prompt calls for request-specific data
- Methods can accept `server_context:` keyword argument for accessing context
### Dependencies
- `json_rpc_handler` ~> 0.1 - JSON-RPC 2.0 message handling
- `json-schema` >= 4.1 - Schema validation
- Ruby 3.2.0+ required
### Integration patterns
- **Rails/Rack apps**: Mount `StreamableHTTPTransport` as a Rack app (e.g., `mount transport => "/mcp"`)
- **Rails controllers**: Use `server.handle_json(request.body.read)` for HTTP endpoints
- **Command-line tools**: Use `StdioTransport.new(server).open` for CLI applications
- **HTTP services**: Use `StreamableHTTPTransport` for web-based servers
- **HTTP services**: Use `StreamableHttpTransport` for web-based servers
### Component definition patterns
1. **Class inheritance**: `class MyTool < MCP::Tool`
2. **Define methods**: `MCP::Tool.define(name: "my_tool") { ... }`
3. **Server registration**: `server.define_tool(name: "my_tool") { ... }`

View file

@ -7,513 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.5.1] - 2026-09-09
This release keeps the HTTP client working with json 3.0. That release accepts the options of `JSON.parse`
as keywords only, and Faraday's JSON response middleware, through 2.14.3, passes them as a positional Hash,
which Ruby 3 no longer converts, so every JSON body the client received failed as an internal error.
The client now parses response bodies itself, the way it already handled streamed ones, and no longer
registers the middleware. A malformed JSON body delivered whole by an adapter without streaming support
now raises `RequestHandlerError` with `error_type: :parse_error`, as the streamed path already did.
### Fixed
- Parse HTTP client response bodies without Faraday's JSON middleware, which json 3.0 breaks (#546)
## [1.5.0] - 2026-09-05
This release makes the client answer a server's `ping` with the empty result the specification requires.
Earlier clients replied with Method not found over Streamable HTTP and stayed silent over stdio,
so a server that checks liveness dropped their long-lived sessions. On the OAuth side,
the embedding application can refuse an authorization request through `authorization_request_validator`,
and stored tokens are refreshed only against the authorization server that issued them:
when the server named for a session differs, the client reauthorizes instead of refreshing,
and the validator sees the newly named server. Tokens stored by earlier releases carry no issuer and keep refreshing.
### Added
- Add `authorization_request_validator` to let the embedding application refuse an authorization request (#539)
### Fixed
- Answer server-to-client pings in the client (#541)
## [1.4.0] - 2026-08-28
This release repairs the `subscriptions/listen` stream. A host that buffers its responses,
the documented Rails controller pattern among them, can decline the method with
`serve_subscriptions_listen: false` instead of answering a modern client with a streaming body
it cannot render, and the stream's ordering guarantees are enforced: no notification reaches
a client before the acknowledgement, and none follows the graceful closing result.
### Added
- Add `serve_subscriptions_listen:` for hosts that cannot hold an SSE stream open (#533)
### Fixed
- Deliver `subscriptions/listen` notifications only after the acknowledgement (#532)
- Serialize `subscriptions/listen` writes so the graceful result is the final message (#535)
## [1.3.0] - 2026-08-22
User-facing documentation now lives on the documentation site at https://ruby.sdk.modelcontextprotocol.io,
and README.md keeps the quick start. Two entries under "Changed" reject traffic that earlier releases accepted
and ship in a minor release under the exceptions described in [VERSIONING.md](VERSIONING.md).
### Added
- Add a `resources_list_handler` for context-dependent resource lists (#509)
- Pass a handler-returned `_meta` through the subscribe result (#510)
### Changed
- Bound OAuth response bodies in the client (#520)
- Reject duplicate in-flight JSON-RPC request ids (#521)
- Move the documentation from README.md to the documentation site (#523)
## [1.2.0] - 2026-08-15
This release completes the SEP-2575 stateless lifecycle of the 2026-07-28 specification, together with
the SEP-2322, SEP-2549, and SEP-2243 features that revision builds on. Several entries under "Changed"
are incompatible with 1.1.0 and ship in a minor release under the spec-conformance and security exceptions
described in [VERSIONING.md](VERSIONING.md).
### Added
- Handle the SEP-2575 modern request envelope in the server core (#475)
- Serve both lifecycle eras over stdio with an era lock per SEP-2575 (#478)
- Serve the sessionless modern path over Streamable HTTP per SEP-2575 (#479)
- Finalize `server/discover` and add client modern lifecycle support per SEP-2575 (#480)
- Let handlers return multi round-trip `input_required` results per SEP-2322 (#481)
- Add `MCP::Elicitation::EnumSchema` builders per SEP-1330 (#482)
- Stamp the required `resultType` on modern results per SEP-2322 (#487)
- Enforce the modern lifecycle admission rules per SEP-2575 (#489)
- Stream modern request notifications and honor the envelope `logLevel` per SEP-2575 (#490)
- Expose the user-defined `server_context` in instrumentation data (#493)
- Serve the `subscriptions/listen` notification stream per SEP-2575 (#495)
- Add opt-in `requestState` sealing via `MCP::Server::RequestStateSecurity` (#496)
- Mirror `x-mcp-header` tool parameters into `Mcp-Param-*` headers per SEP-2243 (#498)
- Stamp the required cache hints on modern cacheable results per SEP-2549 (#499)
- Drive multi round-trip `input_required` results on the client per SEP-2322 (#500)
- Fulfill `input_required` results on the legacy wire per SEP-2322 (#501)
### Changed
- Align modern envelope validation with the finalized specification (#491)
- Require the `Mcp-Method` header on the modern path (#492)
- Bound server-to-client requests with a timeout (#502)
- Refuse server-to-client requests in the modern lifecycle per SEP-2575 (#503)
- Bound the total wait across SSE reconnection attempts (#504)
- Bound automatic pagination in the MCP client (#505)
- Reject modern-removed methods before the connection era locks (#511)
- Stop negotiating modern protocol versions through the `initialize` handshake (#516)
### Deprecated
- Warn on modern client connects that declare the Roots or Sampling capabilities deprecated per SEP-2577 (#406, #516)
### Fixed
- Stop leaking exception messages to clients via JSON-RPC error data (#486)
- Return Invalid Params for unknown prompts and missing prompt arguments (#517)
- Restrict OAuth discovery to same-origin metadata URLs and refuse private-network destinations
## [1.1.0] - 2026-08-01
### Added
- Support 2026-07-28 as the Latest Protocol Version (#476)
- Expose server tool annotations on MCP::Client::Tool (#445)
### Fixed
- Preserve explicit tool response content (#469)
## [1.0.0] - 2026-07-24
First stable release. The public API is now stable: breaking changes ship only in major releases,
apart from the narrow exceptions documented in [VERSIONING.md](VERSIONING.md).
### Added
- Add `ROADMAP.md` outlining the path toward SEP-1730 (#465)
- Add `VERSIONING.md` documenting the versioning and breaking-change policy (#466)
## [0.25.0] - 2026-07-18
### Added
- Support SEP-990 Cross-App Access via ID-JAG and the `jwt-bearer` grant (#454)
- Support client-side sampling via `MCP::Client#on_sampling` (#458)
### Fixed
- Reject notifications carrying an unknown or expired session (#455)
- Handle an `initialize` request sent without an ID (#456)
- Bound client-side message buffering in the HTTP transport (#459)
- Lowercase response header names in `StreamableHTTPTransport` (#460)
## [0.24.0] - 2026-07-12
### Added
- Support SSE reconnection per SEP-1699 in the HTTP client transport (#426)
- Validate the RFC 9207 `iss` authorization response parameter per SEP-2468 (#431)
- Support `private_key_jwt` client authentication for the `client_credentials` grant (#432)
- Recognize multi round-trip `input_required` results per SEP-2322 (#433)
- Add opt-in `ttlMs` / `cacheScope` cache hints to List and Read results per SEP-2549 (#436)
- Add `server/discover` and stateless lifecycle error codes per SEP-2575 (#438)
- Associate server-to-client requests with the originating client request per SEP-2260 (#440)
- Add server-side MCP Apps helpers per SEP-1865 (#441)
- Support client-side elicitation with SEP-1034 schema defaults (#443)
- Support class-based Resource and ResourceTemplate definitions (#447)
### Changed
- Bind stored client credentials to the authorization server issuer per SEP-2352 (#439)
### Fixed
- Handle non-object JSON-RPC messages without raising (#448)
- Perform SSE stream writes outside the session mutex (#449)
- Validate required params for `initialize` and `resources/subscribe` (#451)
## [0.23.0] - 2026-07-07
### Added
- Add a session-ownership hook to `StreamableHTTPTransport`
### Changed
- Require calling `MCP::Client#connect` before sending requests on stdio transport (#427)
### Deprecated
- Annotate Roots, Sampling, and Logging APIs as deprecated per SEP-2577 (#429)
### Fixed
- Fix an incorrect `result: null` response to an id-bearing notification message (#435)
- Validate `Host` and `Origin` headers to prevent DNS rebinding per MCP 2025-11-25
- Bound stateful session retention to prevent an initialize-flood DoS
- Bound stdio frame reads with `max_line_bytes`
- Bound request body and frame reads to prevent memory-exhaustion DoS
## [0.22.0] - 2026-06-27
### Added
- Add `audience` role validation for `MCP::Annotations` per MCP specification (#422)
- Send SEP-2243 `Mcp-Method` and `Mcp-Name` headers per MCP specification (#423)
- Support client-side `notifications/cancelled` per MCP specification (#425)
### Changed
- Conform Tool Schemas to JSON Schema 2020-12 per SEP-2106 (#417)
### Fixed
- Fix a `SyntaxError` on Ruby 2.7.0 caused by arguments forwarding syntax (#419)
## [0.21.0] - 2026-06-20
### Added
- Support capability extensions per SEP-2133 (#405)
- Add range validation for `MCP::Annotations#priority` per MCP specification (#410)
- Isolate stateless requests in ephemeral sessions per SEP-2567 (#415)
### Changed
- Set OIDC `application_type` on Dynamic Client Registration per SEP-837 (#408)
- Fall back to legacy 2025-03-26 OAuth discovery for servers without PRM (#414)
## [0.20.0] - 2026-06-14
### Added
- Support W3C Trace Context Propagation via `_meta` per SEP-414 (#397)
- Support OAuth `client_credentials` grant in OAuth client (#399)
- Add `annotations` field to `MCP::Resource` and `MCP::ResourceTemplate` per MCP specification (#403)
### Changed
- Re-run OAuth flow on 403 `insufficient_scope` (step-up) (#368)
- Speed up `Tool::Schema` validation by 5x to 100x (#369)
- Use JSON-RPC error envelope for `StreamableHTTPTransport` errors (#371)
- Pin RFC 8414 default well-known suffix per SEP-2351 (#395)
- Default missing `MCP-Protocol-Version` to `2025-03-26` in `StreamableHTTPTransport` (#392)
### Fixed
- Preserve the request ID in invalid request error responses (#400)
- Standardize Resource Not Found errors on -32602 with URI data per SEP-2164 (#402)
## [0.19.0] - 2026-06-13
### Added
- Support Client ID Metadata Documents in OAuth client (#361)
- Request `offline_access` scope when supported (#365)
- Add `size` field to `MCP::Resource` per MCP specification (#393)
## [0.18.0] - 2026-05-30
### Added
- Support server-to-client `ping` per MCP specification (#358)
### Changed
- Warn on implicit stdio initialization (#338)
- Cache `Tool::Schema` validation to avoid re-validating identical schemas (#363)
### Fixed
- Fix case-sensitive `Accept` header comparison (#359)
## [0.17.0] - 2026-05-19
### Added
- Add OAuth 2.1 client support for MCP authorization flow (#353)
### Fixed
- Validate `MCP-Protocol-Version` header in `StreamableHTTPTransport` (#347)
- Reject duplicate `initialize` requests (#350)
- Reject non-Hash JSON-RPC bodies in `StreamableHTTPTransport` (#354)
## [0.16.0] - 2026-05-14
### Added
- Add opt-in tool output validation (#344)
### Changed
- Advertise JSON Schema 2020-12 dialect on emitted tool schemas (#342)
### Fixed
- Preserve client tool output schemas (#343)
- Fix missing `output_schema` argument in `define_tool` API (#301)
## [0.15.0] - 2026-05-05
### Added
- Support `notifications/cancelled` per MCP specification (#332)
- Add client-level `connect` for initialize handshake (#327)
- Add client-level `connect` handshake to stdio transport (#336)
### Changed
- Return tool argument validation failures as tool execution errors (#333)
### Removed
- Remove obsolete `MCP::Transports` module (#331)
## [0.14.0] - 2026-04-24
### Added
- Support pagination per MCP specification (#320)
- Support resource subscriptions per MCP specification (#313)
- Add `roots/list` and `notifications/roots/list_changed` support (#315)
- Support JSON response mode for `StreamableHTTPTransport` (#328)
- Add HTTP client close for explicit session termination (#326)
- Track `Mcp-Session-Id` and protocol version in HTTP client (#325)
- Support `ping` client API per MCP specification (#324)
### Fixed
- Handle 202 Accepted response in HTTP client (#323)
### Changed
- Parse SSE responses in HTTP client via `event_stream_parser` (#322)
## [0.13.0] - 2026-04-16
### Added
- Make `StreamableHTTPTransport` a Rack application (#263)
- Support `elicitation/create` per MCP specification (#312)
- Add `around_request` hook for request instrumentation (#309)
- Add `_meta` field to resource, content, and result classes (#310)
### Removed
- Remove `Server#create_sampling_message` direct call (#311)
## [0.12.0] - 2026-04-11
### Added
- Support customizing the Faraday client in `MCP::Client::HTTP` (#306)
### Changed
- Auto-set `server.transport` in `Transport#initialize` (#305)
### Fixed
- Validate Content-Type on POST requests (#304)
## [0.11.0] - 2026-04-06
### Added
- Support `sampling/createMessage` per MCP specification (#282)
- Support `completion/complete` per MCP specification (#289)
### Fixed
- Support POST response SSE streams for server-to-client messages (#294)
- Return protocol errors for invalid arguments and server errors (#285)
- Fix client methods silently swallowing JSON-RPC error responses (#281)
- Close streams outside mutex in session cleanup (#291)
## [0.10.0] - 2026-03-30
### Added
- Session expiry controls for `StreamableHTTPTransport` via `session_idle_timeout:` option (#268)
### Changed
- `ServerSession` for per-connection state (#275)
### Removed
- Remove `Server#notify_progress` broadcast API (#276)
- Remove undocumented handler override methods (#270)
### Fixed
- Reject POST requests without session ID in stateful mode (#274)
## [0.9.2] - 2026-03-27
### Fixed
- Use accessor method in `server_context_with_meta` instead of ivar (#273)
- Reject duplicate SSE connections with 409 to prevent stream hijacking
## [0.9.1] - 2026-03-23
### Added
- Allow `Client#call_tool` to accept a tool name (#266)
### Fixed
- Return 404 for invalid session ID in `handle_delete` (#261)
## [0.9.0] - 2026-03-20
### Added
- `MCP::Client::Stdio` transport (#262)
- Progress notifications per MCP specification (#254)
- Automatic `_meta` parameter extraction support (#172)
- CORS and Accept wildcard support for browser-based MCP clients (#253)
### Changed
- Use `autoload` to defer loading of unused subsystems (#255)
- Reduce release package size (#239)
### Fixed
- Return 404 for invalid session ID in `handle_regular_request` (#257)
- Use mutex-protected `session_exists?` in `handle_regular_request` (#258)
## [0.8.0] - 2026-03-03
### Added
- `Content::EmbeddedResource` class for embedded resource content type (#244)
- `Content::Audio` class for audio content type (#243)
- `$ref` support in `Tool::Schema` for protocol version 2025-11-25 (#242)
- MCP conformance test suite (#248)
### Fixed
- Handle `Errno::ECONNRESET` in SSE stream operations (#249)
- Fix default handler return values to comply with MCP spec (#247)
- Fix `Prompt#validate_arguments!` crash when arguments are `nil` (#246)
- Return 202 Accepted for SSE responses per MCP spec (#245)
- Fix `Content::Image#to_h` to return `mimeType` (camelCase) per MCP spec (#241)
## [0.7.1] - 2026-02-21
### Fixed
- Fix `Resource::Contents#to_h` to use correct property names per MCP spec (#235)
- Return JSON-RPC protocol errors for unknown tool calls (#231)
- Fix `logging/setLevel` to return empty hash per MCP specification (#230)
## [0.7.0] - 2026-02-14
### Added
- `logging` support (#103)
- Protocol version negotiation to server initialization (#223)
- Tool arguments to instrumentation data (#218)
- Client info to instrumentation callback (#221)
- `resource_templates` to `MCP::Client` (#225)
### Changed
- Extract `MCP::Annotations` into a dedicated file (#224)
### Fixed
- `Resource::Embedded` not setting `@resource` in `initialize` (#220)
## [0.6.0] - 2026-01-16
### Changed
- Update licensing to Apache 2.0 for new contributions (#213)
### Fixed
- Omit `icons` from responses when empty or nil to reduce context window usage (#212)
## [0.5.0] - 2026-01-11
### Added
- Protocol specification version "2025-11-25" support (#184)
- `icons` parameter support (#205)
- `websiteUrl` parameter in `serverInfo` (#188)
- `description` parameter in `serverInfo` (#201)
- `additionalProperties` support for schema validation (#198)
- "Draft" protocol version to supported versions (#179)
- `stateless` mode for high availability (#101)
- Exception messages for tool call errors (#194)
- Elicitation skeleton (#178)
- `prompts/list` and `prompts/get` support to client (#163)
- Accept header validation for HTTP client transport (#207)
- Ruby 2.7 - Ruby 3.1 support (#206)
### Changed
- Make tool names stricter (#204)
### Fixed
- Symlink path comparison in schema validation (#193)
- Duplicate tool names across namespaces now raise an error (#199)
- Tool error handling to follow MCP spec (#165)
- XSS vulnerability in json_rpc_handler (#175)
## [0.4.0] - 2025-10-15
### Added
@ -591,4 +84,4 @@ apart from the narrow exceptions documented in [VERSIONING.md](VERSIONING.md).
## [0.1.0] - 2025-05-30
Initial release in collaboration with Shopify
Initial release

View file

@ -1 +0,0 @@
@AGENTS.md

16
Gemfile
View file

@ -6,26 +6,20 @@ source "https://rubygems.org"
gemspec
# Specify development dependencies below
gem "rubocop-minitest", ">= 0.39.0", require: false
gem "rubocop-minitest", require: false
gem "rubocop-rake", require: false
gem "rubocop-shopify", ">= 2.18", require: false if RUBY_VERSION >= "3.1"
gem "rubocop-shopify", require: false
gem "puma", ">= 5.0.0"
gem "rack-cors"
gem "rack", ">= 2.0.0"
gem "rackup", ">= 2.1.0"
gem "activesupport"
# Fix io-console install error when Ruby 3.0.
gem "debug" if RUBY_VERSION >= "3.1"
# Avoid i18n 1.15.0, which breaks on Ruby 3.1 (ruby-i18n/i18n#735).
gem "i18n", "!= 1.15.0"
gem "debug"
gem "rake", "~> 13.0"
gem "sorbet-static-and-runtime" if RUBY_VERSION >= "3.0"
gem "yard", "~> 0.9"
gem "yard-sorbet", "~> 0.9" if RUBY_VERSION >= "3.1"
gem "sorbet-static-and-runtime"
group :test do
gem "event_stream_parser", ">= 1.0"
gem "faraday", ">= 2.0"
gem "minitest", "~> 5.1", require: false
gem "mocha"

216
LICENSE
View file

@ -1,216 +0,0 @@
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
No rights beyond those granted by the applicable original license are conveyed for such contributions.
---
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on behalf
of the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
---
MIT License
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Creative Commons Attribution 4.0 International (CC-BY-4.0)
Documentation in this project (excluding specifications) is licensed under
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
the full license text.

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 TODO: Write your name
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

899
README.md
View file

@ -1,22 +1,13 @@
# MCP Ruby SDK [![Gem Version](https://img.shields.io/gem/v/mcp)](https://rubygems.org/gems/mcp) [![Apache 2.0 licensed](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE) [![CI](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml)
# MCP Ruby SDK [![Gem Version](https://img.shields.io/gem/v/mcp)](https://rubygems.org/gems/mcp) [![MIT licensed](https://img.shields.io/badge/license-MIT-green)](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE.txt) [![CI](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml)
The official Ruby SDK for Model Context Protocol servers and clients.
Detailed guides are available at https://ruby.sdk.modelcontextprotocol.io.
## Features
- Build [MCP servers](https://ruby.sdk.modelcontextprotocol.io/server/) that expose tools, prompts, and resources to any MCP host
- Build [MCP clients](https://ruby.sdk.modelcontextprotocol.io/client/) that connect to any MCP server, with automatic lifecycle negotiation and OAuth 2.1 authorization
- Speak every standard transport: stdio and Streamable HTTP (including SSE), with a Rails integration
- Cover the full protocol surface: server-to-client requests, multi round-trip requests, notifications, progress, logging, cancellation, completions, and pagination
## Installation
Add this line to your application's Gemfile:
```ruby
gem "mcp"
gem 'mcp'
```
And then execute:
@ -33,14 +24,157 @@ $ gem install mcp
You may need to add additional dependencies depending on which features you wish to access.
## Quick Start
## Building an MCP Server
The following minimal programs show both sides of the protocol: a server that exposes a single tool,
and a client that spawns such a server and drives it over stdio.
The `MCP::Server` class is the core component that handles JSON-RPC requests and responses.
It implements the Model Context Protocol specification, handling model context requests and responses.
### MCP Server
### Key Features
A minimal server defines a tool and serves it over the stdio transport:
- Implements JSON-RPC 2.0 message handling
- Supports protocol initialization and capability negotiation
- Manages tool registration and invocation
- Supports prompt registration and execution
- Supports resource registration and retrieval
- Supports stdio & Streamable HTTP (including SSE) transports
- Supports notifications for list changes (tools, prompts, resources)
### Supported Methods
- `initialize` - Initializes the protocol and returns server capabilities
- `ping` - Simple health check
- `tools/list` - Lists all registered tools and their schemas
- `tools/call` - Invokes a specific tool with provided arguments
- `prompts/list` - Lists all registered prompts and their schemas
- `prompts/get` - Retrieves a specific prompt by name
- `resources/list` - Lists all registered resources and their schemas
- `resources/read` - Retrieves a specific resource by name
- `resources/templates/list` - Lists all registered resource templates and their schemas
### Custom Methods
The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
```ruby
server = MCP::Server.new(name: "my_server")
# Define a custom method that returns a result
server.define_custom_method(method_name: "add") do |params|
params[:a] + params[:b]
end
# Define a custom notification method (returns nil)
server.define_custom_method(method_name: "notify") do |params|
# Process notification
nil
end
```
**Key Features:**
- Accepts any method name as a string
- Block receives the request parameters as a hash
- Can handle both regular methods (with responses) and notifications
- Prevents overriding existing MCP protocol methods
- Supports instrumentation callbacks for monitoring
**Usage Example:**
```ruby
# Client request
{
"jsonrpc": "2.0",
"id": 1,
"method": "add",
"params": { "a": 5, "b": 3 }
}
# Server response
{
"jsonrpc": "2.0",
"id": 1,
"result": 8
}
```
**Error Handling:**
- Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
- Supports the same exception reporting and instrumentation as standard methods
### Notifications
The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
#### Notification Methods
The server provides three notification 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
#### Notification Format
Notifications follow the JSON-RPC 2.0 specification and use these method names:
- `notifications/tools/list_changed`
- `notifications/prompts/list_changed`
- `notifications/resources/list_changed`
#### Transport Support
- **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
- **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)
#### Usage Example
```ruby
server = MCP::Server.new(name: "my_server")
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport
# When tools change, notify clients
server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
server.notify_tools_list_changed
```
### Unsupported Features ( to be implemented in future versions )
- Log Level
- Resource subscriptions
- Completions
### Usage
#### Rails Controller
When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
[Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) transport
requests.
You can use the `Server#handle_json` method to handle requests.
```ruby
class ApplicationController < ActionController::Base
def index
server = MCP::Server.new(
name: "my_server",
title: "Example Server Display Name", # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
version: "1.0.0",
instructions: "Use the tools of this server as a last resort",
tools: [SomeTool, AnotherTool],
prompts: [MyPrompt],
server_context: { user_id: current_user.id },
)
render(json: server.handle_json(request.body.read))
end
end
```
#### Stdio Transport
If you want to build a local command-line application, you can use the stdio transport:
```ruby
require "mcp"
@ -76,64 +210,729 @@ transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open
```
Save the script as `server.rb`, run it, and send JSON-RPC requests via stdin:
You can run this script and then type in requests to the server at the command line.
```console
$ ruby server.rb
$ ruby examples/stdio_server.rb
{"jsonrpc":"2.0","id":"1","method":"ping"}
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
```
The same server can also run over Streamable HTTP, including mounted inside a Rails application;
see [Server Transports](https://ruby.sdk.modelcontextprotocol.io/server/transports/).
### Configuration
### MCP Client
A minimal client spawns a stdio server as a subprocess, connects, and lists and calls its tools:
The gem can be configured using the `MCP.configure` block:
```ruby
stdio_transport = MCP::Client::Stdio.new(
command: "bundle",
args: ["exec", "ruby", "path/to/server.rb"],
env: { "API_KEY" => "my_secret_key" },
read_timeout: 30
MCP.configure do |config|
config.exception_reporter = ->(exception, server_context) {
# Your exception reporting logic here
# For example with Bugsnag:
Bugsnag.notify(exception) do |report|
report.add_metadata(:model_context_protocol, server_context)
end
}
config.instrumentation_callback = ->(data) {
puts "Got instrumentation data #{data.inspect}"
}
end
```
or by creating an explicit configuration and passing it into the server.
This is useful for systems where an application hosts more than one MCP server but
they might require different instrumentation callbacks.
```ruby
configuration = MCP::Configuration.new
configuration.exception_reporter = ->(exception, server_context) {
# Your exception reporting logic here
# For example with Bugsnag:
Bugsnag.notify(exception) do |report|
report.add_metadata(:model_context_protocol, server_context)
end
}
configuration.instrumentation_callback = ->(data) {
puts "Got instrumentation data #{data.inspect}"
}
server = MCP::Server.new(
# ... all other options
configuration:,
)
client = MCP::Client.new(transport: stdio_transport)
```
# Perform the MCP initialization handshake before sending any requests.
client.connect
### Server Context and Configuration Block Data
# List available tools.
tools = client.tools
tools.each do |tool|
puts "Tool: #{tool.name} - #{tool.description}"
#### `server_context`
The `server_context` is a user-defined hash that is passed into the server instance and made available to tools, prompts, and exception/instrumentation callbacks. It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
**Type:**
```ruby
server_context: { [String, Symbol] => Any }
```
**Example:**
```ruby
server = MCP::Server.new(
name: "my_server",
server_context: { user_id: current_user.id, request_id: request.uuid }
)
```
This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
#### Configuration Block Data
##### Exception Reporter
The exception reporter receives:
- `exception`: The Ruby exception object that was raised
- `server_context`: The context hash provided to the server
**Signature:**
```ruby
exception_reporter = ->(exception, server_context) { ... }
```
##### Instrumentation Callback
The instrumentation callback receives a hash with the following possible keys:
- `method`: (String) The protocol method called (e.g., "ping", "tools/list")
- `tool_name`: (String, optional) The name of the tool called
- `prompt_name`: (String, optional) The name of the prompt called
- `resource_uri`: (String, optional) The URI of the resource called
- `error`: (String, optional) Error code if a lookup failed
- `duration`: (Float) Duration of the call in seconds
**Type:**
```ruby
instrumentation_callback = ->(data) { ... }
# where data is a Hash with keys as described above
```
**Example:**
```ruby
config.instrumentation_callback = ->(data) {
puts "Instrumentation: #{data.inspect}"
}
```
### Server Protocol Version
The server's protocol version can be overridden using the `protocol_version` keyword argument:
```ruby
configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
MCP::Server.new(name: "test_server", configuration: configuration)
```
This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
```ruby
MCP::Configuration.new(protocol_version: nil)
```
If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/versioning) for the protocol version to understand the supported features for the version being set.
### Exception Reporting
The exception reporter receives two arguments:
- `exception`: The Ruby exception object that was raised
- `server_context`: A hash containing contextual information about where the error occurred
The server_context hash includes:
- For tool calls: `{ tool_name: "name", arguments: { ... } }`
- For general request handling: `{ request: { ... } }`
When an exception occurs:
1. The exception is reported via the configured reporter
2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
3. For other requests, the exception is re-raised after reporting
If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
### Tools
MCP spec includes [Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) which provide functionality to LLM apps.
This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
1. As a class definition:
```ruby
class MyTool < MCP::Tool
title "My Tool" # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
description "This tool performs specific functionality..."
input_schema(
properties: {
message: { type: "string" },
},
required: ["message"]
)
output_schema(
properties: {
result: { type: "string" },
success: { type: "boolean" },
timestamp: { type: "string", format: "date-time" }
},
required: ["result", "success", "timestamp"]
)
annotations(
read_only_hint: true,
destructive_hint: false,
idempotent_hint: true,
open_world_hint: false,
title: "My Tool"
)
def self.call(message:, server_context:)
MCP::Tool::Response.new([{ type: "text", text: "OK" }])
end
end
# Call a specific tool.
tool = MyTool
```
2. By using the `MCP::Tool.define` method with a block:
```ruby
tool = MCP::Tool.define(
name: "my_tool",
title: "My Tool", # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
description: "This tool performs specific functionality...",
annotations: {
read_only_hint: true,
title: "My Tool"
}
) do |args, server_context|
MCP::Tool::Response.new([{ type: "text", text: "OK" }])
end
```
3. By using the `MCP::Server#define_tool` method with a block:
```ruby
server = MCP::Server.new
server.define_tool(
name: "my_tool",
description: "This tool performs specific functionality...",
annotations: {
title: "My Tool",
read_only_hint: true
}
) do |args, server_context|
Tool::Response.new([{ type: "text", text: "OK" }])
end
```
The server_context parameter is the server_context passed into the server and can be used to pass per request information,
e.g. around authentication state.
### Tool Annotations
Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
- `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
- `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
- `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
- `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
- `title`: A human-readable title for the tool
Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
> [!NOTE]
> This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
### Tool Output Schemas
Tools can optionally define an `output_schema` to specify the expected structure of their results. This works similarly to how `input_schema` is defined and can be used in three ways:
1. **Class definition with output_schema:**
```ruby
class WeatherTool < MCP::Tool
tool_name "get_weather"
description "Get current weather for a location"
input_schema(
properties: {
location: { type: "string" },
units: { type: "string", enum: ["celsius", "fahrenheit"] }
},
required: ["location"]
)
output_schema(
properties: {
temperature: { type: "number" },
condition: { type: "string" },
humidity: { type: "integer" }
},
required: ["temperature", "condition", "humidity"]
)
def self.call(location:, units: "celsius", server_context:)
# Call weather API and structure the response
api_response = WeatherAPI.fetch(location, units)
weather_data = {
temperature: api_response.temp,
condition: api_response.description,
humidity: api_response.humidity_percent
}
output_schema.validate_result(weather_data)
MCP::Tool::Response.new([{
type: "text",
text: weather_data.to_json
}])
end
end
```
2. **Using Tool.define with output_schema:**
```ruby
tool = MCP::Tool.define(
name: "calculate_stats",
description: "Calculate statistics for a dataset",
input_schema: {
properties: {
numbers: { type: "array", items: { type: "number" } }
},
required: ["numbers"]
},
output_schema: {
properties: {
mean: { type: "number" },
median: { type: "number" },
count: { type: "integer" }
},
required: ["mean", "median", "count"]
}
) do |args, server_context|
# Calculate statistics and validate against schema
MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
end
```
3. **Using OutputSchema objects:**
```ruby
class DataTool < MCP::Tool
output_schema MCP::Tool::OutputSchema.new(
properties: {
success: { type: "boolean" },
data: { type: "object" }
},
required: ["success"]
)
end
```
Output schema may also describe an array of objects:
```ruby
class WeatherTool < MCP::Tool
output_schema(
type: "array",
item: {
properties: {
temperature: { type: "number" },
condition: { type: "string" },
humidity: { type: "integer" }
},
required: ["temperature", "condition", "humidity"]
}
)
end
```
Please note: in this case, you must provide `type: "array"`. The default type
for output schemas is `object`.
MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) specifies that:
- **Server Validation**: Servers MUST provide structured results that conform to the output schema
- **Client Validation**: Clients SHOULD validate structured results against the output schema
- **Better Integration**: Enables strict schema validation, type information, and improved developer experience
- **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
### Tool Responses with Structured Content
Tools can return structured data alongside text content using the `structured_content` parameter.
The structured content will be included in the JSON-RPC response as the `structuredContent` field.
```ruby
class APITool < MCP::Tool
description "Get current weather and return structured data"
def self.call(endpoint:, server_context:)
# Call weather API and structure the response
api_response = WeatherAPI.fetch(location, units)
weather_data = {
temperature: api_response.temp,
condition: api_response.description,
humidity: api_response.humidity_percent
}
output_schema.validate_result(weather_data)
MCP::Tool::Response.new(
[{
type: "text",
text: weather_data.to_json
}],
structured_content: weather_data
)
end
end
```
### Prompts
MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
The `MCP::Prompt` class provides three ways to create prompts:
1. As a class definition with metadata:
```ruby
class MyPrompt < MCP::Prompt
prompt_name "my_prompt" # Optional - defaults to underscored class name
title "My Prompt" # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
description "This prompt performs specific functionality..."
arguments [
MCP::Prompt::Argument.new(
name: "message",
title: "Message Title",
description: "Input message",
required: true
)
]
meta({ version: "1.0", category: "example" })
class << self
def template(args, server_context:)
MCP::Prompt::Result.new(
description: "Response description",
messages: [
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Text.new("User message")
),
MCP::Prompt::Message.new(
role: "assistant",
content: MCP::Content::Text.new(args["message"])
)
]
)
end
end
end
prompt = MyPrompt
```
2. Using the `MCP::Prompt.define` method:
```ruby
prompt = MCP::Prompt.define(
name: "my_prompt",
title: "My Prompt", # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
description: "This prompt performs specific functionality...",
arguments: [
MCP::Prompt::Argument.new(
name: "message",
title: "Message Title",
description: "Input message",
required: true
)
],
meta: { version: "1.0", category: "example" }
) do |args, server_context:|
MCP::Prompt::Result.new(
description: "Response description",
messages: [
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Text.new("User message")
),
MCP::Prompt::Message.new(
role: "assistant",
content: MCP::Content::Text.new(args["message"])
)
]
)
end
```
3. Using the `MCP::Server#define_prompt` method:
```ruby
server = MCP::Server.new
server.define_prompt(
name: "my_prompt",
description: "This prompt performs specific functionality...",
arguments: [
Prompt::Argument.new(
name: "message",
title: "Message Title",
description: "Input message",
required: true
)
],
meta: { version: "1.0", category: "example" }
) do |args, server_context:|
Prompt::Result.new(
description: "Response description",
messages: [
Prompt::Message.new(
role: "user",
content: Content::Text.new("User message")
),
Prompt::Message.new(
role: "assistant",
content: Content::Text.new(args["message"])
)
]
)
end
```
The server_context parameter is the server_context passed into the server and can be used to pass per request information,
e.g. around authentication state or user preferences.
### Key Components
- `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
- `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
- `MCP::Prompt::Result` - The output of a prompt template containing description and messages
- `MCP::Content::Text` - Text content for messages
### Usage
Register prompts with the MCP server:
```ruby
server = MCP::Server.new(
name: "my_server",
prompts: [MyPrompt],
server_context: { user_id: current_user.id },
)
```
The server will handle prompt listing and execution through the MCP protocol methods:
- `prompts/list` - Lists all registered prompts and their schemas
- `prompts/get` - Retrieves and executes a specific prompt with arguments
### Instrumentation
The server allows registering a callback to receive information about instrumentation.
To register a handler pass a proc/lambda to as `instrumentation_callback` into the server constructor.
```ruby
MCP.configure do |config|
config.instrumentation_callback = ->(data) {
puts "Got instrumentation data #{data.inspect}"
}
end
```
The data contains the following keys:
- `method`: the method called, e.g. `ping`, `tools/list`, `tools/call` etc
- `tool_name`: the name of the tool called
- `prompt_name`: the name of the prompt called
- `resource_uri`: the uri of the resource called
- `error`: if looking up tools/prompts etc failed, e.g. `tool_not_found`
- `duration`: the duration of the call in seconds
`tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
This is to avoid potential issues with metric cardinality
### Resources
MCP spec includes [Resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources).
### Reading Resources
The `MCP::Resource` class provides a way to register resources with the server.
```ruby
resource = MCP::Resource.new(
uri: "https://example.com/my_resource",
name: "my-resource",
title: "My Resource", # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
description: "Lorem ipsum dolor sit amet",
mime_type: "text/html",
)
server = MCP::Server.new(
name: "my_server",
resources: [resource],
)
```
The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
```ruby
server.resources_read_handler do |params|
[{
uri: params[:uri],
mimeType: "text/plain",
text: "Hello from example resource! URI: #{params[:uri]}"
}]
end
```
otherwise `resources/read` requests will be a no-op.
### Resource Templates
The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
```ruby
resource_template = MCP::ResourceTemplate.new(
uri_template: "https://example.com/my_resource_template",
name: "my-resource-template",
title: "My Resource Template", # WARNING: This is a `Draft` and is not supported in the `Version 2025-06-18 (latest)` specification.
description: "Lorem ipsum dolor sit amet",
mime_type: "text/html",
)
server = MCP::Server.new(
name: "my_server",
resource_templates: [resource_template],
)
```
## Building an MCP Client
The `MCP::Client` class provides an interface for interacting with MCP servers.
This class supports:
- Tool listing via the `tools/list` method (`MCP::Client#tools`)
- Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
- Resource listing via the `resources/list` method (`MCP::Client#resources`)
- Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
- Automatic JSON-RPC 2.0 message formatting
- UUID request ID generation
Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
Authorization is handled by the transport layer.
## Transport Layer Interface
If the transport layer you need is not included in the gem, you can build and pass your own instances so long as they conform to the following interface:
```ruby
class CustomTransport
# Sends a JSON-RPC request to the server and returns the raw response.
#
# @param request [Hash] A complete JSON-RPC request object.
# https://www.jsonrpc.org/specification#request_object
# @return [Hash] A hash modeling a JSON-RPC response object.
# https://www.jsonrpc.org/specification#response_object
def send_request(request:)
# Your transport-specific logic here
# - HTTP: POST to endpoint with JSON body
# - WebSocket: Send message over WebSocket
# - stdio: Write to stdout, read from stdin
# - etc.
end
end
```
### HTTP Transport Layer
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
You'll need to add `faraday` as a dependency in order to use the HTTP transport layer:
```ruby
gem 'mcp'
gem 'faraday', '>= 2.0'
```
Example usage:
```ruby
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
client = MCP::Client.new(transport: http_transport)
# List available tools
tools = client.tools
tools.each do |tool|
puts <<~TOOL_INFORMATION
Tool: #{tool.name}
Description: #{tool.description}
Input Schema: #{tool.input_schema}
TOOL_INFORMATION
end
# Call a specific tool
response = client.call_tool(
tool: tools.first,
arguments: { message: "Hello, world!" }
)
# Close the transport when done.
stdio_transport.close
```
The same client can connect to Streamable HTTP servers with `MCP::Client::HTTP`;
see [Client Transports](https://ruby.sdk.modelcontextprotocol.io/client/transports/).
#### HTTP Authorization
## Examples
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:
Runnable examples are available in [`examples/`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples),
including a complete Rails application in [`examples/rails`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples/rails).
```ruby
http_transport = MCP::Client::HTTP.new(
url: "https://api.example.com/mcp",
headers: {
"Authorization" => "Bearer my_token"
}
)
## Documentation
client = MCP::Client.new(transport: http_transport)
client.tools # will make the call using Bearer auth
```
- [SDK guides](https://ruby.sdk.modelcontextprotocol.io)
- [SDK API documentation](https://rubydoc.info/gems/mcp)
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
You can add any custom headers needed for your authentication scheme, or for any other purpose. The client will include these headers on every request.
## License
### Tool Objects
This project is licensed under the Apache License 2.0 for new contributions, with existing code under MIT. See the [LICENSE](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE) file for details.
The client provides a wrapper class for tools returned by the server:
- `MCP::Client::Tool` - Represents a single tool with its metadata
This class provides easy access to tool properties like name, description, input schema, and output schema.
## Releases
This gem is published to [RubyGems.org](https://rubygems.org/gems/mcp)
Releases are triggered by PRs to the `main` branch updating the version number in `lib/mcp/version.rb`.
1. **Update the version number** in `lib/mcp/version.rb`, following [semver](https://semver.org/)
1. **Update CHANGELOG.md**, backfilling the changes since the last release if necessary, and adding a new section for the new version, clearing out the Unreleased section
1. **Create a PR and get approval from a maintainer**
1. **Merge your PR to the main branch** - This will automatically trigger the release workflow via GitHub Actions
When changes are merged to the `main` branch, the GitHub Actions workflow (`.github/workflows/release.yml`) is triggered and the gem is published to RubyGems.

View file

@ -1,12 +0,0 @@
## Releases
This gem is published to [RubyGems.org](https://rubygems.org/gems/mcp)
Releases are triggered by PRs to the `main` branch updating the version number in `lib/mcp/version.rb`.
1. **Update the version number** in `lib/mcp/version.rb`, following [semver](https://semver.org/)
2. **Update CHANGELOG.md**, backfilling the changes since the last release if necessary, and adding a new section for the new version, clearing out the Unreleased section
3. **Create a PR and get approval from a maintainer**
4. **Merge your PR to the main branch** - This will automatically trigger the release workflow via GitHub Actions
When changes are merged to the `main` branch, the GitHub Actions workflow (`.github/workflows/release.yml`) is triggered and the gem is published to RubyGems.

View file

@ -1,51 +0,0 @@
# Roadmap
This roadmap outlines the MCP Ruby SDK's path toward SEP-1730 Tier 1.
It is a living document and will evolve as the SDK matures.
## Current Status
The SDK implements the 2025-06-18, 2025-11-25, and 2026-07-28 spec revisions, and passes all scored server
and client conformance scenarios for each revision's requirement set.
Optional protocol extensions (such as DPoP, workload identity federation, and the tasks extension)
are not yet implemented; SEP-1730 does not require extensions for any tier.
## API Stability
The 1.0.0 release marked the public API as stable: breaking changes ship only in major releases,
apart from the narrow exceptions documented in [VERSIONING.md](VERSIONING.md),
which also defines the Semantic Versioning scheme and breaking-change policy.
## Deprecated Features
The 2026-07-28 spec revision deprecates Roots, Sampling, and Logging (SEP-2577).
These features remain supported throughout 1.x: on the legacy lifecycle they work unchanged
(the deprecations do not apply there), and the 2026-07-28 lifecycle carries them through its own mechanisms:
roots-shaped and sampling-shaped input requests embedded in `InputRequiredResult` responses
(the SEP-2322 multi round-trip requests pattern) and per-request log delivery through the envelope's `logLevel`.
The replacements SEP-2577 names live outside the protocol: tool parameters or configuration for roots,
direct LLM provider APIs for sampling, and stderr or OpenTelemetry for logging.
The client emits a deprecation warning when a modern connect declares the deprecated Roots or Sampling capabilities.
Under Semantic Versioning their removal requires a major release, but no removal is scheduled yet: whether 2.0.0 removes
them depends on how future MCP spec revisions treat these features and on adoption of their replacements.
## Conformance
The SDK maintains a 100% conformance pass rate as new scenarios are added to the conformance suite.
Legacy SSE transport (2024-11-05) is intentionally out of scope; the SDK provides modern Streamable HTTP only.
## Documentation
Reference documentation covers all non-experimental features with examples.
## Tracking New Spec Revisions
The SDK aims to support each new MCP specification revision, with the implementation timeline agreed per
release based on feature complexity.
The 2026-07-28 revision shipped during 1.x: the server serves both lifecycle eras side by side,
so the SEP-2575 stateless lifecycle (including `server/discover` and `subscriptions/listen`),
SEP-2322 multi round-trip requests, SEP-2549 cache hints, and SEP-2243 custom headers are available alongside
the legacy handshake. The few incompatible adjustments the revision required shipped in
a minor release under the spec-conformance and security exceptions described in [VERSIONING.md](VERSIONING.md).
Removing legacy-era support is reserved for a future major release.
The tasks extension (SEP-2663) remains planned ([#391](https://github.com/modelcontextprotocol/ruby-sdk/issues/391)).

View file

@ -14,90 +14,4 @@ require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: [:rubocop, :test, "conformance:test"]
namespace :conformance do
desc "Run MCP conformance tests (PORT, SCENARIO, SPEC_VERSION, VERBOSE)"
task :test do |t|
next unless npx_available?(t.name)
require_relative "conformance/server_runner"
require_relative "conformance/client_runner"
options = {}
options[:port] = Integer(ENV["PORT"]) if ENV["PORT"]
options[:scenario] = ENV["SCENARIO"] if ENV["SCENARIO"]
options[:spec_version] = ENV["SPEC_VERSION"] if ENV["SPEC_VERSION"]
options[:verbose] = true if ENV["VERBOSE"]
Conformance::ServerRunner.new(**options).run
Conformance::ClientRunner.new(**options.reject { |key, _value| key == :port }).run
end
desc "List available conformance scenarios"
task :list do |t|
next unless npx_available?(t.name)
system("npx", "--yes", "@modelcontextprotocol/conformance", "list", "--server")
system("npx", "--yes", "@modelcontextprotocol/conformance", "list", "--client")
end
desc "Start the conformance server (PORT)"
task :server do
require_relative "conformance/server"
options = {}
options[:port] = Integer(ENV["PORT"]) if ENV["PORT"]
Conformance::Server.new(**options).start
end
end
# The other two former names now fail with a "Did you mean?" suggestion, but a bare `conformance`
# would match the `conformance/` directory: Rake synthesizes a file task for it and exits 0 without
# running anything. This keeps the name pointing at the suite instead of at that silent no-op.
desc "Alias for conformance:test"
task conformance: "conformance:test"
namespace :docs do
desc "Serve the documentation site locally at http://localhost:4000 (PORT)"
task :preview do
docs_dir = File.expand_path("docs", __dir__)
generate_docs_versions_data(docs_dir)
env = {
"BUNDLE_GEMFILE" => File.join(docs_dir, "Gemfile"),
"RUBYOPT" => "-r#{File.join(docs_dir, "_preview", "taint_shim.rb")}",
}
port = ENV.fetch("PORT", "4000")
Bundler.with_unbundled_env do
system(env, "bundle", "install", "--quiet", chdir: docs_dir, exception: true)
system(env, "bundle", "exec", "jekyll", "serve", "--port", port, chdir: docs_dir, exception: true)
rescue Interrupt
# Ctrl-C is the way to stop the preview, not an error.
end
end
end
# Mirrors bin/generate-gh-pages.sh: the released site receives `_data/versions.yml` from
# the version tags at deploy time, and the preview generates the same data so the nav footer
# shows the released-gem version line.
def generate_docs_versions_data(docs_dir)
versions = %x(git tag --list).split("\n").filter_map { |tag|
tag[/\A[^0-9]*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)\z/, 1]
}.sort_by { |version|
Gem::Version.new(version)
}.reverse
return if versions.empty?
mkdir_p(File.join(docs_dir, "_data"))
File.write(File.join(docs_dir, "_data", "versions.yml"), versions.map { |version| "- #{version}\n" }.join)
end
def npx_available?(task_name)
return true if system("which", "npx", out: File::NULL, err: File::NULL)
warn("Skipping #{task_name}: npx is not installed. Install Node.js to run this task: https://nodejs.org/")
false
end
task default: [:test, :rubocop]

View file

@ -1,21 +0,0 @@
# Security Policy
Thank you for helping keep the Model Context Protocol and its ecosystem secure.
## Reporting Security Issues
If you discover a security vulnerability in this repository, please report it through
the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
for this repository.
Please **do not** report security vulnerabilities through public GitHub issues, discussions,
or pull requests.
## What to Include
To help us triage and respond quickly, please include:
- A description of the vulnerability
- Steps to reproduce the issue
- The potential impact
- Any suggested fixes (optional)

View file

@ -1,56 +0,0 @@
# Versioning Policy
The MCP Ruby SDK follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
This document describes what each version component means for users of the `mcp` gem,
what counts as a breaking change, and how breaking changes are communicated.
## Version Scheme
Given a version number `MAJOR.MINOR.PATCH`:
- **MAJOR** is incremented for incompatible changes to the public API.
- **MINOR** is incremented for backwards-compatible new functionality.
- **PATCH** is incremented for backwards-compatible bug fixes.
### Current 0.x Phase
While the SDK is at version 0.x, the public API is not yet considered stable.
Following SemVer conventions for initial development, minor releases (0.x -> 0.y) may contain breaking changes.
All breaking changes are recorded in [CHANGELOG.md](CHANGELOG.md).
### After 1.0.0
Once 1.0.0 is released, breaking changes will only ship in major releases, with the exceptions described below.
## What Counts as a Breaking Change
A breaking change is any change that requires users to modify their code when upgrading, including:
- Removing or renaming a public class, module, method, or constant
- Changing a public method's signature in a way that rejects previously valid arguments
- Changing documented behavior that users can reasonably depend on
New functionality that is purely additive (new classes, new methods, new optional keyword arguments)
is not a breaking change.
## Exceptions: Spec Compliance, Security, and Clear Defects
Minor releases avoid incompatible changes as much as possible. However, the SDK's primary contract is
conformance to the [MCP specification](https://modelcontextprotocol.io/specification/).
Behavior that deviates from the specification is treated as a bug, even when users may have come to rely on it.
Therefore, incompatible changes may be shipped in a minor release when they are required to:
- Fix incorrect conformance to the MCP specification
- Address a security vulnerability
- Fix a clear defect, such as behavior that contradicts the documentation, a crash,
or data corruption, where compatibility would mean preserving the defect
When such a change ships, the release notes and CHANGELOG entry explicitly call out the incompatibility
and describe how to migrate.
## How Breaking Changes Are Communicated
- [CHANGELOG.md](CHANGELOG.md) records every release in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format;
breaking changes appear under the "Changed" or "Removed" headings
- [GitHub Releases](https://github.com/modelcontextprotocol/ruby-sdk/releases) mirror the changelog for each release
- Where practical, deprecation warnings are added at least one minor release before removal

View file

@ -1,119 +0,0 @@
#!/bin/bash
set -e
# Generates versioned documentation links and commits to gh-pages branch
#
# PURPOSE:
# This script generates a landing page with links to API documentation on
# RubyDoc.info for a specific version tag. This script is invoked by the
# publish-gh-pages job in the GitHub Actions workflow
# (.github/workflows/release.yml) when a release is published.
#
# HOW IT WORKS:
# - Creates isolated git worktrees for the specified tag and gh-pages branch
# - Copies static Jekyll template files from docs/
# - Generates _data/versions.yml with list of versions
# - Commits changes to gh-pages (does not push automatically)
#
# WORKFLOW:
# 1. Run this script with a tag name: `generate-gh-pages.sh v1.2.3`
# 2. Script generates docs and commits to local gh-pages branch
# 3. Push gh-pages branch to deploy: `git push origin gh-pages`
# Parse semantic version from tag name (ignoring arbitrary prefixes)
if [[ "${1}" =~ ([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?)$ ]]; then
VERSION="v${BASH_REMATCH[1]}"
else
echo "Error: Must specify a tag name that contains a valid semantic version"
echo "Usage: ${0} <tag-name>"
echo "Examples:"
echo " ${0} 1.2.3"
echo " ${0} v2.0.0-rc.1"
exit 1
fi
TAG_NAME="${1}"
REPO_ROOT="$(git rev-parse --show-toplevel)"
echo "Generating documentation for tag: ${TAG_NAME}"
# Create temporary directories for both worktrees
WORKTREE_DIR=$(mktemp -d)
GHPAGES_WORKTREE_DIR=$(mktemp -d)
# Set up trap to clean up both worktrees on exit
trap 'git worktree remove --force "${WORKTREE_DIR}" 2>/dev/null || true; \
git worktree remove --force "${GHPAGES_WORKTREE_DIR}" 2>/dev/null || true' EXIT
echo "Creating worktree for ${TAG_NAME}..."
git worktree add --quiet "${WORKTREE_DIR}" "${TAG_NAME}"
# Check if gh-pages branch exists
if git show-ref --verify --quiet refs/heads/gh-pages; then
echo "Creating worktree for existing gh-pages branch..."
git worktree add --quiet "${GHPAGES_WORKTREE_DIR}" gh-pages
elif git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1; then
echo "Creating worktree for gh-pages branch from remote..."
git worktree add --quiet "${GHPAGES_WORKTREE_DIR}" -b gh-pages origin/gh-pages
else
echo "Creating worktree for new orphan gh-pages branch..."
git worktree add --quiet --detach "${GHPAGES_WORKTREE_DIR}"
git -C "${GHPAGES_WORKTREE_DIR}" checkout --orphan gh-pages
git -C "${GHPAGES_WORKTREE_DIR}" rm -rf . > /dev/null 2>&1 || true
fi
# Change to gh-pages worktree
cd "${GHPAGES_WORKTREE_DIR}"
# Determine if this tag is the latest version
echo "Determining if ${VERSION} is the latest version..."
# Get all existing version tags from the repository (reverse sorted, newest first)
ALL_VERSIONS=$(
git -C "${REPO_ROOT}" tag --list | \
sed -nE 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?)$/v\1/p' | \
sort -Vr
)
# Get the latest version from all version tags
LATEST_VERSION=$(echo "${ALL_VERSIONS}" | head -n 1)
if [ "${VERSION}" = "${LATEST_VERSION}" ]; then
echo "${VERSION} is the latest version"
else
echo "${VERSION} is not the latest version (latest is ${LATEST_VERSION})"
fi
# Update custom documentation for latest version
if [ "${VERSION}" = "${LATEST_VERSION}" ]; then
echo "Updating custom documentation..."
# Clean up old custom docs from gh-pages root
echo "Cleaning gh-pages root..."
git ls-tree --name-only HEAD | xargs -r git rm -rf
# Copy custom docs from docs/ directory
echo "Copying custom docs from ${WORKTREE_DIR}/docs/..."
cp -r "${WORKTREE_DIR}/docs/." "${GHPAGES_WORKTREE_DIR}/"
fi
# Generate version data for Jekyll
echo "Generating _data/versions.yml..."
mkdir -p _data
echo "${ALL_VERSIONS}" | sed 's/^v/- /' > _data/versions.yml
# Stage all changes
git add .
# Commit if there are changes
if git diff --staged --quiet; then
echo "No changes to commit"
else
echo "Committing documentation for ${VERSION}..."
git commit -m "Add ${VERSION} docs"
echo "Documentation committed to gh-pages branch!"
echo "Push to remote to deploy to GitHub Pages"
fi
echo "Done!"

View file

@ -1,105 +0,0 @@
# MCP Conformance Tests
Validates the Ruby SDK's conformance to the MCP specification using [`@modelcontextprotocol/conformance`](https://github.com/modelcontextprotocol/conformance).
## Prerequisites
- Node.js (for `npx`)
- `bundle install` completed
## Running the Tests
### Run all scenarios
```bash
bundle exec rake conformance:test
```
Runs both server and client conformance tests in sequence. Server conformance starts the
conformance server and tests it. Client conformance spawns test servers for each scenario
and invokes `conformance/client.rb` against them. Scenarios listed in `expected_failures.yml`
are allowed to fail without affecting the exit code.
### Environment variables
| Variable | Description | Default |
|----------------|---------------------------------------|---------|
| `PORT` | Server port (server conformance only) | `9292` |
| `SCENARIO` | Run a single scenario by name | (all) |
| `SPEC_VERSION` | Filter scenarios by spec version | (all) |
| `VERBOSE` | Show raw JSON output when set | (off) |
```bash
# Run a single server scenario
bundle exec rake conformance:test SCENARIO=ping
# Use a different port with verbose output
bundle exec rake conformance:test PORT=3000 VERBOSE=1
# Start the server on a specific port
bundle exec rake conformance:server PORT=3000
```
### Start the server and test separately
```bash
# Terminal 1: start the server
bundle exec rake conformance:server
# Terminal 2: run all scenarios
npx @modelcontextprotocol/conformance@alpha server --url http://localhost:9292/mcp
# Terminal 2: run a single scenario
npx @modelcontextprotocol/conformance@alpha server --url http://localhost:9292/mcp --scenario ping
```
Keeps the server alive between test runs, which avoids the startup overhead when iterating
on a single scenario. Stop the server with Ctrl+C when done.
### List available scenarios
```bash
bundle exec rake conformance:list
```
Prints all scenario names that can be passed to `SCENARIO`.
## SDK Tier Report
The [MCP SDK Tier system](https://modelcontextprotocol.io/community/sdk-tiers) requires SDK
maintainers to self-assess and report results to the SDK Working Group via
[modelcontextprotocol/modelcontextprotocol issues](https://github.com/modelcontextprotocol/modelcontextprotocol/issues).
To generate a full tier assessment report, use the `/mcp-sdk-tier-audit` slash command from
the [modelcontextprotocol/conformance](https://github.com/modelcontextprotocol/conformance)
repository with the conformance server running:
```bash
# Terminal 1 (this repository): start the conformance server
bundle exec rake conformance:server
# Terminal 2 (conformance repository): run the tier audit skill as a slash command in Claude Code
/mcp-sdk-tier-audit /path/to/modelcontextprotocol/ruby-sdk http://localhost:9292/mcp
```
The skill evaluates conformance pass rate, issue label taxonomy, triage metrics, documentation
coverage, and policy compliance, then produces a markdown report suitable for tier advancement
submissions.
## File Structure
```
conformance/
server.rb # Conformance server (Rack + Puma, default port 9292)
server_runner.rb # Starts the server, runs npx conformance server, exits with result code
client.rb # Conformance client (invoked by npx conformance client)
client_runner.rb # Runs npx conformance client, exits with result code
expected_failures.yml # Baseline of known-failing scenarios (server and client)
README.md # This file
```
## Known Limitations
Known-failing scenarios are registered in `conformance/expected_failures.yml`. They are allowed to
fail without affecting the exit code and are tracked to catch regressions.
These are shown in the output of `bundle exec rake conformance:test`.

View file

@ -1,306 +0,0 @@
# frozen_string_literal: true
# Conformance test client for the MCP Ruby SDK.
# Invoked by the conformance runner:
# MCP_CONFORMANCE_SCENARIO=<scenario> bundle exec ruby conformance/client.rb <server-url>
#
# The server URL is passed as the last positional argument.
# The scenario name is read from the MCP_CONFORMANCE_SCENARIO environment variable,
# which is set automatically by the conformance test runner.
require "faraday"
require "json"
require_relative "../lib/mcp"
scenario = ENV["MCP_CONFORMANCE_SCENARIO"]
server_url = ARGV.last
unless scenario && server_url
abort("Usage: MCP_CONFORMANCE_SCENARIO=<scenario> ruby conformance/client.rb <server-url>")
end
# URL the conformance harness expects to see as the OAuth `client_id` for the `auth/basic-cimd` scenario
# when the AS advertises `client_id_metadata_document_supported`. The harness does not fetch the document,
# only matches the value, so the URL does not need to resolve.
CONFORMANCE_CIMD_URL = "https://conformance-test.local/client-metadata.json"
# The conformance harness optionally injects scenario-specific data via
# the `MCP_CONFORMANCE_CONTEXT` environment variable as a JSON document. The shape is
# defined by the harness, not the MCP spec, and has varied between versions:
#
# - Newer (`@modelcontextprotocol/conformance` >= 0.x): scenario fields are
# spread at the top level alongside `name`, e.g.
# `{"name":"auth/pre-registration","client_id":"...","client_secret":"..."}`.
# - Older: a nested `context` object: `{"name":"...","context":{...}}`.
#
# Both shapes are accepted so the client conforms to whichever harness version
# the developer has on hand.
def conformance_context
raw = ENV["MCP_CONFORMANCE_CONTEXT"]
return {} if raw.nil? || raw.empty?
parsed = JSON.parse(raw)
return {} unless parsed.is_a?(Hash)
if parsed["context"].is_a?(Hash)
parsed["context"]
else
parsed.reject { |key, _| key == "name" }
end
rescue JSON::ParserError
{}
end
# Saves the pre-registered `client_id` / `client_secret` the harness injects
# via context (used by pre-registration and client_credentials scenarios).
def storage_for(context)
storage = MCP::Client::OAuth::InMemoryStorage.new
if context["client_id"]
storage.save_client_information(
"client_id" => context["client_id"],
"client_secret" => context["client_secret"],
"token_endpoint_auth_method" => context["token_endpoint_auth_method"] || "client_secret_basic",
)
end
storage
end
# Builds a `client_credentials`-only provider (machine-to-machine, no redirect).
# The pre-registered credentials are injected by the harness via context:
# a shared secret for the basic scenario, or a PEM private key plus signing algorithm
# (default ES256, matching the TypeScript and Python conformance clients)
# for the `private_key_jwt` scenario.
def build_client_credentials_provider(context)
if context["private_key_pem"]
MCP::Client::OAuth::ClientCredentialsProvider.new(
client_id: context["client_id"],
token_endpoint_auth_method: "private_key_jwt",
private_key: context["private_key_pem"],
signing_algorithm: context["signing_algorithm"] || "ES256",
)
else
MCP::Client::OAuth::ClientCredentialsProvider.new(
client_id: context["client_id"],
client_secret: context["client_secret"],
token_endpoint_auth_method: context["token_endpoint_auth_method"] || "client_secret_basic",
)
end
end
# Builds a SEP-990 Cross-App Access provider: the harness injects an IdP ID token plus the IdP token endpoint
# via context; the assertion provider exchanges them for an ID-JAG (RFC 8693), which the flow then presents to
# the MCP authorization server with the jwt-bearer grant.
def build_cross_app_access_provider(context)
assertion_provider = ->(audience:, resource:) do
MCP::Client::OAuth::IDJAGTokenExchange.request(
token_endpoint: context["idp_token_endpoint"],
id_token: context["idp_id_token"],
client_id: context["idp_client_id"],
audience: audience,
resource: resource,
)
end
MCP::Client::OAuth::CrossAppAccessProvider.new(
client_id: context["client_id"],
client_secret: context["client_secret"],
assertion_provider: assertion_provider,
)
end
# Builds an OAuth provider that drives the authorization code + PKCE + DCR flow
# non-interactively against the conformance test's auth server. The conformance
# `/authorize` endpoint redirects synchronously to `redirect_uri` with
# `code=test-auth-code`, so we follow it manually instead of opening a browser.
def build_oauth_provider(context, scenario:)
callback_holder = {}
redirect_uri = "http://localhost:0/callback"
redirect_handler = ->(authorization_url) do
response = Faraday.new.get(authorization_url) do |req|
req.options.params_encoder = nil
end
location = response.headers["location"] || response.headers["Location"]
abort("Authorization request did not redirect: #{response.status}.") unless location
callback_holder[:url] = URI.parse(location)
end
callback_handler = -> do
query = URI.decode_www_form(callback_holder.fetch(:url).query).to_h
# The 3-element form opts into SEP-2468 / RFC 9207 `iss` validation;
# `query["iss"]` is nil when the authorization response carried no `iss`.
[query["code"], query["state"], query["iss"]]
end
MCP::Client::OAuth::Provider.new(
client_metadata: {
client_name: "ruby-sdk-conformance-client",
redirect_uris: [redirect_uri],
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: "none",
},
redirect_uri: redirect_uri,
redirect_handler: redirect_handler,
callback_handler: callback_handler,
storage: storage_for(context),
client_id_metadata_document_url: (scenario == "auth/basic-cimd" ? CONFORMANCE_CIMD_URL : nil),
)
end
def build_provider_for(scenario, context)
if context["idp_id_token"]
build_cross_app_access_provider(context)
elsif scenario.start_with?("auth/client-credentials")
build_client_credentials_provider(context)
else
build_oauth_provider(context, scenario: scenario)
end
end
oauth = scenario.start_with?("auth/") ? build_provider_for(scenario, conformance_context) : nil
transport = MCP::Client::HTTP.new(url: server_url, oauth: oauth)
client = MCP::Client.new(transport: transport)
# SEP-2322: the MRTR scenario exercises the automatic driver; this elicitation handler accepts
# every embedded confirmation request, and the driver echoes `requestState` byte-exactly.
# The same registration answers a real `elicitation/create` request, so nothing here is MRTR-specific.
if scenario == "sep-2322-client-request-state"
client.on_elicitation { |_params| { action: "accept", content: { confirmed: true } } }
end
capabilities = scenario == "elicitation-sep1034-client-defaults" ? { elicitation: {} } : {}
# The conformance harness asserts on the observed protocol exchange, not on the client's exit status,
# and echoes the client's output only when the exit status is non-zero. Several negative auth scenarios
# (e.g. `auth/resource-mismatch`, `auth/scope-retry-limit`) are expected to drive the client into
# a terminal failure: the SDK correctly refuses a mismatched resource or stops after the scope step-up
# retry limit. Report such an expected failure as a one-line reason and exit cleanly so the run stays
# free of noise, while letting any unexpected error (a real SDK bug) still raise with a full backtrace
# and a failing exit status.
begin
# The harness names the wire version per scenario run: the 2026 draft selects the stateless modern lifecycle,
# and every dated version through 2025-11-25 keeps the legacy `initialize` handshake
# (pinned rather than `:auto`, whose `server/discover` probe the legacy scenario servers do not expect).
mode = ENV["MCP_CONFORMANCE_PROTOCOL_VERSION"] == "2026-07-28" ? :modern : :legacy
connect = lambda do
client.connect(
client_info: { name: "ruby-sdk-conformance-client", version: MCP::VERSION },
capabilities: capabilities,
mode: mode,
)
end
begin
connect.call
rescue MCP::Client::RequestHandlerError
# SEP-2575 version negotiation: a server may reject the first request with `-32022` naming
# its supported versions. Retry once so the harness observes a follow-up request carrying
# a mutually supported version (this client already speaks the latest draft).
raise unless mode == :modern
connect.call
end
case scenario
when "initialize"
client.tools
when "json-schema-2020-12-preservation"
# SEP-1613/SEP-2106: echo the focal tool's observed inputSchema verbatim so the harness
# can diff what the client preserved. Preservation holds because `Client::Tool#input_schema`
# is the parsed response untouched.
tools = client.tools
focal = tools.find { |t| t.name == "json_schema_2020_12_tool" }
abort("Tool json_schema_2020_12_tool not found") unless focal
client.call_tool(name: "json_schema_echo", arguments: { "schema" => focal.input_schema })
when "request-metadata"
# SEP-2575: the checks read the envelope and headers of the requests the modern connect
# and this listing already produce.
client.tools
when "sep-2322-client-request-state"
# SEP-2322: the driver resumes `test_mrtr_echo_state` automatically (fresh JSON-RPC id,
# byte-exact `requestState` echo); `test_mrtr_unrelated` must carry neither retry field;
# `test_mrtr_no_state` retries without inventing a state; `test_mrtr_no_result_type`
# has no `resultType` and therefore must be treated as complete, with no retry.
client.tools
client.call_tool(name: "test_mrtr_echo_state", arguments: {})
client.call_tool(name: "test_mrtr_unrelated", arguments: {})
client.call_tool(name: "test_mrtr_no_state", arguments: {})
client.call_tool(name: "test_mrtr_no_result_type", arguments: {})
when "http-custom-headers"
# SEP-2243: the harness names the exact tool calls (with encoding edge-case values) via context;
# listing first teaches the transport the `x-mcp-header` declarations it mirrors.
client.tools
(conformance_context["toolCalls"] || []).each do |tool_call|
client.call_tool(name: tool_call["name"], arguments: tool_call["arguments"])
end
when "http-invalid-tool-headers"
# SEP-2243: the listing carries one valid tool among invalid `x-mcp-header` definitions;
# calling only the valid one shows the invalid definitions did not block it.
client.tools
client.call_tool(name: "valid_tool", arguments: { "message" => "hello" })
when "http-standard-headers"
# SEP-2243: the harness inspects the `Mcp-Method` header on every POST and the `Mcp-Name` header on
# the name-carrying requests, so drive one of each standard request kind.
tools = client.tools
tool = tools.find { |t| t.name == "test_headers" } || tools.first
abort("No tool exposed by conformance server for #{scenario}") unless tool
client.call_tool(tool: tool, arguments: {})
resource = client.resources.first
abort("No resource exposed by conformance server for #{scenario}") unless resource
client.read_resource(uri: resource["uri"])
prompt = client.prompts.first
abort("No prompt exposed by conformance server for #{scenario}") unless prompt
client.get_prompt(name: prompt["name"])
when "json-schema-ref-no-deref"
# SEP-2106: listing tools whose schemas carry `$ref` must not trigger network dereferencing.
client.tools
when "tools_call"
tools = client.tools
add_numbers = tools.find { |t| t.name == "add_numbers" }
abort("Tool add_numbers not found") unless add_numbers
client.call_tool(tool: add_numbers, arguments: { a: 1, b: 2 })
when "sse-retry"
# SEP-1699: the server closes the tools/call SSE stream right after a priming event.
# The transport waits the server's `retry:` interval, reconnects with a GET carrying `Last-Event-ID`,
# and receives the tool result on the resumed stream; the harness verifies the reconnect,
# its timing, and the header.
tools = client.tools
test_reconnection = tools.find { |t| t.name == "test_reconnection" }
abort("Tool test_reconnection not found") unless test_reconnection
client.call_tool(tool: test_reconnection, arguments: {})
when "elicitation-sep1034-client-defaults"
# SEP-1034: the server sends `elicitation/create` on the tools/call SSE stream with every property declaring
# a `default` and none required. The handler accepts and fills the omitted fields from those defaults.
client.on_elicitation do |params|
{ action: "accept", content: MCP::Client::Elicitation.apply_defaults(params["requestedSchema"] || {}) }
end
tools = client.tools
defaults_tool = tools.find { |t| t.name == "test_client_elicitation_defaults" }
abort("Tool test_client_elicitation_defaults not found") unless defaults_tool
client.call_tool(tool: defaults_tool, arguments: {})
when %r|\Aauth/|
# Auth-only scenarios: the protocol-level checks (PRM/AS metadata, DCR, PKCE, token usage)
# are observed by the conformance server during `connect` and the subsequent request below.
# Listing tools forces a second authenticated MCP request so the bearer token usage check fires.
tools = client.tools
# `auth/scope-step-up` only fires its escalation 403 on `tools/call`, not `tools/list`,
# so the client must actually invoke a tool to drive the second authorization request
# the scenario asserts on.
if scenario == "auth/scope-step-up"
tool = tools.find { |t| t.name == "test-tool" } || tools.first
abort("No tool exposed by conformance server for #{scenario}") unless tool
client.call_tool(tool: tool, arguments: {})
end
else
abort("Unknown or unsupported scenario: #{scenario}")
end
rescue MCP::Client::OAuth::Flow::AuthorizationError, MCP::Client::RequestHandlerError, Faraday::Error => e
warn("#{scenario}: client ended in the expected terminal failure (#{e.class}: #{e.message})")
exit
end

Some files were not shown because too many files have changed in this diff Show more