Skip to content

Handle a _meta that is not an object without raising - #650

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
emirn:fix-non-object-meta
Nov 2, 2026
Merged

koic merged 1 commit into
modelcontextprotocol:mainfrom
emirn:fix-non-object-meta

Conversation

@emirn

@emirn emirn commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

MCP requires _meta to be an object, but a request whose _meta is a string, an array or a number
makes the server raise TypeError from dig instead of answering a client error:

  • Streamable HTTP, MCP-Protocol-Version: 2026-07-28: validate_modern_headers calls
    params.dig(:_meta, ...), and the catch-all in handle_modern answers HTTP 500 -32603 for every method.
    RequestEnvelope.parse! already treats a non-object _meta as empty to answer -32602, but the header check runs first.
  • Legacy requests: call_tool and build_server_context call dig(:_meta, :progressToken),
    so tools/call and prompts/get answer -32603 Internal error. tools/list already ignores the value.

A non-object _meta carries no protocol version and no progress token, so these reads now skip it:

  • the 2026-07-28 path answers HTTP 400 -32602 from envelope validation;
  • legacy tools/call and prompts/get ignore it, as tools/list does and as RequestEnvelope.modern? does.

Similar to #609 / #616 (a malformed Accept member answered 500).

How Has This Been Tested?

  • test/mcp/server/transports/streamable_http_transport_test.rb: a modern POST with a string or array _meta answers 400 with -32602.
  • test/mcp/server_test.rb: tools/call and prompts/get with a string or array _meta run normally.
  • Both tests fail without the change (500 instead of 400; -32603 "Internal error calling tool"). rake test and rake rubocop pass.

Breaking Changes

None. Requests with an object _meta are handled as before. Only requests that raised now get a client error (2026-07-28) or are served (legacy).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

MCP requires `_meta` to be an object. When a request's `_meta` is a
string, an array or a number, three `dig(:_meta, ...)` calls raised
TypeError instead of answering a client error:

- `validate_modern_headers` (Streamable HTTP, 2026-07-28): every method
  answered HTTP 500 -32603. `RequestEnvelope.parse!` already treats a
  non-object `_meta` as empty to answer -32602, but the header check
  runs first.
- `call_tool`: legacy `tools/call` answered -32603 Internal error.
- `build_server_context`: legacy `prompts/get` answered -32603.

A non-object `_meta` carries no protocol version and no progress token,
so these reads now skip it. The 2026-07-28 path answers HTTP 400 -32602,
and legacy `tools/call` and `prompts/get` ignore the value, as legacy
`tools/list` and `RequestEnvelope.modern?` already do.
@koic
koic merged commit 51db52a into modelcontextprotocol:main Nov 2, 2026
13 checks passed
koic added a commit that referenced this pull request Nov 3, 2026
## Motivation and Context

`_meta` is an object on every request (`MetaObject` in the schema). After #650, a String, Array, or number there no
longer raises from `dig`, and the modern lifecycle answers -32602 because the envelope keys are missing; the legacy
lifecycle, though, went on to run the handler. That left two gaps. The value still reached `server_context[:_meta]`,
where the `server_context.dig(:_meta, ...)` the server context page shows raises `TypeError` on a String or an Array,
so a handler written as documented still turned the request into -32603. And the TypeScript and Python servers refuse
such a request as invalid params on both lifecycles, through schema validation of `params` and their inbound checks,
so the Ruby server answered differently from both.

The request is now refused with -32602 "Invalid params: _meta must be an object" before the envelope is read and
before any handler runs, on both lifecycles. A notification handed to a handler is dropped the same way, without a response,
as the TypeScript server drops a notification with invalid params; `notifications/cancelled` is served before
the check and reads no `_meta`. A `null` `_meta` reads as absent, as it does for the Python server's request model.
The server context page states the rule, since it tells handlers to read `_meta` from `server_context`.

## How Has This Been Tested?

test/mcp/server_test.rb and test/mcp/server/transports/streamable_http_transport_test.rb. Before the fix,
legacy `tools/call` with `_meta: "not-an-object"` ran the tool, and a tool reading `server_context.dig(:_meta, :key)`
answered -32603 "Internal error" with "Internal error calling tool meta_reader" as the data.

## Breaking Changes

A legacy-lifecycle request whose `_meta` is not an object now answers -32602. Released versions served such
a request for methods that never read `_meta`, such as `tools/list`, and answered -32603 for `tools/call` and `prompts/get`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants