mcp 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
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
+
9
+ A simple server that communicates over standard input/output. This is useful for desktop applications and command-line tools.
10
+
11
+ **Usage:**
12
+
13
+ ```console
14
+ $ ruby examples/stdio_server.rb
15
+ {"jsonrpc":"2.0","id":0,"method":"tools/list"}
16
+ ```
17
+
18
+ ### 2. HTTP Server (`http_server.rb`)
19
+
20
+ A standalone HTTP server built with Rack that implements the MCP Streamable HTTP transport protocol. This demonstrates how to create a web-based MCP server with session management and Server-Sent Events (SSE) support.
21
+
22
+ **Features:**
23
+
24
+ - HTTP transport with Server-Sent Events (SSE) for streaming
25
+ - Session management with unique session IDs
26
+ - Example tools, prompts, and resources
27
+ - JSON-RPC 2.0 protocol implementation
28
+ - Full MCP protocol compliance
29
+
30
+ **Usage:**
31
+
32
+ ```console
33
+ $ ruby examples/http_server.rb
34
+ ```
35
+
36
+ The server will start on `http://localhost:9292` and provide:
37
+
38
+ - **Tools**:
39
+ - `ExampleTool` - adds two numbers
40
+ - `echo` - echoes back messages
41
+ - **Prompts**: `ExamplePrompt` - echoes back arguments as a prompt
42
+ - **Resources**: `test_resource` - returns example content
43
+
44
+ ### 3. HTTP Client Example (`http_client.rb`)
45
+
46
+ A client that demonstrates how to interact with the HTTP server using all MCP protocol methods.
47
+
48
+ **Usage:**
49
+
50
+ 1. Start the HTTP server in one terminal:
51
+
52
+ ```console
53
+ $ ruby examples/http_server.rb
54
+ ```
55
+
56
+ 2. Run the client example in another terminal:
57
+ ```console
58
+ $ ruby examples/http_client.rb
59
+ ```
60
+
61
+ The client will demonstrate:
62
+
63
+ - Session initialization
64
+ - Ping requests
65
+ - Listing and calling tools
66
+ - Listing and getting prompts
67
+ - Listing and reading resources
68
+ - Session cleanup
69
+
70
+ ### 4. Streamable HTTP Server (`streamable_http_server.rb`)
71
+
72
+ A specialized HTTP server designed to test and demonstrate Server-Sent Events (SSE) functionality in the MCP protocol.
73
+
74
+ **Features:**
75
+
76
+ - Tools specifically designed to trigger SSE notifications
77
+ - Real-time progress updates and notifications
78
+ - Detailed SSE-specific logging
79
+
80
+ **Available Tools:**
81
+
82
+ - `NotificationTool` - Send custom SSE notifications with optional delays
83
+ - `echo` - Simple echo tool for basic testing
84
+
85
+ **Usage:**
86
+
87
+ ```console
88
+ $ ruby examples/streamable_http_server.rb
89
+ ```
90
+
91
+ The server will start on `http://localhost:9393` and provide detailed instructions for testing SSE functionality.
92
+
93
+ ### 5. Streamable HTTP Client (`streamable_http_client.rb`)
94
+
95
+ An interactive client that connects to the SSE stream and provides a menu-driven interface for testing SSE functionality.
96
+
97
+ **Features:**
98
+
99
+ - Automatic SSE stream connection
100
+ - Interactive menu for triggering various SSE events
101
+ - Real-time display of received SSE notifications
102
+ - Session management
103
+
104
+ **Usage:**
105
+
106
+ 1. Start the SSE test server in one terminal:
107
+
108
+ ```console
109
+ $ ruby examples/streamable_http_server.rb
110
+ ```
111
+
112
+ 2. Run the SSE test client in another terminal:
113
+ ```console
114
+ $ ruby examples/streamable_http_client.rb
115
+ ```
116
+
117
+ The client will:
118
+
119
+ - Initialize a session automatically
120
+ - Connect to the SSE stream
121
+ - Provide an interactive menu to trigger notifications
122
+ - Display all received SSE events in real-time
123
+
124
+ ### Testing SSE with cURL
125
+
126
+ You can also test SSE functionality manually using cURL:
127
+
128
+ 1. Initialize a session:
129
+
130
+ ```console
131
+ SESSION_ID=$(curl -D - -s -o /dev/null http://localhost:9393 \
132
+ --json '{"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')
133
+ ```
134
+
135
+ 2. Connect to SSE stream (in one terminal):
136
+
137
+ ```console
138
+ curl -i -N -H "Mcp-Session-Id: $SESSION_ID" http://localhost:9393
139
+ ```
140
+
141
+ 3. Trigger notifications (in another terminal):
142
+
143
+ ```console
144
+ # Send immediate notification
145
+ curl -i http://localhost:9393 \
146
+ -H "Mcp-Session-Id: $SESSION_ID" \
147
+ --json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"notification_tool","arguments":{"message":"Hello from cURL!"}}}'
148
+ ```
149
+
150
+ ## Streamable HTTP Transport Details
151
+
152
+ ### Protocol Flow
153
+
154
+ The HTTP server implements the MCP Streamable HTTP transport protocol:
155
+
156
+ 1. **Initialize Session**:
157
+
158
+ - Client sends POST request with `initialize` method
159
+ - Server responds with session ID in `Mcp-Session-Id` header
160
+
161
+ 2. **Establish SSE Connection** (optional):
162
+
163
+ - Client sends GET request with `Mcp-Session-Id` header
164
+ - Server establishes Server-Sent Events stream for notifications
165
+
166
+ 3. **Send Requests**:
167
+
168
+ - Client sends POST requests with JSON-RPC 2.0 format
169
+ - Server processes and responds with results
170
+
171
+ 4. **Close Session**:
172
+ - Client sends DELETE request with `Mcp-Session-Id` header
173
+
174
+ ### Example cURL Commands
175
+
176
+ Initialize a session:
177
+
178
+ ```console
179
+ curl -i http://localhost:9292 \
180
+ --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
181
+ ```
182
+
183
+ List tools (using the session ID from initialization):
184
+
185
+ ```console
186
+ curl -i http://localhost:9292 \
187
+ -H "Mcp-Session-Id: YOUR_SESSION_ID" \
188
+ --json '{"jsonrpc":"2.0","method":"tools/list","id":2}'
189
+ ```
190
+
191
+ Call a tool:
192
+
193
+ ```console
194
+ curl -i http://localhost:9292 \
195
+ -H "Mcp-Session-Id: YOUR_SESSION_ID" \
196
+ --json '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"ExampleTool","arguments":{"a":5,"b":3}}}'
197
+ ```
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ # Simple HTTP client example for interacting with the MCP HTTP server
8
+ class MCPHTTPClient
9
+ def initialize(base_url = "http://localhost:9292")
10
+ @base_url = base_url
11
+ @session_id = nil
12
+ end
13
+
14
+ def send_request(method, params = nil, id = nil)
15
+ uri = URI(@base_url)
16
+ http = Net::HTTP.new(uri.host, uri.port)
17
+
18
+ request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path)
19
+ request["Content-Type"] = "application/json"
20
+ request["Mcp-Session-Id"] = @session_id if @session_id
21
+
22
+ body = {
23
+ jsonrpc: "2.0",
24
+ method: method,
25
+ params: params,
26
+ id: id || rand(10000),
27
+ }.compact
28
+
29
+ request.body = body.to_json
30
+
31
+ response = http.request(request)
32
+
33
+ # Store session ID if provided
34
+ if response["Mcp-Session-Id"]
35
+ @session_id = response["Mcp-Session-Id"]
36
+ puts "Session ID: #{@session_id}"
37
+ end
38
+
39
+ JSON.parse(response.body)
40
+ end
41
+
42
+ def initialize_session
43
+ puts "=== Initializing session ==="
44
+ result = send_request("initialize", {
45
+ protocolVersion: "2024-11-05",
46
+ capabilities: {},
47
+ clientInfo: {
48
+ name: "example_client",
49
+ version: "1.0",
50
+ },
51
+ })
52
+ puts "Response: #{JSON.pretty_generate(result)}"
53
+
54
+ result
55
+ end
56
+
57
+ def ping
58
+ puts "=== Sending ping ==="
59
+ result = send_request("ping")
60
+ puts "Response: #{JSON.pretty_generate(result)}"
61
+
62
+ result
63
+ end
64
+
65
+ def list_tools
66
+ puts "=== Listing tools ==="
67
+ result = send_request("tools/list")
68
+ puts "Response: #{JSON.pretty_generate(result)}"
69
+
70
+ result
71
+ end
72
+
73
+ def call_tool(name, arguments)
74
+ puts "=== Calling tool: #{name} ==="
75
+ result = send_request("tools/call", {
76
+ name: name,
77
+ arguments: arguments,
78
+ })
79
+ puts "Response: #{JSON.pretty_generate(result)}"
80
+
81
+ result
82
+ end
83
+
84
+ def list_prompts
85
+ puts "=== Listing prompts ==="
86
+ result = send_request("prompts/list")
87
+ puts "Response: #{JSON.pretty_generate(result)}"
88
+
89
+ result
90
+ end
91
+
92
+ def get_prompt(name, arguments)
93
+ puts "=== Getting prompt: #{name} ==="
94
+ result = send_request("prompts/get", {
95
+ name: name,
96
+ arguments: arguments,
97
+ })
98
+ puts "Response: #{JSON.pretty_generate(result)}"
99
+
100
+ result
101
+ end
102
+
103
+ def list_resources
104
+ puts "=== Listing resources ==="
105
+ result = send_request("resources/list")
106
+ puts "Response: #{JSON.pretty_generate(result)}"
107
+
108
+ result
109
+ end
110
+
111
+ def read_resource(uri)
112
+ puts "=== Reading resource: #{uri} ==="
113
+ result = send_request("resources/read", {
114
+ uri: uri,
115
+ })
116
+ puts "Response: #{JSON.pretty_generate(result)}"
117
+
118
+ result
119
+ end
120
+
121
+ def close_session
122
+ return unless @session_id
123
+
124
+ puts "=== Closing session ==="
125
+ uri = URI(@base_url)
126
+ http = Net::HTTP.new(uri.host, uri.port)
127
+
128
+ request = Net::HTTP::Delete.new(uri.path.empty? ? "/" : uri.path)
129
+ request["Mcp-Session-Id"] = @session_id
130
+
131
+ response = http.request(request)
132
+ result = JSON.parse(response.body)
133
+ puts "Response: #{JSON.pretty_generate(result)}"
134
+
135
+ @session_id = nil
136
+ result
137
+ end
138
+ end
139
+
140
+ # Main script
141
+ if __FILE__ == $PROGRAM_NAME
142
+ puts <<~MESSAGE
143
+ MCP HTTP Client Example
144
+ Make sure the HTTP server is running (ruby examples/http_server.rb)
145
+ #{"=" * 50}
146
+ MESSAGE
147
+
148
+ client = MCPHTTPClient.new
149
+
150
+ begin
151
+ # Initialize session
152
+ client.initialize_session
153
+
154
+ # Test ping
155
+ client.ping
156
+
157
+ # List available tools
158
+ client.list_tools
159
+
160
+ # Call the example_tool (note: snake_case name)
161
+ client.call_tool("example_tool", { a: 5, b: 3 })
162
+
163
+ # Call the echo tool
164
+ client.call_tool("echo", { message: "Hello from client!" })
165
+
166
+ # List prompts
167
+ client.list_prompts
168
+
169
+ # Get a prompt (note: snake_case name)
170
+ client.get_prompt("example_prompt", { message: "This is a test message" })
171
+
172
+ # List resources
173
+ client.list_resources
174
+
175
+ # Read a resource
176
+ client.read_resource("test_resource")
177
+ rescue => e
178
+ puts "Error: #{e.message}"
179
+ puts e.backtrace
180
+ ensure
181
+ # Clean up session
182
+ client.close_session
183
+ end
184
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+ require "mcp"
5
+ require "mcp/server/transports/streamable_http_transport"
6
+ require "rack"
7
+ require "rackup"
8
+ require "json"
9
+ require "logger"
10
+
11
+ # Create a simple tool
12
+ class ExampleTool < MCP::Tool
13
+ description "A simple example tool that adds two numbers"
14
+ input_schema(
15
+ properties: {
16
+ a: { type: "number" },
17
+ b: { type: "number" },
18
+ },
19
+ required: ["a", "b"],
20
+ )
21
+
22
+ class << self
23
+ def call(a:, b:)
24
+ MCP::Tool::Response.new([{
25
+ type: "text",
26
+ text: "The sum of #{a} and #{b} is #{a + b}",
27
+ }])
28
+ end
29
+ end
30
+ end
31
+
32
+ # Create a simple prompt
33
+ class ExamplePrompt < MCP::Prompt
34
+ description "A simple example prompt that echoes back its arguments"
35
+ arguments [
36
+ MCP::Prompt::Argument.new(
37
+ name: "message",
38
+ description: "The message to echo back",
39
+ required: true,
40
+ ),
41
+ ]
42
+
43
+ class << self
44
+ def template(args, server_context:)
45
+ MCP::Prompt::Result.new(
46
+ messages: [
47
+ MCP::Prompt::Message.new(
48
+ role: "user",
49
+ content: MCP::Content::Text.new(args[:message]),
50
+ ),
51
+ ],
52
+ )
53
+ end
54
+ end
55
+ end
56
+
57
+ # Set up the server
58
+ server = MCP::Server.new(
59
+ name: "example_http_server",
60
+ tools: [ExampleTool],
61
+ prompts: [ExamplePrompt],
62
+ resources: [
63
+ MCP::Resource.new(
64
+ uri: "https://test_resource.invalid",
65
+ name: "test-resource",
66
+ title: "Test Resource",
67
+ description: "Test resource that echoes back the uri as its content",
68
+ mime_type: "text/plain",
69
+ ),
70
+ ],
71
+ )
72
+
73
+ server.define_tool(
74
+ name: "echo",
75
+ description: "A simple example tool that echoes back its arguments",
76
+ input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
77
+ ) do |message:|
78
+ MCP::Tool::Response.new(
79
+ [
80
+ {
81
+ type: "text",
82
+ text: "Hello from echo tool! Message: #{message}",
83
+ },
84
+ ],
85
+ )
86
+ end
87
+
88
+ server.resources_read_handler do |params|
89
+ [{
90
+ uri: params[:uri],
91
+ mimeType: "text/plain",
92
+ text: "Hello from HTTP server resource!",
93
+ }]
94
+ end
95
+
96
+ # Create the Streamable HTTP transport
97
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
98
+ server.transport = transport
99
+
100
+ # Create a logger for MCP-specific logging
101
+ mcp_logger = Logger.new($stdout)
102
+ mcp_logger.formatter = proc do |_severity, _datetime, _progname, msg|
103
+ "[MCP] #{msg}\n"
104
+ end
105
+
106
+ # Create a Rack application with logging
107
+ app = proc do |env|
108
+ request = Rack::Request.new(env)
109
+
110
+ # Log MCP-specific details for POST requests
111
+ if request.post?
112
+ body = request.body.read
113
+ request.body.rewind
114
+ begin
115
+ parsed_body = JSON.parse(body)
116
+ mcp_logger.info("Request: #{parsed_body["method"]} (id: #{parsed_body["id"]})")
117
+ mcp_logger.debug("Request body: #{JSON.pretty_generate(parsed_body)}")
118
+ rescue JSON::ParserError
119
+ mcp_logger.warn("Request body (raw): #{body}")
120
+ end
121
+ end
122
+
123
+ # Handle the request
124
+ response = transport.handle_request(request)
125
+
126
+ # Log the MCP response details
127
+ _, _, body = response
128
+ if body.is_a?(Array) && !body.empty? && body.first
129
+ begin
130
+ parsed_response = JSON.parse(body.first)
131
+ if parsed_response["error"]
132
+ mcp_logger.error("Response error: #{parsed_response["error"]["message"]}")
133
+ else
134
+ mcp_logger.info("Response: #{parsed_response["result"] ? "success" : "empty"} (id: #{parsed_response["id"]})")
135
+ end
136
+ mcp_logger.debug("Response body: #{JSON.pretty_generate(parsed_response)}")
137
+ rescue JSON::ParserError
138
+ mcp_logger.warn("Response body (raw): #{body}")
139
+ end
140
+ end
141
+
142
+ response
143
+ end
144
+
145
+ # Wrap the app with Rack middleware
146
+ rack_app = Rack::Builder.new do
147
+ # Use CommonLogger for standard HTTP request logging
148
+ use(Rack::CommonLogger, Logger.new($stdout))
149
+
150
+ # Add other useful middleware
151
+ use(Rack::ShowExceptions)
152
+
153
+ run(app)
154
+ end
155
+
156
+ # Start the server
157
+ puts <<~MESSAGE
158
+ Starting MCP HTTP server on http://localhost:9292
159
+ Use POST requests to initialize and send JSON-RPC commands
160
+ Example initialization:
161
+ curl -i http://localhost:9292 --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
162
+
163
+ The server will return a session ID in the Mcp-Session-Id header.
164
+ Use this session ID for subsequent requests.
165
+
166
+ Press Ctrl+C to stop the server
167
+ MESSAGE
168
+
169
+ # Run the server
170
+ # Use Rackup to run the server
171
+ Rackup::Handler.get("puma").run(rack_app, Port: 9292, Host: "localhost")
@@ -1,9 +1,8 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
4
  require "mcp"
6
- require "mcp/transports/stdio"
5
+ require "mcp/server/transports/stdio_transport"
7
6
 
8
7
  # Create a simple tool
9
8
  class ExampleTool < MCP::Tool
@@ -59,8 +58,9 @@ server = MCP::Server.new(
59
58
  prompts: [ExamplePrompt],
60
59
  resources: [
61
60
  MCP::Resource.new(
62
- uri: "test_resource",
63
- name: "Test resource",
61
+ uri: "https://test_resource.invalid",
62
+ name: "test-resource",
63
+ title: "Test Resource",
64
64
  description: "Test resource that echoes back the uri as its content",
65
65
  mime_type: "text/plain",
66
66
  ),
@@ -86,10 +86,10 @@ server.resources_read_handler do |params|
86
86
  [{
87
87
  uri: params[:uri],
88
88
  mimeType: "text/plain",
89
- text: "Hello, world!",
89
+ text: "Hello, world! URI: #{params[:uri]}",
90
90
  }]
91
91
  end
92
92
 
93
93
  # Create and start the transport
94
- transport = MCP::Transports::StdioTransport.new(server)
94
+ transport = MCP::Server::Transports::StdioTransport.new(server)
95
95
  transport.open