Skip to content

Document That Tool Arguments Arrive with Symbol Keys#413

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:document_tool_argument_symbol_keys
Open

Document That Tool Arguments Arrive with Symbol Keys#413
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:document_tool_argument_symbol_keys

Conversation

@koic

@koic koic commented Jun 15, 2026

Copy link
Copy Markdown
Member

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.

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

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.

@koic koic force-pushed the document_tool_argument_symbol_keys branch 2 times, most recently from 648f553 to 4efef38 Compare June 15, 2026 18:09
## 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 modelcontextprotocol#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.
@koic koic force-pushed the document_tool_argument_symbol_keys branch from 4efef38 to 2c295fe Compare June 15, 2026 18:09
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.

Tool arguments arrive deep-symbolized, and demos and tests never exercise that shape

2 participants