Commit graph ruby-sdk/docs
Author SHA1 Message Date
Koichi ITO
d20eac0a4b
Merge pull request #476 from koic/support_2026_07_28_as_latest_protocol_version
Support 2026-07-28 as the Latest Protocol Version
2026-08-01 11:06:39 +09:00
Koichi ITO
dc25610162 Support 2026-07-28 as the Latest Protocol Version
## Motivation and Context

Clients that require MCP 2026-07-28, such as ChatGPT, reject connections because the SDK
does not advertise that version: `Server#init` answers a 2026-07-28 request with 2025-11-25,
the Streamable HTTP transport 400s an `MCP-Protocol-Version: 2026-07-28` header,
and `server/discover` omits the version from `supportedVersions`.

Making 2026-07-28 the latest entry in `SUPPORTED_STABLE_PROTOCOL_VERSIONS` fixes all three at once,
since they all read that list. The version now serves both lifecycles of the SEP-2575 dual-era model:
negotiable through the legacy `initialize` handshake, and simultaneously the version of
the per-request modern lifecycle.

Parts of the 2026-07-28 feature set remain unimplemented, such as the stateless modern lifecycle
over the transports, and are being built separately. This change deliberately limits itself to
the version bump, focusing first on eliminating the connection errors that 2026-07-28 clients hit.

Known follow-up: the in-progress modern HTTP path routes requests to the sessionless modern lifecycle
when the header version is absent from `SUPPORTED_STABLE_PROTOCOL_VERSIONS`, so that routing predicate
needs rework on top of this change.

Closes #474

## How Has This Been Tested?

New regression tests: `initialize` with `protocolVersion: "2026-07-28"` echoes it back (`server_test.rb`),
a follow-up POST with `MCP-Protocol-Version: 2026-07-28` passes header validation
(`streamable_http_transport_test.rb`), and `Configuration.new(protocol_version: "2026-07-28")` is accepted
(`configuration_test.rb`).

`bundle exec rake test` passes (1454 runs, 0 failures). The conformance server suite passes 30/30.
The client `initialize` scenario is added to the expected-failures baseline: the conformance framework pins
the client's requested version to 2025-11-25 and does not accept 2026-07-28 yet; the entry carries a TODO
to remove it when the framework catches up.

## Breaking Changes

Servers that do not pin `configuration.protocol_version` now answer version negotiation fallbacks
and default client requests with 2026-07-28 instead of 2025-11-25. 2025-11-25 remains fully supported
and negotiable; clients that request it are unaffected.
2026-07-31 12:55:21 +09:00
Koichi ITO
c2879fb640 Fix flash of unstyled content when loading the docs site
## Motivation and Context

Opening https://ruby.sdk.modelcontextprotocol.io/ briefly showed a black half-circle
at the bottom of the screen. The theme preference script in `head_custom.html` called
`window.jtd.setTheme` on every page load. That helper rewrites the `href` of
the render-blocking color stylesheet link, which detaches the loaded stylesheet
and refetches it, so the page paints unstyled until the replacement arrives.
During that window the search magnifier SVG, sized only by CSS, rendered at its fallback size
and appeared as a large black half-circle near the bottom of the viewport.

The site sets no `color_scheme`, so the default stylesheet is identical to the light one
and most visitors paid the flash for a no-op swap.

This change replaces the `jtd.setTheme` call with a local swapper that:

- Leaves the stylesheet untouched when the resolved theme already matches
  (treating `default` as `light`).
- Swaps non-destructively otherwise: the new link is inserted next to the current one
  and the old link is removed only after the new stylesheet has loaded. On error the new link
  is dropped and the page keeps its current styles.
- Sets `blocking="render"` on the new link so supporting browsers hold first paint until
  the dark stylesheet arrives.

It also pre-applies the dark background and text colors in `custom.scss`, keyed on
the `data-theme` attribute that is set synchronously in the head, to soften the light-colored
first paint for dark-preference visitors in browsers without `blocking="render"` support.

## How Has This Been Tested?

Mirrored the live site locally, patched the inline script with the new code,
and drove it in Chrome:

- Unpatched baseline reproduced the old behavior: the link `href` was rewritten
  and `just-the-docs-light.css` was fetched in addition to `just-the-docs-default.css`.
- Patched light and system-light loads keep the original link untouched with no extra stylesheet request.
- Patched dark loads settle on a single `just-the-docs-dark.css` link, with `jtd.getTheme()`
  still reporting the right theme.
- Rapid toggling keeps at most two color stylesheet links and settles on exactly one matching
  the final preference.
- With `just-the-docs-dark.css` returning 404, the failed link is dropped and the page stays styled.

## Breaking Changes

None.
2026-07-30 12:44:28 +09:00
Koichi ITO
b6df40b134 Support class-based Resource and ResourceTemplate definitions
## Motivation and Context

Resolve the design difference raised in issue #98: Tools and Prompts support class-based definitions
while Resources and Resource Templates were plain data objects requiring a `resources_read_handler` block
with manual URI routing.

`MCP::Resource` and `MCP::ResourceTemplate` now support the same class-level DSL as `MCP::Tool`
and `MCP::Prompt` (`uri`, `resource_name`, `title`, `description`, `icons`, `mime_type`, `annotations`,
`size`, `meta`, and `uri_template` / `resource_template_name` for templates), plus `define` factory methods
and `MCP::Server#define_resource` / `#define_resource_template`.

The default `resources/read` handler now routes requests automatically: an exact URI match calls
the class-based resource's `contents` method, and class-based resource templates match
simple RFC 6570 level 1 `{var}` expressions, passing extracted variables as keyword arguments.
`contents` can opt in to a `server_context:` keyword argument. When class-based entries are registered
and no match is found, the server responds with -32602 carrying the URI in error data per SEP-2164.

## How Has This Been Tested?

- Added regression tests covering the class-level DSL, URI template matching, auto-routing,
  `server_context:` opt-in, error responses, and backward compatibility of the no-op default.
- Verified end-to-end with a smoke script exercising exact-match reads, template-match reads,
  unknown URIs, and legacy instance-only setups.

## Breaking Changes

No. Instance-based registration, `resources_read_handler` precedence, and the no-op `[]` response for
setups without class-based entries are unchanged. Reassigning `Server#resources` now rebuilds
the internal URI index.

Closes #98
2026-07-10 22:39:42 +09:00
Koichi ITO
267b8fa628 Bound stdio frame reads with max_line_bytes
## Motivation and Context

CRuby's `IO#gets` without a limit reads until the next newline with no upper bound.
A peer that never emits a newline can therefore grow a single String until the process
is killed by the OS OOM killer.

This is a denial-of-service exposure (GHSA-7683-3w9x-ch42), identified during
a cross-SDK audit of the same unbounded-buffer pattern across the MCP SDKs.
It is most relevant when a host application spawns a sandboxed or untrusted MCP server
and pipes its stdout into an unconstrained host process, where a memory-capped peer
can exhaust the host.

Each frame is now capped at a configurable `max_line_bytes` (default 4 MiB,
large enough for any realistic frame including base64-embedded images,
and the value recommended in the advisory). A frame that reaches the limit without
a newline is treated as a transport error: the client raises, and the server reports
the error and closes the connection. A short final frame that ends at EOF without
a trailing newline is still accepted, so legitimate traffic is unaffected.

Both constructors validate `max_line_bytes:` as a positive Integer,
since `nil` or a non-positive value makes `IO#gets` read without an upper bound and
would silently disable the protection.

## How Has This Been Tested?

Server tests cover the `open` read loop stopping and reporting on an over-limit frame,
the `send_request` read loop raising on an over-limit response, and a valid final frame
without a trailing newline still being processed.

Client tests cover `read_response` raising on an over-limit response frame and
a normal response still round-tripping within a custom `max_line_bytes`.

Both transports also have tests rejecting invalid `max_line_bytes:` values
(`nil`, zero, negative, non-Integer) with an `ArgumentError`.

The full test suite and RuboCop pass.

## Breaking Changes

None. The default 4 MiB limit is far larger than any realistic JSON-RPC frame,
so existing well-behaved traffic is unaffected. The new `max_line_bytes:` keyword
on both transport constructors is purely additive and optional.
2026-07-05 21:30:28 +09:00
Koichi ITO
2c295fe930 Document That Tool Arguments Arrive with Symbol Keys
## Motivation and Context

The transports parse incoming JSON with `symbolize_names: true`, so a tool receives every key,
at every nesting level, as a Ruby symbol. The dispatch in `call_tool_with_args` only runs
a shallow `transform_keys(&:to_sym)`, which is effectively a no-op given the deep parse upstream.
Nothing in the SDK exercised that delivered shape: the README and examples take flat or primitive arguments,
and the tool tests invoke `call` with Ruby keyword arguments, so none go through the JSON parse to dispatch path.
A tool that reads a nested object with string keys (`payload["subject"]`) therefore passes its tests and then
returns nil against a real client.

Ruby keyword arguments bind only on symbol keys, so the top-level keys must be symbols for
the ergonomic `def call(message:, ...)` API to work. Aligning fully with the JSON wire shape that the spec and
the Python and TypeScript SDKs use (string keys) is not possible without a breaking change to that API.
This change therefore documents the existing contract rather than altering behavior.

Closes #412

## How Has This Been Tested?

New regression tests in `test/mcp/server_test.rb`:

- `tools/call` through the full `handle_json` path delivers nested object arguments with symbol keys at every level,
  and string-key access on a nested value returns nil.
- A tool called under the JSON-round-tripped argument shape receives symbol keys, mirroring what a transport hands it at runtime.

## Breaking Changes

None. This is a documentation and test change with no behavior change.
The `lib/mcp/server.rb` edit is a clarifying comment on the existing key handling.

## Additional context

Making nested objects tolerant of both string and symbol keys (indifferent access) is a possible future improvement
that would remove the footgun and move closer to the other SDKs. It is intentionally left out of scope here and
can be designed separately, since it is a behavior and dependency decision rather than a documentation fix.
2026-06-16 03:09:55 +09:00
Yuki Minamiya
a28a343741 [Doc] Fix typos in MCP::Client method references
The methods are defined as singular `call_tool` and `read_resource`,
not plural `call_tools` and `read_resources`:
dd7daca/lib/mcp/client.rb (L315)
dd7daca/lib/mcp/client.rb (L331)
2026-05-25 03:01:23 +09:00
Ates Goral
5f94073503 docs: add dark mode support 2026-05-11 08:46:40 -04:00
Koichi ITO
46b738e945 Add client-level connect handshake to stdio transport
## Motivation and Context

This commit implements Phase 1 of 3 for #334.

The MCP specification requires an `initialize` request followed by
a `notifications/initialized` notification before any other interaction.
PR #327 added `MCP::Client#connect` and `MCP::Client::HTTP#connect`
to expose this handshake explicitly for the HTTP transport,
but stdio remained on its private `initialize_session` lazy-init path,
inconsistent with the Python SDK (`ClientSession.initialize()`) and
TypeScript SDK (`Client.connect(transport)`),
both of which require an explicit handshake call regardless of transport.

The implicit init path on the first `send_request` is
preserved as a non-breaking compatibility shim. Public API added on `Stdio`:

- `connect(client_info:, protocol_version:, capabilities:)` performs the
  handshake, validates the negotiated `protocolVersion` against
  `MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS`,
  caches the `InitializeResult`, and is idempotent.
- `connected?` reports handshake completion.
- `server_info` exposes the cached `InitializeResult`.

Documentation and examples are updated so new users learn the explicit pattern
from the start.

## How Has This Been Tested?

Fifteen regression tests cover: explicit handshake, `server_info` caching,
idempotence, custom keyword arguments, JSON-RPC error response, missing
or non-Hash `result`, unsupported protocol version with state rollback,
state rollback when the `notifications/initialized` write fails,
retry after a failed handshake, and the `connected?` / `server_info` lifecycle.

The pre-existing stdio tests pass without modification, verifying that
the implicit-init compatibility path still works.

## Breaking Changes

None for the typical "implicit init via `client.tools` / `client.call_tool`" usage.
Existing stdio clients keep working unchanged.

Three observable behavior changes for code that inspects transport state directly:

- `Client#connected?` for a never-initialized stdio client returns `false`
  (previously fell through to a `true` default via the `respond_to?` fallback).
- `Client#server_info` for stdio returns the cached `InitializeResult`
  after the handshake (previously always `nil` via the fallback).
- The implicit handshake path now validates the server's negotiated
  protocol version, raising `RequestHandlerError` for unsupported versions.
  Production servers that return one of the supported versions are unaffected;
  this closes a pre-existing spec-conformance gap on stdio.
2026-05-03 15:57:22 +09:00
Ates Goral
ddfbfad81e Add client-level connect for initialize handshake 2026-05-01 20:17:45 -04:00
Ates Goral
ff3a42f3fb Add HTTP client close for explicit session termination
Per MCP spec, clients that no longer need a session SHOULD send an HTTP
DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
terminate it. Servers MAY respond with 405 Method Not Allowed when they
don't support client-initiated termination.

`MCP::Client::HTTP#close` sends the DELETE with session headers, clears
local session state regardless of outcome, and is a no-op when no
session has been established.

https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
2026-04-20 13:58:41 -04:00
Ates Goral
07226b7cf1 Track Mcp-Session-Id and protocol version in HTTP client
Per the Streamable HTTP spec, if the server returns an Mcp-Session-Id
header during `initialize`, the client MUST include it on subsequent
requests. Capture the session ID and protocol version from the
`initialize` response and attach them automatically on subsequent
POSTs.

Per the Protocol Version Header section of the spec, the client MUST
also include `MCP-Protocol-Version` on all subsequent requests so the
server can respond based on the negotiated protocol version.

Map 404 responses to a new `SessionExpiredError` (a subclass of
`RequestHandlerError` for backward compatibility) and clear local
session state so callers can start a fresh session with a new
`initialize` request, as required by the spec.

- https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
- https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 00:23:07 -04:00
Ates Goral
b832933f9f Parse SSE responses in HTTP client via event_stream_parser
The Streamable HTTP spec allows servers to respond with either
`application/json` or `text/event-stream`. The client previously
rejected the latter. Add SSE parsing via the optional
`event_stream_parser` gem (users add it to their Gemfile when their
server uses SSE), dispatch on the response Content-Type, and scan the
event stream for the first JSON-RPC response message.

Document the new optional dependency in both the README's HTTP
Transport Layer section and docs/building-clients.md.

No other Streamable HTTP features (202 handling, session IDs,
`connect`, DELETE termination) are included here; those will land in
follow-up PRs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:54:03 -04:00
Koichi ITO
0c5432b027 [Doc] Update homepage with just-the-docs theme
Follow-up to https://github.com/modelcontextprotocol/ruby-sdk/pull/181.

The landing page at https://ruby.sdk.modelcontextprotocol.io previously only listed version links
to RubyDoc.info. Add multi-page documentation using the just-the-docs Jekyll theme with
sidebar navigation, search, and code highlighting.
Pages: Introduction, Installation, Building Servers, and Building Clients.

This PR introduces a basic set of pages. Additional content can be added incrementally in future updates.
2026-04-15 22:48:15 +09:00
Koichi ITO
5c51f7dde8 [Doc] Add CNAME file to preserve custom domain across deployments
Accessing https://ruby.sdk.modelcontextprotocol.io/ is expected to show
the content of https://modelcontextprotocol.github.io/ruby-sdk/,
but it currently does not. This change fixes that issue.

The `generate-gh-pages.sh` script cleans all files from the gh-pages branch root
before copying from `docs/`. This removes the CNAME file that GitHub Pages uses for
the custom domain, causing `ruby.sdk.modelcontextprotocol.io` to break on every release.
https://github.com/modelcontextprotocol/ruby-sdk/blob/v0.10.0/bin/generate-gh-pages.sh#L91-L93

By placing the CNAME file in `docs/`, it will be automatically copied to `gh-pages` during each deployment.
https://github.com/modelcontextprotocol/ruby-sdk/blob/v0.10.0/bin/generate-gh-pages.sh#L95-L97

The following is a PR related to the configuration of ruby.sdk.modelcontextprotocol.io.

- https://github.com/modelcontextprotocol/ruby-sdk/pull/181
- https://github.com/modelcontextprotocol/dns/pull/14
- https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2486
2026-03-30 13:27:58 +09:00
Jonathan Hefner
281c334075 Add API documentation with GitHub Pages deployment
This adds automated deployment of a versioned API documentation landing
page using GitHub Pages. The landing page links to documentation hosted
on RubyDoc.info, the de facto standard documentation site for the Ruby
ecosystem. The landing page will update to reflect new releases.

Changes
-------

1. **Documentation generation script** (see `bin/generate-gh-pages.sh`)

    Copies and commits documentation files to the gh-pages branch. The
    script uses git worktrees to isolate the process from the main
    workspace.

    The script:

    - Creates the gh-pages branch as an orphan branch if it doesn't exist
    - Parses semantic versions from tag names, ignoring arbitrary
      prefixes (e.g., tags `1.2.3`, `v1.2.3`, and `release-1.2.3` all
      create `v1.2.3`)
    - Determines the latest version using semantic version sorting
    - Copies static Jekyll template files from `docs/` to the gh-pages
      root (for the latest version only)
    - Generates `docs/_data/versions.yml` with a list of all versions
      for the Jekyll templates to consume

2. **Jekyll template files** (see `docs/` directory)

    - `docs/_config.yml` - Jekyll configuration
    - `docs/index.md` - Landing page template that links to all versions
      on RubyDoc.info based on generated `docs/_data/versions.yml`
    - `docs/latest/index.html` - Redirect page template that redirects
      to the latest documentation on RubyDoc.info

3. **GitHub Actions workflow** (see `.github/workflows/release.yml`)

    Added a `publish_gh_pages` job that runs after the `publish_gem` job
    on release events. The job invokes the generation script with the
    release tag name, then pushes the updated gh-pages branch to deploy
    the documentation.

4. **CI validation** (see `.github/workflows/ci.yml`)

    Added a `yard` job that runs YARD validation with `--no-output`.
    This ensures YARD can successfully parse the codebase (without
    generating output), catching documentation issues early in CI.

5. **YARD dependencies** (see `Gemfile`)

    Added `yard` and `yard-sorbet` gems as development dependencies to
    support documentation validation in CI.

    YARD documentation is automatically generated and hosted by
    RubyDoc.info when the gem is published to RubyGems.org.

6. **Documentation links** (see `README.md` and `mcp.gemspec`)

    Added links to the published API documentation on RubyDoc.info in
    the README and gemspec metadata.

Documentation URL
-----------------

Custom documentation will be available at:
https://modelcontextprotocol.github.io/ruby-sdk/

RubyDoc.info documentation will be accessible at:
- https://rubydoc.info/gems/mcp (latest version)
- https://rubydoc.info/gems/mcp/1.2.3 (specific versions)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 12:21:38 -06:00