Skip to content

Commit 16f1d64

Browse files
Ginjatopherbullock
andcommitted
Implement StreamableHTTP transport
Co-authored-by: Topher Bullock <topher.bullock@shopify.com>
1 parent 0b49c3f commit 16f1d64

File tree

15 files changed

+1809
-15
lines changed

15 files changed

+1809
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ It implements the Model Context Protocol specification, handling model context r
3333
- Manages tool registration and invocation
3434
- Supports prompt registration and execution
3535
- Supports resource registration and retrieval
36+
- Supports stdio & StreamableHTTP (including SSE) transports
3637

3738
### Supported Methods
3839
- `initialize` - Initializes the protocol and returns server capabilities

examples/README.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# MCP Ruby Examples
2+
3+
This directory contains examples of how to use the Model Context Protocol (MCP) Ruby library.
4+
5+
## Available Examples
6+
7+
### 1. STDIO Server (`stdio_server.rb`)
8+
A simple server that communicates over standard input/output. This is useful for desktop applications and command-line tools.
9+
10+
**Usage:**
11+
```bash
12+
ruby examples/stdio_server.rb
13+
{"jsonrpc":"2.0","id":0,"method":"tools/list"}
14+
```
15+
16+
### 2. HTTP Server (`http_server.rb`)
17+
A standalone HTTP server built with Rack that implements the MCP StreamableHTTP transport protocol. This demonstrates how to create a web-based MCP server with session management and Server-Sent Events (SSE) support.
18+
19+
**Features:**
20+
- HTTP transport with Server-Sent Events (SSE) for streaming
21+
- Session management with unique session IDs
22+
- Example tools, prompts, and resources
23+
- JSON-RPC 2.0 protocol implementation
24+
- Full MCP protocol compliance
25+
26+
**Usage:**
27+
```bash
28+
ruby examples/http_server.rb
29+
```
30+
31+
The server will start on `http://localhost:9292` and provide:
32+
- **Tools**:
33+
- `ExampleTool` - adds two numbers
34+
- `echo` - echoes back messages
35+
- **Prompts**: `ExamplePrompt` - echoes back arguments as a prompt
36+
- **Resources**: `test_resource` - returns example content
37+
38+
### 3. HTTP Client Example (`http_client.rb`)
39+
A client that demonstrates how to interact with the HTTP server using all MCP protocol methods.
40+
41+
**Usage:**
42+
1. Start the HTTP server in one terminal:
43+
```bash
44+
ruby examples/http_server.rb
45+
```
46+
47+
2. Run the client example in another terminal:
48+
```bash
49+
ruby examples/http_client.rb
50+
```
51+
52+
The client will demonstrate:
53+
- Session initialization
54+
- Ping requests
55+
- Listing and calling tools
56+
- Listing and getting prompts
57+
- Listing and reading resources
58+
- Session cleanup
59+
60+
### 4. SSE Test Server (`sse_test_server.rb`)
61+
A specialized HTTP server designed to test and demonstrate Server-Sent Events (SSE) functionality in the MCP protocol.
62+
63+
**Features:**
64+
- Tools specifically designed to trigger SSE notifications
65+
- Real-time progress updates and notifications
66+
- Detailed SSE-specific logging
67+
68+
**Available Tools:**
69+
- `NotificationTool` - Send custom SSE notifications with optional delays
70+
- `echo` - Simple echo tool for basic testing
71+
72+
**Usage:**
73+
```bash
74+
ruby examples/sse_test_server.rb
75+
```
76+
77+
The server will start on `http://localhost:9393` and provide detailed instructions for testing SSE functionality.
78+
79+
### 5. SSE Test Client (`sse_test_client.rb`)
80+
An interactive client that connects to the SSE stream and provides a menu-driven interface for testing SSE functionality.
81+
82+
**Features:**
83+
- Automatic SSE stream connection
84+
- Interactive menu for triggering various SSE events
85+
- Real-time display of received SSE notifications
86+
- Session management
87+
88+
**Usage:**
89+
1. Start the SSE test server in one terminal:
90+
```bash
91+
ruby examples/sse_test_server.rb
92+
```
93+
94+
2. Run the SSE test client in another terminal:
95+
```bash
96+
ruby examples/sse_test_client.rb
97+
```
98+
99+
The client will:
100+
- Initialize a session automatically
101+
- Connect to the SSE stream
102+
- Provide an interactive menu to trigger notifications
103+
- Display all received SSE events in real-time
104+
105+
### Testing SSE with cURL
106+
107+
You can also test SSE functionality manually using cURL:
108+
109+
1. Initialize a session:
110+
```bash
111+
SESSION_ID=$(curl -D - -s -o /dev/null -X POST http://localhost:9393 \
112+
-H "Content-Type: application/json" \
113+
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}' |grep -i "Mcp-Session-Id:" | cut -d' ' -f2- | tr -d '\r')
114+
```
115+
116+
2. Connect to SSE stream (in one terminal):
117+
```bash
118+
curl -N -H "Mcp-Session-Id: $SESSION_ID" http://localhost:9393
119+
```
120+
121+
3. Trigger notifications (in another terminal):
122+
```bash
123+
# Send immediate notification
124+
curl -X POST http://localhost:9393 \
125+
-H "Content-Type: application/json" \
126+
-H "Mcp-Session-Id: $SESSION_ID" \
127+
-d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"notification_tool","arguments":{"message":"Hello from cURL!"}}}'
128+
```
129+
130+
## StreamableHTTP Transport Details
131+
132+
### Protocol Flow
133+
134+
The HTTP server implements the MCP StreamableHTTP transport protocol:
135+
136+
1. **Initialize Session**:
137+
- Client sends POST request with `initialize` method
138+
- Server responds with session ID in `Mcp-Session-Id` header
139+
140+
2. **Establish SSE Connection** (optional):
141+
- Client sends GET request with `Mcp-Session-Id` header
142+
- Server establishes Server-Sent Events stream for notifications
143+
144+
3. **Send Requests**:
145+
- Client sends POST requests with JSON-RPC 2.0 format
146+
- Server processes and responds with results
147+
148+
4. **Close Session**:
149+
- Client sends DELETE request with `Mcp-Session-Id` header
150+
151+
### Example cURL Commands
152+
153+
Initialize a session:
154+
```bash
155+
curl -X POST http://localhost:9292 \
156+
-H "Content-Type: application/json" \
157+
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
158+
```
159+
160+
List tools (using the session ID from initialization):
161+
```bash
162+
curl -X POST http://localhost:9292 \
163+
-H "Content-Type: application/json" \
164+
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
165+
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
166+
```
167+
168+
Call a tool:
169+
```bash
170+
curl -X POST http://localhost:9292 \
171+
-H "Content-Type: application/json" \
172+
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
173+
-d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"ExampleTool","arguments":{"a":5,"b":3}}}'
174+
```

examples/http_client.rb

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "net/http"
5+
require "json"
6+
require "uri"
7+
8+
# Simple HTTP client example for interacting with the MCP HTTP server
9+
class MCPHTTPClient
10+
def initialize(base_url = "http://localhost:9292")
11+
@base_url = base_url
12+
@session_id = nil
13+
end
14+
15+
def send_request(method, params = nil, id = nil)
16+
uri = URI(@base_url)
17+
http = Net::HTTP.new(uri.host, uri.port)
18+
19+
request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path)
20+
request["Content-Type"] = "application/json"
21+
request["Mcp-Session-Id"] = @session_id if @session_id
22+
23+
body = {
24+
jsonrpc: "2.0",
25+
method: method,
26+
params: params,
27+
id: id || rand(10000),
28+
}.compact
29+
30+
request.body = body.to_json
31+
32+
response = http.request(request)
33+
34+
# Store session ID if provided
35+
if response["Mcp-Session-Id"]
36+
@session_id = response["Mcp-Session-Id"]
37+
puts "Session ID: #{@session_id}"
38+
end
39+
40+
JSON.parse(response.body)
41+
end
42+
43+
def initialize_session
44+
puts "=== Initializing session ==="
45+
result = send_request("initialize", {
46+
protocolVersion: "2024-11-05",
47+
capabilities: {},
48+
clientInfo: {
49+
name: "example_client",
50+
version: "1.0",
51+
},
52+
})
53+
puts "Response: #{JSON.pretty_generate(result)}"
54+
puts
55+
result
56+
end
57+
58+
def ping
59+
puts "=== Sending ping ==="
60+
result = send_request("ping")
61+
puts "Response: #{JSON.pretty_generate(result)}"
62+
puts
63+
result
64+
end
65+
66+
def list_tools
67+
puts "=== Listing tools ==="
68+
result = send_request("tools/list")
69+
puts "Response: #{JSON.pretty_generate(result)}"
70+
puts
71+
result
72+
end
73+
74+
def call_tool(name, arguments)
75+
puts "=== Calling tool: #{name} ==="
76+
result = send_request("tools/call", {
77+
name: name,
78+
arguments: arguments,
79+
})
80+
puts "Response: #{JSON.pretty_generate(result)}"
81+
puts
82+
result
83+
end
84+
85+
def list_prompts
86+
puts "=== Listing prompts ==="
87+
result = send_request("prompts/list")
88+
puts "Response: #{JSON.pretty_generate(result)}"
89+
puts
90+
result
91+
end
92+
93+
def get_prompt(name, arguments)
94+
puts "=== Getting prompt: #{name} ==="
95+
result = send_request("prompts/get", {
96+
name: name,
97+
arguments: arguments,
98+
})
99+
puts "Response: #{JSON.pretty_generate(result)}"
100+
puts
101+
result
102+
end
103+
104+
def list_resources
105+
puts "=== Listing resources ==="
106+
result = send_request("resources/list")
107+
puts "Response: #{JSON.pretty_generate(result)}"
108+
puts
109+
result
110+
end
111+
112+
def read_resource(uri)
113+
puts "=== Reading resource: #{uri} ==="
114+
result = send_request("resources/read", {
115+
uri: uri,
116+
})
117+
puts "Response: #{JSON.pretty_generate(result)}"
118+
puts
119+
result
120+
end
121+
122+
def close_session
123+
return unless @session_id
124+
125+
puts "=== Closing session ==="
126+
uri = URI(@base_url)
127+
http = Net::HTTP.new(uri.host, uri.port)
128+
129+
request = Net::HTTP::Delete.new(uri.path.empty? ? "/" : uri.path)
130+
request["Mcp-Session-Id"] = @session_id
131+
132+
response = http.request(request)
133+
result = JSON.parse(response.body)
134+
puts "Response: #{JSON.pretty_generate(result)}"
135+
puts
136+
137+
@session_id = nil
138+
result
139+
end
140+
end
141+
142+
# Main script
143+
if __FILE__ == $PROGRAM_NAME
144+
puts "MCP HTTP Client Example"
145+
puts "Make sure the HTTP server is running (ruby examples/http_server.rb)"
146+
puts "=" * 50
147+
puts
148+
149+
client = MCPHTTPClient.new
150+
151+
begin
152+
# Initialize session
153+
client.initialize_session
154+
155+
# Test ping
156+
client.ping
157+
158+
# List available tools
159+
client.list_tools
160+
161+
# Call the example_tool (note: snake_case name)
162+
client.call_tool("example_tool", { a: 5, b: 3 })
163+
164+
# Call the echo tool
165+
client.call_tool("echo", { message: "Hello from client!" })
166+
167+
# List prompts
168+
client.list_prompts
169+
170+
# Get a prompt (note: snake_case name)
171+
client.get_prompt("example_prompt", { message: "This is a test message" })
172+
173+
# List resources
174+
client.list_resources
175+
176+
# Read a resource
177+
client.read_resource("test_resource")
178+
rescue => e
179+
puts "Error: #{e.message}"
180+
puts e.backtrace
181+
ensure
182+
# Clean up session
183+
client.close_session
184+
end
185+
end

0 commit comments

Comments
 (0)