## Motivation and Context `Server#notify_progress` broadcasts progress notifications to all connected clients. Progress notifications are tied to a specific request's `progressToken` and have no meaningful broadcast use case. Neither the Python SDK nor the TypeScript SDK provides a server-level progress broadcast capability. The MCP specification requires that progress notifications only reference tokens provided in an active request: > Progress notifications MUST only reference tokens that: > - Were provided in an active request > - Are associated with an in-progress operation Ref: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress Broadcasting progress to all clients does not comply with this requirement, since clients that did not originate the request have no matching `progressToken`. The introduction of `ServerSession` for per-connection state (#275) made this removal possible: `Progress` and `ServerContext` now use `notification_target:` (a `ServerSession`) to route notifications to the originating session only. ### Changes - Removed `Server#notify_progress` entirely. The method is no longer needed as `notification_target` in `call_tool_with_args` now passes `session` directly (nil when no session is available, in which case `Progress#report` and `ServerContext#notify_log_message` are no-ops). - Removed `ServerContext#notify_progress` as well. Progress notifications should only be sent via `server_context.report_progress`, which enforces the correct `progressToken` from the originating request. - Added nil guards to `Progress#report` and `ServerContext#notify_log_message` for when no session is available. - Rewrote progress tests to use `ServerSession#handle` instead of `Server#handle`, reflecting that progress notifications are always session-scoped. - Removed 4 tests that called `server.notify_progress` directly as a public broadcast API. - Removed `notify_progress` from the README.md notification methods list. - Removed the "Server-Side: Direct `notify_progress` Usage" section from the README.md. Progress notifications should be sent via `server_context.report_progress` inside tool handlers, which automatically scopes them to the originating client session. ## Breaking Changes Progress notifications are scoped to a specific request via `progressToken` per the MCP specification, so the broadcast behavior of `Server#notify_progress` was a spec violation. This is treated as a bug fix and is made without a deprecation period. This aligns the Ruby SDK with the Python and TypeScript SDKs, neither of which provides a server-level progress broadcast API. `Server#notify_progress` and `ServerContext#notify_progress` are no longer available. Users should use `server_context.report_progress` inside tool handlers instead, which provides session-scoped delivery with the correct `progressToken`. This feature was only recently introduced in mcp 0.9.0: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.9.0 So an early release should help limit its impact on adoption.
744 lines
22 KiB
Ruby
744 lines
22 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
module MCP
|
|
class ServerContextTest < ActiveSupport::TestCase
|
|
test "ServerContext delegates method calls to the underlying context" do
|
|
context = { user: "test_user" }
|
|
progress = Progress.new(notification_target: mock, progress_token: nil)
|
|
|
|
server_context = ServerContext.new(context, progress: progress, notification_target: mock)
|
|
|
|
assert_equal "test_user", server_context[:user]
|
|
end
|
|
|
|
test "ServerContext respond_to? returns true for methods on the underlying context" do
|
|
context = { user: "test_user" }
|
|
progress = Progress.new(notification_target: mock, progress_token: nil)
|
|
|
|
server_context = ServerContext.new(context, progress: progress, notification_target: mock)
|
|
|
|
assert_respond_to server_context, :[]
|
|
assert_respond_to server_context, :keys
|
|
end
|
|
|
|
test "ServerContext respond_to? returns false for methods not on the underlying context" do
|
|
context = { user: "test_user" }
|
|
progress = Progress.new(notification_target: mock, progress_token: nil)
|
|
|
|
server_context = ServerContext.new(context, progress: progress, notification_target: mock)
|
|
|
|
refute_respond_to server_context, :nonexistent_method
|
|
end
|
|
|
|
test "ServerContext raises NoMethodError for methods not on the underlying context" do
|
|
context = { user: "test_user" }
|
|
progress = Progress.new(notification_target: mock, progress_token: nil)
|
|
|
|
server_context = ServerContext.new(context, progress: progress, notification_target: mock)
|
|
|
|
assert_raises(NoMethodError) { server_context.nonexistent_method }
|
|
end
|
|
|
|
test "ServerContext delegates to custom object context" do
|
|
context = Object.new
|
|
def context.custom_method
|
|
"custom_value"
|
|
end
|
|
progress = Progress.new(notification_target: mock, progress_token: nil)
|
|
|
|
server_context = ServerContext.new(context, progress: progress, notification_target: mock)
|
|
|
|
assert_equal "custom_value", server_context.custom_method
|
|
end
|
|
|
|
test "ServerContext#report_progress works with nil context" do
|
|
progress = mock
|
|
progress.expects(:report).with(50, total: 100, message: nil).once
|
|
|
|
server_context = ServerContext.new(nil, progress: progress, notification_target: mock)
|
|
server_context.report_progress(50, total: 100)
|
|
end
|
|
|
|
test "ServerContext#notify_log_message is a no-op when notification_target is nil" do
|
|
progress = Progress.new(notification_target: nil, progress_token: nil)
|
|
server_context = ServerContext.new(nil, progress: progress, notification_target: nil)
|
|
|
|
assert_nothing_raised { server_context.notify_log_message(data: "test", level: "info") }
|
|
end
|
|
|
|
# Tool without server_context parameter
|
|
class SimpleToolWithoutContext < Tool
|
|
tool_name "simple_without_context"
|
|
description "A tool that doesn't use server_context"
|
|
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
|
|
|
|
class << self
|
|
def call(message:)
|
|
Tool::Response.new([
|
|
{ type: "text", content: "SimpleToolWithoutContext: #{message}" },
|
|
])
|
|
end
|
|
end
|
|
end
|
|
|
|
# Tool with optional server_context parameter
|
|
class ToolWithOptionalContext < Tool
|
|
tool_name "tool_with_optional_context"
|
|
description "A tool with optional server_context"
|
|
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
|
|
|
|
class << self
|
|
def call(message:, server_context: nil)
|
|
user = server_context.respond_to?(:[]) ? server_context[:user] : nil
|
|
context_info = user ? "with context: #{user}" : "no context"
|
|
Tool::Response.new([
|
|
{ type: "text", content: "ToolWithOptionalContext: #{message} (#{context_info})" },
|
|
])
|
|
end
|
|
end
|
|
end
|
|
|
|
# Tool with required server_context parameter
|
|
class ToolWithRequiredContext < Tool
|
|
tool_name "tool_with_required_context"
|
|
description "A tool that requires server_context"
|
|
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
|
|
|
|
class << self
|
|
def call(message:, server_context:)
|
|
Tool::Response.new([
|
|
{ type: "text", content: "ToolWithRequiredContext: #{message} for user #{server_context[:user]}" },
|
|
])
|
|
end
|
|
end
|
|
end
|
|
|
|
setup do
|
|
@server_with_context = Server.new(
|
|
name: "test_server",
|
|
tools: [SimpleToolWithoutContext, ToolWithOptionalContext, ToolWithRequiredContext],
|
|
server_context: { user: "test_user" },
|
|
)
|
|
|
|
@server_without_context = Server.new(
|
|
name: "test_server_no_context",
|
|
tools: [SimpleToolWithoutContext, ToolWithOptionalContext],
|
|
)
|
|
end
|
|
|
|
test "tool without server_context parameter works when server has context" do
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "simple_without_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = @server_with_context.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "SimpleToolWithoutContext: Hello", response[:result][:content][0][:content]
|
|
end
|
|
|
|
test "tool with optional server_context receives context when server has it" do
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "tool_with_optional_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = @server_with_context.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "ToolWithOptionalContext: Hello (with context: test_user)",
|
|
response[:result][:content][0][:content]
|
|
end
|
|
|
|
test "tool with optional server_context works when server has no context" do
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "tool_with_optional_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = @server_without_context.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "ToolWithOptionalContext: Hello (no context)",
|
|
response[:result][:content][0][:content]
|
|
end
|
|
|
|
test "tool with required server_context receives context" do
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "tool_with_required_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = @server_with_context.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "ToolWithRequiredContext: Hello for user test_user",
|
|
response[:result][:content][0][:content]
|
|
end
|
|
|
|
test "tool with required server_context fails when server has no context" do
|
|
server_no_context = Server.new(
|
|
name: "test_server_no_context",
|
|
tools: [ToolWithRequiredContext],
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "tool_with_required_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server_no_context.handle(request)
|
|
|
|
assert_nil response[:error], "Expected no JSON-RPC error"
|
|
assert response[:result][:isError]
|
|
assert_equal "text", response[:result][:content][0][:type]
|
|
assert_match(/Internal error calling tool tool_with_required_context: /, response[:result][:content][0][:text])
|
|
end
|
|
|
|
test "call_tool_with_args correctly detects server_context parameter presence" do
|
|
# Tool without server_context
|
|
refute SimpleToolWithoutContext.method(:call).parameters.any? { |_type, name| name == :server_context }
|
|
|
|
# Tool with optional server_context
|
|
assert ToolWithOptionalContext.method(:call).parameters.any? { |_type, name| name == :server_context }
|
|
|
|
# Tool with required server_context
|
|
assert ToolWithRequiredContext.method(:call).parameters.any? { |_type, name| name == :server_context }
|
|
end
|
|
|
|
test "tools can use splat kwargs to accept any arguments including server_context" do
|
|
class FlexibleTool < Tool
|
|
tool_name "flexible_tool"
|
|
|
|
class << self
|
|
def call(**kwargs)
|
|
message = kwargs[:message]
|
|
context = kwargs[:server_context]
|
|
|
|
Tool::Response.new([
|
|
{
|
|
type: "text",
|
|
content: "FlexibleTool: #{message} (context: #{context ? "present" : "absent"})",
|
|
},
|
|
])
|
|
end
|
|
end
|
|
end
|
|
|
|
server = Server.new(
|
|
name: "test_server",
|
|
tools: [FlexibleTool],
|
|
server_context: { user: "test_user" },
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "flexible_tool",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "FlexibleTool: Hello (context: present)",
|
|
response[:result][:content][0][:content]
|
|
end
|
|
|
|
# Prompt tests
|
|
|
|
# Prompt without server_context parameter
|
|
class SimplePromptWithoutContext < Prompt
|
|
prompt_name "simple_prompt_without_context"
|
|
description "A prompt that doesn't use server_context"
|
|
arguments [Prompt::Argument.new(name: "message", required: true)]
|
|
|
|
class << self
|
|
def template(args)
|
|
Prompt::Result.new(
|
|
messages: [
|
|
Prompt::Message.new(
|
|
role: "user",
|
|
content: Content::Text.new("SimplePromptWithoutContext: #{args[:message]}"),
|
|
),
|
|
],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Prompt with optional server_context parameter
|
|
class PromptWithOptionalContext < Prompt
|
|
prompt_name "prompt_with_optional_context"
|
|
description "A prompt with optional server_context"
|
|
arguments [Prompt::Argument.new(name: "message", required: true)]
|
|
|
|
class << self
|
|
def template(args, server_context: nil)
|
|
user = server_context.respond_to?(:[]) ? server_context[:user] : nil
|
|
context_info = user ? "with context: #{user}" : "no context"
|
|
Prompt::Result.new(
|
|
messages: [
|
|
Prompt::Message.new(
|
|
role: "user",
|
|
content: Content::Text.new("PromptWithOptionalContext: #{args[:message]} (#{context_info})"),
|
|
),
|
|
],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Prompt with required server_context parameter
|
|
class PromptWithRequiredContext < Prompt
|
|
prompt_name "prompt_with_required_context"
|
|
description "A prompt that requires server_context"
|
|
arguments [Prompt::Argument.new(name: "message", required: true)]
|
|
|
|
class << self
|
|
def template(args, server_context:)
|
|
Prompt::Result.new(
|
|
messages: [
|
|
Prompt::Message.new(
|
|
role: "user",
|
|
content: Content::Text.new(
|
|
"PromptWithRequiredContext: #{args[:message]} for user #{server_context[:user]}",
|
|
),
|
|
),
|
|
],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
test "prompt without server_context parameter works when server has context" do
|
|
server = Server.new(
|
|
name: "test_server",
|
|
prompts: [SimplePromptWithoutContext],
|
|
server_context: { user: "test_user" },
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "prompts/get",
|
|
params: {
|
|
name: "simple_prompt_without_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "SimplePromptWithoutContext: Hello", response[:result][:messages][0][:content][:text]
|
|
end
|
|
|
|
test "prompt with optional server_context receives context when server has it" do
|
|
server = Server.new(
|
|
name: "test_server",
|
|
prompts: [PromptWithOptionalContext],
|
|
server_context: { user: "test_user" },
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "prompts/get",
|
|
params: {
|
|
name: "prompt_with_optional_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "PromptWithOptionalContext: Hello (with context: test_user)",
|
|
response[:result][:messages][0][:content][:text]
|
|
end
|
|
|
|
test "prompt with optional server_context works when server has no context" do
|
|
server = Server.new(
|
|
name: "test_server",
|
|
prompts: [PromptWithOptionalContext],
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "prompts/get",
|
|
params: {
|
|
name: "prompt_with_optional_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "PromptWithOptionalContext: Hello (no context)",
|
|
response[:result][:messages][0][:content][:text]
|
|
end
|
|
|
|
test "prompt with required server_context receives context" do
|
|
server = Server.new(
|
|
name: "test_server",
|
|
prompts: [PromptWithRequiredContext],
|
|
server_context: { user: "test_user" },
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "prompts/get",
|
|
params: {
|
|
name: "prompt_with_required_context",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "PromptWithRequiredContext: Hello for user test_user",
|
|
response[:result][:messages][0][:content][:text]
|
|
end
|
|
|
|
test "prompts can use splat kwargs to accept any arguments including server_context" do
|
|
class FlexiblePrompt < Prompt
|
|
prompt_name "flexible_prompt"
|
|
arguments [Prompt::Argument.new(name: "message", required: true)]
|
|
|
|
class << self
|
|
def template(args, **kwargs)
|
|
message = args[:message]
|
|
context = kwargs[:server_context]
|
|
|
|
Prompt::Result.new(
|
|
messages: [
|
|
Prompt::Message.new(
|
|
role: "user",
|
|
content: Content::Text.new("FlexiblePrompt: #{message} (context: #{context ? "present" : "absent"})"),
|
|
),
|
|
],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
server = Server.new(
|
|
name: "test_server",
|
|
prompts: [FlexiblePrompt],
|
|
server_context: { user: "test_user" },
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "prompts/get",
|
|
params: {
|
|
name: "flexible_prompt",
|
|
arguments: { message: "Hello" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
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 "non-Hash server_context is preserved when _meta is present" do
|
|
server_obj = Object.new
|
|
def server_obj.custom_method
|
|
"custom_value"
|
|
end
|
|
|
|
tool_class = Class.new(Tool) do
|
|
tool_name "tool_with_non_hash_context"
|
|
|
|
define_singleton_method(:call) do |server_context:, **_args|
|
|
Tool::Response.new([
|
|
{ type: "text", content: "custom: #{server_context.custom_method}" },
|
|
])
|
|
end
|
|
end
|
|
|
|
server = Server.new(
|
|
name: "test_server",
|
|
tools: [tool_class],
|
|
server_context: server_obj,
|
|
)
|
|
|
|
request = {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "tool_with_non_hash_context",
|
|
arguments: {},
|
|
_meta: { progressToken: "token-1" },
|
|
},
|
|
}
|
|
|
|
response = server.handle(request)
|
|
|
|
assert response[:result]
|
|
assert_equal "custom: custom_value", response[:result][:content][0][:content]
|
|
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
|