From 50cd86c01c9173bf757f63bed47ab8fd99d1f16f Mon Sep 17 00:00:00 2001 From: Eric Kreutzer Date: Wed, 4 Mar 2026 10:11:07 -0700 Subject: [PATCH] Add automatic _meta parameter extraction support --- README.md | 44 +++++++ lib/mcp/server.rb | 21 ++- test/mcp/server_context_test.rb | 221 ++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 88d6b798..ef685187 100644 --- a/README.md +++ b/README.md @@ -375,6 +375,50 @@ server = MCP::Server.new( This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks. +#### Request-specific `_meta` Parameter + +The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`. + +**Access Pattern:** + +When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`: + +```ruby +class MyTool < MCP::Tool + def self.call(message:, server_context:) + # Access provider-specific metadata + session_id = server_context.dig(:_meta, :session_id) + request_id = server_context.dig(:_meta, :request_id) + + # Access server's original context + user_id = server_context.dig(:user_id) + + MCP::Tool::Response.new([{ + type: "text", + text: "Processing for user #{user_id} in session #{session_id}" + }]) + end +end +``` + +**Client Request Example:** + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "my_tool", + "arguments": { "message": "Hello" }, + "_meta": { + "session_id": "abc123", + "request_id": "req_456" + } + } +} +``` + #### Configuration Block Data ##### Exception Reporter diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index e9a4e507..862c3cd1 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -419,7 +419,7 @@ def call_tool(request) end end - call_tool_with_args(tool, arguments) + call_tool_with_args(tool, arguments, server_context_with_meta(request)) rescue RequestHandlerError raise rescue => e @@ -445,7 +445,7 @@ def get_prompt(request) prompt_args = request[:arguments] prompt.validate_arguments!(prompt_args) - call_prompt_template_with_args(prompt, prompt_args) + call_prompt_template_with_args(prompt, prompt_args, server_context_with_meta(request)) end def list_resources(request) @@ -488,7 +488,7 @@ def accepts_server_context?(method_object) parameters.any? { |type, name| type == :keyrest || name == :server_context } end - def call_tool_with_args(tool, arguments) + def call_tool_with_args(tool, arguments, server_context) args = arguments&.transform_keys(&:to_sym) || {} if accepts_server_context?(tool.method(:call)) @@ -498,12 +498,25 @@ def call_tool_with_args(tool, arguments) end end - def call_prompt_template_with_args(prompt, args) + def call_prompt_template_with_args(prompt, args, server_context) if accepts_server_context?(prompt.method(:template)) prompt.template(args, server_context: server_context).to_h else prompt.template(args).to_h end end + + def server_context_with_meta(request) + meta = request[:_meta] + if @server_context && meta + context = @server_context.dup + context[:_meta] = meta + context + elsif meta + { _meta: meta } + elsif @server_context + @server_context + end + end end end diff --git a/test/mcp/server_context_test.rb b/test/mcp/server_context_test.rb index 8307af54..88bf609b 100644 --- a/test/mcp/server_context_test.rb +++ b/test/mcp/server_context_test.rb @@ -414,5 +414,226 @@ def template(args, **kwargs) assert_equal "FlexiblePrompt: Hello (context: present)", response[:result][:messages][0][:content][:text] end + + test "tool receives _meta when provided in request params" do + class ToolWithMeta < Tool + tool_name "tool_with_meta" + description "A tool that uses _meta" + input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) + + class << self + def call(message:, server_context:) + meta_info = server_context.dig(:_meta, :provider, :metadata) + Tool::Response.new([ + { type: "text", content: "Message: #{message}, Metadata: #{meta_info}" }, + ]) + end + end + end + + server = Server.new( + name: "test_server", + tools: [ToolWithMeta], + ) + + request = { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "tool_with_meta", + arguments: { message: "Hello" }, + _meta: { + provider: { + metadata: "test_value", + }, + }, + }, + } + + response = server.handle(request) + + assert response[:result] + assert_equal "Message: Hello, Metadata: test_value", + response[:result][:content][0][:content] + end + + test "_meta is nested within server_context" do + class ToolWithNestedMeta < Tool + tool_name "tool_with_nested_meta" + description "A tool that uses nested _meta" + input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) + + class << self + def call(message:, server_context:) + user = server_context[:user] + session_id = server_context.dig(:_meta, :session_id) + Tool::Response.new([ + { type: "text", content: "User: #{user}, Session: #{session_id}, Message: #{message}" }, + ]) + end + end + end + + server = Server.new( + name: "test_server", + tools: [ToolWithNestedMeta], + server_context: { user: "test_user", original_field: "value" }, + ) + + request = { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "tool_with_nested_meta", + arguments: { message: "Hello" }, + _meta: { + session_id: "abc123", + }, + }, + } + + response = server.handle(request) + + assert response[:result] + assert_equal "User: test_user, Session: abc123, Message: Hello", + response[:result][:content][0][:content] + end + + test "_meta preserves original server_context" do + class ToolPreservesContext < Tool + tool_name "tool_preserves_context" + description "A tool that checks context preservation" + + class << self + def call(server_context:) + priority = server_context[:priority] + meta_priority = server_context.dig(:_meta, :priority) + Tool::Response.new([ + { type: "text", content: "Context priority: #{priority}, Meta priority: #{meta_priority}" }, + ]) + end + end + end + + server = Server.new( + name: "test_server", + tools: [ToolPreservesContext], + server_context: { priority: "low" }, + ) + + request = { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "tool_preserves_context", + arguments: {}, + _meta: { + priority: "high", + }, + }, + } + + response = server.handle(request) + + assert response[:result] + assert_equal "Context priority: low, Meta priority: high", response[:result][:content][0][:content] + end + + test "prompt receives _meta when provided in request params" do + class PromptWithMeta < Prompt + prompt_name "prompt_with_meta" + description "A prompt that uses _meta" + arguments [Prompt::Argument.new(name: "message", required: true)] + + class << self + def template(args, server_context:) + meta_info = server_context.dig(:_meta, :request_id) + Prompt::Result.new( + messages: [ + Prompt::Message.new( + role: "user", + content: Content::Text.new("Message: #{args[:message]}, Request ID: #{meta_info}"), + ), + ], + ) + end + end + end + + server = Server.new( + name: "test_server", + prompts: [PromptWithMeta], + ) + + request = { + jsonrpc: "2.0", + id: 1, + method: "prompts/get", + params: { + name: "prompt_with_meta", + arguments: { message: "Hello" }, + _meta: { + request_id: "req_12345", + }, + }, + } + + response = server.handle(request) + + assert response[:result] + assert_equal "Message: Hello, Request ID: req_12345", + response[:result][:messages][0][:content][:text] + end + + test "_meta is nested within server_context for prompts" do + class PromptWithNestedContext < Prompt + prompt_name "prompt_with_nested_context" + description "A prompt that uses nested context" + arguments [Prompt::Argument.new(name: "message", required: true)] + + class << self + def template(args, server_context:) + user = server_context[:user] + trace_id = server_context.dig(:_meta, :trace_id) + Prompt::Result.new( + messages: [ + Prompt::Message.new( + role: "user", + content: Content::Text.new("User: #{user}, Trace: #{trace_id}, Message: #{args[:message]}"), + ), + ], + ) + end + end + end + + server = Server.new( + name: "test_server", + prompts: [PromptWithNestedContext], + server_context: { user: "prompt_user" }, + ) + + request = { + jsonrpc: "2.0", + id: 1, + method: "prompts/get", + params: { + name: "prompt_with_nested_context", + arguments: { message: "World" }, + _meta: { + trace_id: "trace_xyz789", + }, + }, + } + + response = server.handle(request) + + assert response[:result] + assert_equal "User: prompt_user, Trace: trace_xyz789, Message: World", + response[:result][:messages][0][:content][:text] + end end end