Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,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: nil)
# 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
Expand Down
32 changes: 26 additions & 6 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,16 @@ def call_tool(request)
end
end

# Include _meta from request params as nested field in server_context
meta = request[:_meta]
context_with_meta = if @server_context || meta
context = @server_context ? @server_context.dup : {}
context[:_meta] = meta if meta
context
end

begin
call_tool_with_args(tool, arguments)
call_tool_with_args(tool, arguments, context_with_meta)
rescue => e
report_exception(e, { request: request })
Tool::Response.new(
Expand Down Expand Up @@ -332,7 +340,15 @@ def get_prompt(request)
prompt_args = request[:arguments]
prompt.validate_arguments!(prompt_args)

call_prompt_template_with_args(prompt, prompt_args)
# Include _meta from request params as nested field in server_context
meta = request[:_meta]
context_with_meta = if @server_context || meta
context = @server_context ? @server_context.dup : {}
context[:_meta] = meta if meta
context
end

call_prompt_template_with_args(prompt, prompt_args, context_with_meta)
end

def list_resources(request)
Expand Down Expand Up @@ -365,19 +381,23 @@ 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, context = nil)
args = arguments&.transform_keys(&:to_sym) || {}
effective_context = context || server_context

if accepts_server_context?(tool.method(:call))
tool.call(**args, server_context: server_context).to_h
tool.call(**args, server_context: effective_context).to_h
else
tool.call(**args).to_h
end
end

def call_prompt_template_with_args(prompt, args)

def call_prompt_template_with_args(prompt, args, context = nil)
effective_context = context || server_context

if accepts_server_context?(prompt.method(:template))
prompt.template(args, server_context: server_context).to_h
prompt.template(args, server_context: effective_context).to_h
else
prompt.template(args).to_h
end
Expand Down
224 changes: 224 additions & 0 deletions test/mcp/server_context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -414,5 +414,229 @@ def template(args, **kwargs)
assert_equal "FlexiblePrompt: Hello (context: present)",
response[:result][:messages][0][:content][:text]
end

# _meta extraction tests

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: nil)
meta_info = server_context&.dig(:_meta, :provider, :metadata) || "no 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: nil)
user = server_context&.dig(:user) || "unknown"
session_id = server_context&.dig(:_meta, :session_id) || "unknown"
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: nil)
priority = server_context&.dig(:priority) || "none"
meta_priority = server_context&.dig(:_meta, :priority) || "none"
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: nil)
meta_info = server_context&.dig(:_meta, :request_id) || "no 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: nil)
user = server_context&.dig(:user) || "unknown"
trace_id = server_context&.dig(:_meta, :trace_id) || "unknown"
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