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
60 changes: 55 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ class McpController < ActionController::API
)
# Since the `MCP-Session-Id` is not shared across requests, `stateless: true` is set.
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
server.transport = transport
status, headers, body = transport.handle_request(request)

render(json: body.first, status: status, headers: headers)
Expand Down Expand Up @@ -821,7 +820,61 @@ This enables servers to leverage the client's LLM capabilities without needing d
- **Tool Support**: When using tools in sampling requests, clients must declare `sampling.tools` capability
- **Human-in-the-Loop**: Clients can implement user approval before forwarding requests to LLMs

**Using Sampling in Tools:**
**Usage Example (Stdio transport):**

`Server#create_sampling_message` is for single-client transports (e.g., `StdioTransport`).
For multi-client transports (e.g., `StreamableHTTPTransport`), use `server_context.create_sampling_message` inside tools instead,
which routes the request to the correct client session.

```ruby
server = MCP::Server.new(name: "my_server")
transport = MCP::Server::Transports::StdioTransport.new(server)
```

Client must declare sampling capability during initialization.
This happens automatically when the client connects.

```ruby
result = server.create_sampling_message(
messages: [
{ role: "user", content: { type: "text", text: "What is the capital of France?" } }
],
max_tokens: 100,
system_prompt: "You are a helpful assistant.",
temperature: 0.7
)
```

Result contains the LLM response:

```ruby
{
role: "assistant",
content: { type: "text", text: "The capital of France is Paris." },
model: "claude-3-sonnet-20240307",
stopReason: "endTurn"
}
```

**Parameters:**

Required:

- `messages:` (Array) - Array of message objects with `role` and `content`
- `max_tokens:` (Integer) - Maximum tokens in the response

Optional:

- `system_prompt:` (String) - System prompt for the LLM
- `model_preferences:` (Hash) - Model selection preferences (e.g., `{ intelligencePriority: 0.8 }`)
- `include_context:` (String) - Context inclusion: `"none"`, `"thisServer"`, or `"allServers"` (soft-deprecated)
- `temperature:` (Float) - Sampling temperature
- `stop_sequences:` (Array) - Sequences that stop generation
- `metadata:` (Hash) - Additional metadata
- `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
- `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)

**Using Sampling in Tools (works with both Stdio and HTTP transports):**

Tools that accept a `server_context:` parameter can call `create_sampling_message` on it.
The request is automatically routed to the correct client session.
Expand Down Expand Up @@ -1120,7 +1173,6 @@ For more details, see the [MCP Logging specification](https://modelcontextprotoc
```ruby
server = MCP::Server.new(name: "my_server")
transport = MCP::Server::Transports::StdioTransport.new(server)
server.transport = transport

# The client first configures the logging level (on the client side):
transport.send_request(
Expand Down Expand Up @@ -1174,8 +1226,6 @@ server = MCP::Server.new(name: "my_server")
# Default Streamable HTTP - session oriented
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)

server.transport = transport

# When tools change, notify clients
server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
server.notify_tools_list_changed
Expand Down
8 changes: 1 addition & 7 deletions conformance/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def initialize(port: DEFAULT_PORT)

def start
server = build_server
transport = build_transport(server)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
configure_handlers(server)
rack_app = build_rack_app(transport)

Expand Down Expand Up @@ -480,12 +480,6 @@ def resource_templates
]
end

def build_transport(server)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport
transport
end

def configure_handlers(server)
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "debug")
server.server_context = server
Expand Down
1 change: 0 additions & 1 deletion examples/http_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def template(args, server_context:)

# Create the Streamable HTTP transport
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport

# Create a logger for MCP-specific logging
mcp_logger = Logger.new($stdout)
Expand Down
1 change: 0 additions & 1 deletion examples/streamable_http_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ def call(message:, delay: 0)

# Create the Streamable HTTP transport
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport

# Create a logger for MCP request/response logging
mcp_logger = Logger.new($stdout)
Expand Down
1 change: 1 addition & 0 deletions lib/mcp/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Transport
# Initialize the transport with the server instance
def initialize(server)
@server = server
server.transport = self
end

# Send a response to the client
Expand Down
1 change: 0 additions & 1 deletion test/mcp/progress_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def handle_request(request); end
setup do
@server = Server.new(name: "test_server")
@transport = MockTransport.new(@server)
@server.transport = @transport
@session = ServerSession.new(server: @server, transport: @transport)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def closed?
resources: [],
)
@transport = StdioTransport.new(@server)
@server.transport = @transport
end

teardown do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class StreamableHTTPNotificationIntegrationTest < ActiveSupport::TestCase
resources: [],
)
@transport = StreamableHTTPTransport.new(@server)
@server.transport = @transport
end

test "server notification methods send SSE notifications through HTTP transport" do
Expand Down
4 changes: 0 additions & 4 deletions test/mcp/server/transports/streamable_http_transport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2591,7 +2591,6 @@ def string
server = Server.new(name: "test", tools: [], prompts: [], resources: [])
server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "debug")
transport = StreamableHTTPTransport.new(server)
server.transport = transport

server.define_tool(name: "log_tool") do |server_context:|
server_context.notify_log_message(data: "secret", level: "info")
Expand Down Expand Up @@ -2668,7 +2667,6 @@ def string
test "session-scoped progress notification is sent only to the originating session" do
server = Server.new(name: "test", tools: [], prompts: [], resources: [])
transport = StreamableHTTPTransport.new(server)
server.transport = transport

server.define_tool(name: "progress_tool") do |server_context:|
server_context.report_progress(50, total: 100, message: "halfway")
Expand Down Expand Up @@ -2749,7 +2747,6 @@ def string
test "each session stores its own client info independently" do
server = Server.new(name: "test", tools: [], prompts: [], resources: [])
transport = StreamableHTTPTransport.new(server)
server.transport = transport

# Initialize session 1 with client "alpha".
init1 = create_rack_request(
Expand Down Expand Up @@ -2794,7 +2791,6 @@ def string
test "each session stores its own logging level independently" do
server = Server.new(name: "test", tools: [], prompts: [], resources: [])
transport = StreamableHTTPTransport.new(server)
server.transport = transport

# Initialize two sessions.
init1 = create_rack_request(
Expand Down
6 changes: 2 additions & 4 deletions test/mcp/server_notification_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ def handle_request(request); end
)

@mock_transport = MockTransport.new(@server)
@server.transport = @mock_transport
end

test "#notify_tools_list_changed sends notification through transport" do
Expand Down Expand Up @@ -122,14 +121,13 @@ def handle_request(request); end
end

test "notification methods handle transport errors gracefully" do
# Create a transport that raises errors
error_transport = Class.new(MockTransport) do
# Replace server's transport with one that raises on send_notification.
Class.new(MockTransport) do
def send_notification(method, params = nil)
raise StandardError, "Transport error"
end
end.new(@server)

@server.transport = error_transport
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")

# Mock the exception reporter
Expand Down
1 change: 0 additions & 1 deletion test/mcp/server_progress_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def call(**kwargs)
)

@mock_transport = MockTransport.new(@server)
@server.transport = @mock_transport
@session = ServerSession.new(server: @server, transport: @mock_transport)
end

Expand Down
14 changes: 3 additions & 11 deletions test/mcp/server_sampling_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def close; end
)

@mock_transport = MockTransport.new(@server)
@server.transport = @mock_transport

# Simulate client initialization with sampling capability.
@server.handle({
Expand Down Expand Up @@ -197,8 +196,7 @@ def close; end

test "init with sampling capability allows create_sampling_message" do
server = Server.new(name: "test", version: "1.0")
mock_transport = MockTransport.new(server)
server.transport = mock_transport
MockTransport.new(server)

server.handle({
jsonrpc: "2.0",
Expand All @@ -222,8 +220,7 @@ def close; end

test "init without capabilities rejects create_sampling_message" do
server = Server.new(name: "test", version: "1.0")
mock_transport = MockTransport.new(server)
server.transport = mock_transport
MockTransport.new(server)

server.handle({
jsonrpc: "2.0",
Expand All @@ -247,7 +244,6 @@ def close; end

test "create_sampling_message uses per-session capabilities via ServerSession" do
transport = MCP::Server::Transports::StreamableHTTPTransport.new(@server)
@server.transport = transport

# Session with sampling capability passes validation (fails at send_request due to no stream).
session_with_sampling = ServerSession.new(server: @server, transport: transport, session_id: "s1")
Expand Down Expand Up @@ -277,7 +273,6 @@ def close; end

test "ServerSession#client_capabilities falls back to server global capabilities" do
transport = MCP::Server::Transports::StreamableHTTPTransport.new(@server)
@server.transport = transport

# Session without capabilities stored falls back to @server.client_capabilities.
session = ServerSession.new(server: @server, transport: transport, session_id: "s3")
Expand All @@ -295,8 +290,7 @@ def close; end

test "session init does not overwrite server global client_capabilities" do
server = Server.new(name: "test", version: "1.0")
mock_transport = MockTransport.new(server)
server.transport = mock_transport
MockTransport.new(server)

# Non-session init sets global capabilities.
server.handle({
Expand All @@ -314,7 +308,6 @@ def close; end

# Session-scoped init must NOT overwrite global capabilities.
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport
session = ServerSession.new(server: server, transport: transport, session_id: "s1")

server.handle(
Expand All @@ -340,7 +333,6 @@ def close; end
test "Server#create_sampling_message does not see session-scoped capabilities from HTTP init" do
server = Server.new(name: "test", version: "1.0")
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
server.transport = transport

# HTTP init stores capabilities on the session, not on the server.
session = ServerSession.new(server: server, transport: transport, session_id: "s1")
Expand Down