Skip to content

Commit 50cd86c

Browse files
committed
Add automatic _meta parameter extraction support
1 parent 4bf1688 commit 50cd86c

File tree

3 files changed

+282
-4
lines changed

3 files changed

+282
-4
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,50 @@ server = MCP::Server.new(
375375

376376
This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
377377

378+
#### Request-specific `_meta` Parameter
379+
380+
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`.
381+
382+
**Access Pattern:**
383+
384+
When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
385+
386+
```ruby
387+
class MyTool < MCP::Tool
388+
def self.call(message:, server_context:)
389+
# Access provider-specific metadata
390+
session_id = server_context.dig(:_meta, :session_id)
391+
request_id = server_context.dig(:_meta, :request_id)
392+
393+
# Access server's original context
394+
user_id = server_context.dig(:user_id)
395+
396+
MCP::Tool::Response.new([{
397+
type: "text",
398+
text: "Processing for user #{user_id} in session #{session_id}"
399+
}])
400+
end
401+
end
402+
```
403+
404+
**Client Request Example:**
405+
406+
```json
407+
{
408+
"jsonrpc": "2.0",
409+
"id": 1,
410+
"method": "tools/call",
411+
"params": {
412+
"name": "my_tool",
413+
"arguments": { "message": "Hello" },
414+
"_meta": {
415+
"session_id": "abc123",
416+
"request_id": "req_456"
417+
}
418+
}
419+
}
420+
```
421+
378422
#### Configuration Block Data
379423

380424
##### Exception Reporter

lib/mcp/server.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def call_tool(request)
419419
end
420420
end
421421

422-
call_tool_with_args(tool, arguments)
422+
call_tool_with_args(tool, arguments, server_context_with_meta(request))
423423
rescue RequestHandlerError
424424
raise
425425
rescue => e
@@ -445,7 +445,7 @@ def get_prompt(request)
445445
prompt_args = request[:arguments]
446446
prompt.validate_arguments!(prompt_args)
447447

448-
call_prompt_template_with_args(prompt, prompt_args)
448+
call_prompt_template_with_args(prompt, prompt_args, server_context_with_meta(request))
449449
end
450450

451451
def list_resources(request)
@@ -488,7 +488,7 @@ def accepts_server_context?(method_object)
488488
parameters.any? { |type, name| type == :keyrest || name == :server_context }
489489
end
490490

491-
def call_tool_with_args(tool, arguments)
491+
def call_tool_with_args(tool, arguments, server_context)
492492
args = arguments&.transform_keys(&:to_sym) || {}
493493

494494
if accepts_server_context?(tool.method(:call))
@@ -498,12 +498,25 @@ def call_tool_with_args(tool, arguments)
498498
end
499499
end
500500

501-
def call_prompt_template_with_args(prompt, args)
501+
def call_prompt_template_with_args(prompt, args, server_context)
502502
if accepts_server_context?(prompt.method(:template))
503503
prompt.template(args, server_context: server_context).to_h
504504
else
505505
prompt.template(args).to_h
506506
end
507507
end
508+
509+
def server_context_with_meta(request)
510+
meta = request[:_meta]
511+
if @server_context && meta
512+
context = @server_context.dup
513+
context[:_meta] = meta
514+
context
515+
elsif meta
516+
{ _meta: meta }
517+
elsif @server_context
518+
@server_context
519+
end
520+
end
508521
end
509522
end

test/mcp/server_context_test.rb

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,5 +414,226 @@ def template(args, **kwargs)
414414
assert_equal "FlexiblePrompt: Hello (context: present)",
415415
response[:result][:messages][0][:content][:text]
416416
end
417+
418+
test "tool receives _meta when provided in request params" do
419+
class ToolWithMeta < Tool
420+
tool_name "tool_with_meta"
421+
description "A tool that uses _meta"
422+
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
423+
424+
class << self
425+
def call(message:, server_context:)
426+
meta_info = server_context.dig(:_meta, :provider, :metadata)
427+
Tool::Response.new([
428+
{ type: "text", content: "Message: #{message}, Metadata: #{meta_info}" },
429+
])
430+
end
431+
end
432+
end
433+
434+
server = Server.new(
435+
name: "test_server",
436+
tools: [ToolWithMeta],
437+
)
438+
439+
request = {
440+
jsonrpc: "2.0",
441+
id: 1,
442+
method: "tools/call",
443+
params: {
444+
name: "tool_with_meta",
445+
arguments: { message: "Hello" },
446+
_meta: {
447+
provider: {
448+
metadata: "test_value",
449+
},
450+
},
451+
},
452+
}
453+
454+
response = server.handle(request)
455+
456+
assert response[:result]
457+
assert_equal "Message: Hello, Metadata: test_value",
458+
response[:result][:content][0][:content]
459+
end
460+
461+
test "_meta is nested within server_context" do
462+
class ToolWithNestedMeta < Tool
463+
tool_name "tool_with_nested_meta"
464+
description "A tool that uses nested _meta"
465+
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
466+
467+
class << self
468+
def call(message:, server_context:)
469+
user = server_context[:user]
470+
session_id = server_context.dig(:_meta, :session_id)
471+
Tool::Response.new([
472+
{ type: "text", content: "User: #{user}, Session: #{session_id}, Message: #{message}" },
473+
])
474+
end
475+
end
476+
end
477+
478+
server = Server.new(
479+
name: "test_server",
480+
tools: [ToolWithNestedMeta],
481+
server_context: { user: "test_user", original_field: "value" },
482+
)
483+
484+
request = {
485+
jsonrpc: "2.0",
486+
id: 1,
487+
method: "tools/call",
488+
params: {
489+
name: "tool_with_nested_meta",
490+
arguments: { message: "Hello" },
491+
_meta: {
492+
session_id: "abc123",
493+
},
494+
},
495+
}
496+
497+
response = server.handle(request)
498+
499+
assert response[:result]
500+
assert_equal "User: test_user, Session: abc123, Message: Hello",
501+
response[:result][:content][0][:content]
502+
end
503+
504+
test "_meta preserves original server_context" do
505+
class ToolPreservesContext < Tool
506+
tool_name "tool_preserves_context"
507+
description "A tool that checks context preservation"
508+
509+
class << self
510+
def call(server_context:)
511+
priority = server_context[:priority]
512+
meta_priority = server_context.dig(:_meta, :priority)
513+
Tool::Response.new([
514+
{ type: "text", content: "Context priority: #{priority}, Meta priority: #{meta_priority}" },
515+
])
516+
end
517+
end
518+
end
519+
520+
server = Server.new(
521+
name: "test_server",
522+
tools: [ToolPreservesContext],
523+
server_context: { priority: "low" },
524+
)
525+
526+
request = {
527+
jsonrpc: "2.0",
528+
id: 1,
529+
method: "tools/call",
530+
params: {
531+
name: "tool_preserves_context",
532+
arguments: {},
533+
_meta: {
534+
priority: "high",
535+
},
536+
},
537+
}
538+
539+
response = server.handle(request)
540+
541+
assert response[:result]
542+
assert_equal "Context priority: low, Meta priority: high", response[:result][:content][0][:content]
543+
end
544+
545+
test "prompt receives _meta when provided in request params" do
546+
class PromptWithMeta < Prompt
547+
prompt_name "prompt_with_meta"
548+
description "A prompt that uses _meta"
549+
arguments [Prompt::Argument.new(name: "message", required: true)]
550+
551+
class << self
552+
def template(args, server_context:)
553+
meta_info = server_context.dig(:_meta, :request_id)
554+
Prompt::Result.new(
555+
messages: [
556+
Prompt::Message.new(
557+
role: "user",
558+
content: Content::Text.new("Message: #{args[:message]}, Request ID: #{meta_info}"),
559+
),
560+
],
561+
)
562+
end
563+
end
564+
end
565+
566+
server = Server.new(
567+
name: "test_server",
568+
prompts: [PromptWithMeta],
569+
)
570+
571+
request = {
572+
jsonrpc: "2.0",
573+
id: 1,
574+
method: "prompts/get",
575+
params: {
576+
name: "prompt_with_meta",
577+
arguments: { message: "Hello" },
578+
_meta: {
579+
request_id: "req_12345",
580+
},
581+
},
582+
}
583+
584+
response = server.handle(request)
585+
586+
assert response[:result]
587+
assert_equal "Message: Hello, Request ID: req_12345",
588+
response[:result][:messages][0][:content][:text]
589+
end
590+
591+
test "_meta is nested within server_context for prompts" do
592+
class PromptWithNestedContext < Prompt
593+
prompt_name "prompt_with_nested_context"
594+
description "A prompt that uses nested context"
595+
arguments [Prompt::Argument.new(name: "message", required: true)]
596+
597+
class << self
598+
def template(args, server_context:)
599+
user = server_context[:user]
600+
trace_id = server_context.dig(:_meta, :trace_id)
601+
Prompt::Result.new(
602+
messages: [
603+
Prompt::Message.new(
604+
role: "user",
605+
content: Content::Text.new("User: #{user}, Trace: #{trace_id}, Message: #{args[:message]}"),
606+
),
607+
],
608+
)
609+
end
610+
end
611+
end
612+
613+
server = Server.new(
614+
name: "test_server",
615+
prompts: [PromptWithNestedContext],
616+
server_context: { user: "prompt_user" },
617+
)
618+
619+
request = {
620+
jsonrpc: "2.0",
621+
id: 1,
622+
method: "prompts/get",
623+
params: {
624+
name: "prompt_with_nested_context",
625+
arguments: { message: "World" },
626+
_meta: {
627+
trace_id: "trace_xyz789",
628+
},
629+
},
630+
}
631+
632+
response = server.handle(request)
633+
634+
assert response[:result]
635+
assert_equal "User: prompt_user, Trace: trace_xyz789, Message: World",
636+
response[:result][:messages][0][:content][:text]
637+
end
417638
end
418639
end

0 commit comments

Comments
 (0)