Add automatic _meta parameter extraction support
This commit is contained in:
parent
4bf168840e
commit
50cd86c01c
3 changed files with 282 additions and 4 deletions
44
README.md
44
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.
|
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
|
#### Configuration Block Data
|
||||||
|
|
||||||
##### Exception Reporter
|
##### Exception Reporter
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,7 @@ module MCP
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
call_tool_with_args(tool, arguments)
|
call_tool_with_args(tool, arguments, server_context_with_meta(request))
|
||||||
rescue RequestHandlerError
|
rescue RequestHandlerError
|
||||||
raise
|
raise
|
||||||
rescue => e
|
rescue => e
|
||||||
|
|
@ -445,7 +445,7 @@ module MCP
|
||||||
prompt_args = request[:arguments]
|
prompt_args = request[:arguments]
|
||||||
prompt.validate_arguments!(prompt_args)
|
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
|
end
|
||||||
|
|
||||||
def list_resources(request)
|
def list_resources(request)
|
||||||
|
|
@ -488,7 +488,7 @@ module MCP
|
||||||
parameters.any? { |type, name| type == :keyrest || name == :server_context }
|
parameters.any? { |type, name| type == :keyrest || name == :server_context }
|
||||||
end
|
end
|
||||||
|
|
||||||
def call_tool_with_args(tool, arguments)
|
def call_tool_with_args(tool, arguments, server_context)
|
||||||
args = arguments&.transform_keys(&:to_sym) || {}
|
args = arguments&.transform_keys(&:to_sym) || {}
|
||||||
|
|
||||||
if accepts_server_context?(tool.method(:call))
|
if accepts_server_context?(tool.method(:call))
|
||||||
|
|
@ -498,12 +498,25 @@ module MCP
|
||||||
end
|
end
|
||||||
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))
|
if accepts_server_context?(prompt.method(:template))
|
||||||
prompt.template(args, server_context: server_context).to_h
|
prompt.template(args, server_context: server_context).to_h
|
||||||
else
|
else
|
||||||
prompt.template(args).to_h
|
prompt.template(args).to_h
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -414,5 +414,226 @@ module MCP
|
||||||
assert_equal "FlexiblePrompt: Hello (context: present)",
|
assert_equal "FlexiblePrompt: Hello (context: present)",
|
||||||
response[:result][:messages][0][:content][:text]
|
response[:result][:messages][0][:content][:text]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue