Compare commits
233 changed files with 2862 additions and 55049 deletions
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
|
|
@ -4,22 +4,16 @@ on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
entry:
|
entry:
|
||||||
- { ruby: '2.7.0', allowed-failure: false } # Minimum supported Ruby version.
|
- { ruby: 3.2, allowed-failure: false }
|
||||||
- { ruby: '3.0', allowed-failure: false }
|
- { ruby: 3.3, allowed-failure: false }
|
||||||
- { ruby: '3.1', allowed-failure: false }
|
- { ruby: 3.4, allowed-failure: false }
|
||||||
- { ruby: '3.2', allowed-failure: false }
|
- { ruby: head, allowed-failure: true }
|
||||||
- { ruby: '3.3', allowed-failure: false }
|
|
||||||
- { ruby: '3.4', allowed-failure: false }
|
|
||||||
- { ruby: '4.0', allowed-failure: false }
|
|
||||||
- { ruby: 'head', allowed-failure: true }
|
|
||||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v5
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.entry.ruby }}
|
ruby-version: ${{ matrix.entry.ruby }}
|
||||||
|
|
@ -29,26 +23,11 @@ jobs:
|
||||||
|
|
||||||
rubocop:
|
rubocop:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
name: RuboCop
|
name: RuboCop
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v5
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 4.0 # Specify the latest supported Ruby version.
|
ruby-version: 3.2 # Specify the oldest supported Ruby version.
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
- run: bundle exec rake rubocop
|
- 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
|
|
||||||
|
|
|
||||||
29
.github/workflows/conformance.yml
vendored
29
.github/workflows/conformance.yml
vendored
|
|
@ -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
|
|
||||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
|
|
@ -16,42 +16,10 @@ jobs:
|
||||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
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
|
contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v5
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
ruby-version: 4.0
|
ruby-version: 3.4
|
||||||
- uses: rubygems/release-gem@v1
|
- 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
5
.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
.ruby-version
|
.ruby-version
|
||||||
/*.gem
|
|
||||||
/.bundle/
|
/.bundle/
|
||||||
/.yardoc
|
/.yardoc
|
||||||
/_yardoc/
|
/_yardoc/
|
||||||
|
|
@ -9,7 +8,3 @@
|
||||||
/spec/reports/
|
/spec/reports/
|
||||||
/tmp/
|
/tmp/
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
docs/_site/
|
|
||||||
docs/.jekyll-cache/
|
|
||||||
docs/.jekyll-metadata
|
|
||||||
docs/_data/versions.yml
|
|
||||||
|
|
|
||||||
11
.rubocop.yml
11
.rubocop.yml
|
|
@ -5,19 +5,8 @@ plugins:
|
||||||
- rubocop-minitest
|
- rubocop-minitest
|
||||||
- rubocop-rake
|
- rubocop-rake
|
||||||
|
|
||||||
AllCops:
|
|
||||||
TargetRubyVersion: 2.7
|
|
||||||
SuggestExtensions: false
|
|
||||||
|
|
||||||
Gemspec/DevelopmentDependencies:
|
Gemspec/DevelopmentDependencies:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
Lint/IncompatibleIoSelectWithFiberScheduler:
|
|
||||||
Enabled: true
|
|
||||||
|
|
||||||
Minitest/LiteralAsActualArgument:
|
Minitest/LiteralAsActualArgument:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
Naming/MethodName:
|
|
||||||
AllowedPatterns:
|
|
||||||
- '\Atest_'
|
|
||||||
|
|
|
||||||
49
AGENTS.md
49
AGENTS.md
|
|
@ -6,18 +6,16 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
|
||||||
|
|
||||||
## Dev environment setup
|
## 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
|
- 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
|
## Build and test commands
|
||||||
|
|
||||||
- `bundle install` - Install dependencies
|
- `bundle install` - Install dependencies
|
||||||
- `rake test` - Run all tests
|
- `rake test` - Run all tests
|
||||||
- `rake rubocop` - Run linter
|
- `rake rubocop` - Run linter
|
||||||
- `rake` - Run linting, tests, and the conformance suite (default task)
|
- `rake` - Run tests and linting (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)
|
|
||||||
- `ruby -I lib -I test test/path/to/specific_test.rb` - Run single test file
|
- `ruby -I lib -I test test/path/to/specific_test.rb` - Run single test file
|
||||||
- `gem build mcp.gemspec` - Build the gem
|
- `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
|
- Use frozen string literals
|
||||||
- Follow Ruby community conventions
|
- Follow Ruby community conventions
|
||||||
- Keep dependencies minimal
|
- 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
|
## 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`):
|
**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
|
- 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`
|
- Supports custom method registration via `define_custom_method`
|
||||||
- Handles instrumentation, exception reporting, and notifications
|
- Handles instrumentation, exception reporting, and notifications
|
||||||
- Uses JsonRpcHandler for request processing
|
- 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`):
|
**MCP::Client** (`lib/mcp/client.rb`):
|
||||||
|
|
||||||
- Client interface for communicating with MCP servers
|
- 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.)
|
- Protocol version 2025-03-26+ supports tool annotations (destructive_hint, idempotent_hint, etc.)
|
||||||
- Validation is configurable via `configuration.validate_tool_call_arguments`
|
- 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**:
|
**Context Passing**:
|
||||||
|
|
||||||
- `server_context` hash passed through tool/prompt calls for request-specific data
|
- `server_context` hash passed through tool/prompt calls for request-specific data
|
||||||
- Methods can accept `server_context:` keyword argument for accessing context
|
- 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
|
### 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
|
- **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") { ... }`
|
||||||
509
CHANGELOG.md
509
CHANGELOG.md
|
|
@ -7,513 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.4.0] - 2025-10-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
@ -591,4 +84,4 @@ apart from the narrow exceptions documented in [VERSIONING.md](VERSIONING.md).
|
||||||
|
|
||||||
## [0.1.0] - 2025-05-30
|
## [0.1.0] - 2025-05-30
|
||||||
|
|
||||||
Initial release in collaboration with Shopify
|
Initial release
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
@AGENTS.md
|
|
||||||
16
Gemfile
16
Gemfile
|
|
@ -6,26 +6,20 @@ source "https://rubygems.org"
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
# Specify development dependencies below
|
# Specify development dependencies below
|
||||||
gem "rubocop-minitest", ">= 0.39.0", require: false
|
gem "rubocop-minitest", require: false
|
||||||
gem "rubocop-rake", 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 "puma", ">= 5.0.0"
|
||||||
gem "rack-cors"
|
gem "rack", ">= 2.0.0"
|
||||||
gem "rackup", ">= 2.1.0"
|
gem "rackup", ">= 2.1.0"
|
||||||
|
|
||||||
gem "activesupport"
|
gem "activesupport"
|
||||||
# Fix io-console install error when Ruby 3.0.
|
gem "debug"
|
||||||
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 "rake", "~> 13.0"
|
gem "rake", "~> 13.0"
|
||||||
gem "sorbet-static-and-runtime" if RUBY_VERSION >= "3.0"
|
gem "sorbet-static-and-runtime"
|
||||||
gem "yard", "~> 0.9"
|
|
||||||
gem "yard-sorbet", "~> 0.9" if RUBY_VERSION >= "3.1"
|
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem "event_stream_parser", ">= 1.0"
|
|
||||||
gem "faraday", ">= 2.0"
|
gem "faraday", ">= 2.0"
|
||||||
gem "minitest", "~> 5.1", require: false
|
gem "minitest", "~> 5.1", require: false
|
||||||
gem "mocha"
|
gem "mocha"
|
||||||
|
|
|
||||||
216
LICENSE
216
LICENSE
|
|
@ -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
21
LICENSE.txt
Normal 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
899
README.md
|
|
@ -1,22 +1,13 @@
|
||||||
# MCP Ruby SDK [](https://rubygems.org/gems/mcp) [](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE) [](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml)
|
# MCP Ruby SDK [](https://rubygems.org/gems/mcp) [](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE.txt) [](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml)
|
||||||
|
|
||||||
The official Ruby SDK for Model Context Protocol servers and clients.
|
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
|
## Installation
|
||||||
|
|
||||||
Add this line to your application's Gemfile:
|
Add this line to your application's Gemfile:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
gem "mcp"
|
gem 'mcp'
|
||||||
```
|
```
|
||||||
|
|
||||||
And then execute:
|
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.
|
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,
|
The `MCP::Server` class is the core component that handles JSON-RPC requests and responses.
|
||||||
and a client that spawns such a server and drives it over stdio.
|
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
|
```ruby
|
||||||
require "mcp"
|
require "mcp"
|
||||||
|
|
@ -76,64 +210,729 @@ transport = MCP::Server::Transports::StdioTransport.new(server)
|
||||||
transport.open
|
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
|
```console
|
||||||
$ ruby server.rb
|
$ ruby examples/stdio_server.rb
|
||||||
{"jsonrpc":"2.0","id":"1","method":"ping"}
|
{"jsonrpc":"2.0","id":"1","method":"ping"}
|
||||||
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
|
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
|
||||||
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
|
{"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;
|
### Configuration
|
||||||
see [Server Transports](https://ruby.sdk.modelcontextprotocol.io/server/transports/).
|
|
||||||
|
|
||||||
### MCP Client
|
The gem can be configured using the `MCP.configure` block:
|
||||||
|
|
||||||
A minimal client spawns a stdio server as a subprocess, connects, and lists and calls its tools:
|
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
stdio_transport = MCP::Client::Stdio.new(
|
MCP.configure do |config|
|
||||||
command: "bundle",
|
config.exception_reporter = ->(exception, server_context) {
|
||||||
args: ["exec", "ruby", "path/to/server.rb"],
|
# Your exception reporting logic here
|
||||||
env: { "API_KEY" => "my_secret_key" },
|
# For example with Bugsnag:
|
||||||
read_timeout: 30
|
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.
|
### Server Context and Configuration Block Data
|
||||||
client.connect
|
|
||||||
|
|
||||||
# List available tools.
|
#### `server_context`
|
||||||
tools = client.tools
|
|
||||||
tools.each do |tool|
|
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.
|
||||||
puts "Tool: #{tool.name} - #{tool.description}"
|
|
||||||
|
**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
|
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(
|
response = client.call_tool(
|
||||||
tool: tools.first,
|
tool: tools.first,
|
||||||
arguments: { message: "Hello, world!" }
|
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`;
|
#### HTTP Authorization
|
||||||
see [Client Transports](https://ruby.sdk.modelcontextprotocol.io/client/transports/).
|
|
||||||
|
|
||||||
## 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),
|
```ruby
|
||||||
including a complete Rails application in [`examples/rails`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples/rails).
|
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)
|
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.
|
||||||
- [SDK API documentation](https://rubydoc.info/gems/mcp)
|
|
||||||
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
|
||||||
12
RELEASE.md
12
RELEASE.md
|
|
@ -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.
|
|
||||||
51
ROADMAP.md
51
ROADMAP.md
|
|
@ -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)).
|
|
||||||
88
Rakefile
88
Rakefile
|
|
@ -14,90 +14,4 @@ require "rubocop/rake_task"
|
||||||
|
|
||||||
RuboCop::RakeTask.new
|
RuboCop::RakeTask.new
|
||||||
|
|
||||||
task default: [:rubocop, :test, "conformance:test"]
|
task default: [:test, :rubocop]
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
||||||
21
SECURITY.md
21
SECURITY.md
|
|
@ -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)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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!"
|
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue