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.
|
||||
|
||||
#### 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
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ module MCP
|
|||
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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
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 @@ module MCP
|
|||
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
|
||||
|
|
|
|||
|
|
@ -414,5 +414,226 @@ module MCP
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue