mcp 0.1.0 → 0.2.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,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
+ puts
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
+ puts
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
+ puts
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
+ puts
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
+ puts
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
+ puts
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
+ puts
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
+ puts
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
+ puts
135
+
136
+ @session_id = nil
137
+ result
138
+ end
139
+ end
140
+
141
+ # Main script
142
+ if __FILE__ == $PROGRAM_NAME
143
+ puts "MCP HTTP Client Example"
144
+ puts "Make sure the HTTP server is running (ruby examples/http_server.rb)"
145
+ puts "=" * 50
146
+ puts
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,168 @@
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: "test_resource",
65
+ name: "Test resource",
66
+ description: "Test resource that echoes back the uri as its content",
67
+ mime_type: "text/plain",
68
+ ),
69
+ ],
70
+ )
71
+
72
+ server.define_tool(
73
+ name: "echo",
74
+ description: "A simple example tool that echoes back its arguments",
75
+ input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
76
+ ) do |message:|
77
+ MCP::Tool::Response.new(
78
+ [
79
+ {
80
+ type: "text",
81
+ text: "Hello from echo tool! Message: #{message}",
82
+ },
83
+ ],
84
+ )
85
+ end
86
+
87
+ server.resources_read_handler do |params|
88
+ [{
89
+ uri: params[:uri],
90
+ mimeType: "text/plain",
91
+ text: "Hello from HTTP server resource!",
92
+ }]
93
+ end
94
+
95
+ # Create the Streamable HTTP transport
96
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
97
+ server.transport = transport
98
+
99
+ # Create a logger for MCP-specific logging
100
+ mcp_logger = Logger.new($stdout)
101
+ mcp_logger.formatter = proc do |_severity, _datetime, _progname, msg|
102
+ "[MCP] #{msg}\n"
103
+ end
104
+
105
+ # Create a Rack application with logging
106
+ app = proc do |env|
107
+ request = Rack::Request.new(env)
108
+
109
+ # Log MCP-specific details for POST requests
110
+ if request.post?
111
+ body = request.body.read
112
+ request.body.rewind
113
+ begin
114
+ parsed_body = JSON.parse(body)
115
+ mcp_logger.info("Request: #{parsed_body["method"]} (id: #{parsed_body["id"]})")
116
+ mcp_logger.debug("Request body: #{JSON.pretty_generate(parsed_body)}")
117
+ rescue JSON::ParserError
118
+ mcp_logger.warn("Request body (raw): #{body}")
119
+ end
120
+ end
121
+
122
+ # Handle the request
123
+ response = transport.handle_request(request)
124
+
125
+ # Log the MCP response details
126
+ _, _, body = response
127
+ if body.is_a?(Array) && !body.empty? && body.first
128
+ begin
129
+ parsed_response = JSON.parse(body.first)
130
+ if parsed_response["error"]
131
+ mcp_logger.error("Response error: #{parsed_response["error"]["message"]}")
132
+ else
133
+ mcp_logger.info("Response: #{parsed_response["result"] ? "success" : "empty"} (id: #{parsed_response["id"]})")
134
+ end
135
+ mcp_logger.debug("Response body: #{JSON.pretty_generate(parsed_response)}")
136
+ rescue JSON::ParserError
137
+ mcp_logger.warn("Response body (raw): #{body}")
138
+ end
139
+ end
140
+
141
+ response
142
+ end
143
+
144
+ # Wrap the app with Rack middleware
145
+ rack_app = Rack::Builder.new do
146
+ # Use CommonLogger for standard HTTP request logging
147
+ use(Rack::CommonLogger, Logger.new($stdout))
148
+
149
+ # Add other useful middleware
150
+ use(Rack::ShowExceptions)
151
+
152
+ run(app)
153
+ end
154
+
155
+ # Start the server
156
+ puts "Starting MCP HTTP server on http://localhost:9292"
157
+ puts "Use POST requests to initialize and send JSON-RPC commands"
158
+ puts "Example initialization:"
159
+ puts ' 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"}}}\''
160
+ puts ""
161
+ puts "The server will return a session ID in the Mcp-Session-Id header."
162
+ puts "Use this session ID for subsequent requests."
163
+ puts ""
164
+ puts "Press Ctrl+C to stop the server"
165
+
166
+ # Run the server
167
+ # Use Rackup to run the server
168
+ 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
@@ -91,5 +90,5 @@ server.resources_read_handler do |params|
91
90
  end
92
91
 
93
92
  # Create and start the transport
94
- transport = MCP::Transports::StdioTransport.new(server)
93
+ transport = MCP::Server::Transports::StdioTransport.new(server)
95
94
  transport.open
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "logger"
7
+
8
+ # Logger for client operations
9
+ logger = Logger.new($stdout)
10
+ logger.formatter = proc do |severity, datetime, _progname, msg|
11
+ "[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
12
+ end
13
+
14
+ # Server configuration
15
+ SERVER_URL = "http://localhost:9393/mcp"
16
+ PROTOCOL_VERSION = "2024-11-05"
17
+
18
+ # Helper method to make JSON-RPC requests
19
+ def make_request(session_id, method, params = {}, id = nil)
20
+ uri = URI(SERVER_URL)
21
+ http = Net::HTTP.new(uri.host, uri.port)
22
+
23
+ request = Net::HTTP::Post.new(uri)
24
+ request["Content-Type"] = "application/json"
25
+ request["Mcp-Session-Id"] = session_id if session_id
26
+
27
+ body = {
28
+ jsonrpc: "2.0",
29
+ method: method,
30
+ params: params,
31
+ id: id || SecureRandom.uuid,
32
+ }
33
+
34
+ request.body = body.to_json
35
+ response = http.request(request)
36
+
37
+ {
38
+ status: response.code,
39
+ headers: response.to_hash,
40
+ body: JSON.parse(response.body),
41
+ }
42
+ rescue => e
43
+ { error: e.message }
44
+ end
45
+
46
+ # Connect to SSE stream
47
+ def connect_sse(session_id, logger)
48
+ uri = URI(SERVER_URL)
49
+
50
+ logger.info("Connecting to SSE stream...")
51
+
52
+ Net::HTTP.start(uri.host, uri.port) do |http|
53
+ request = Net::HTTP::Get.new(uri)
54
+ request["Mcp-Session-Id"] = session_id
55
+ request["Accept"] = "text/event-stream"
56
+ request["Cache-Control"] = "no-cache"
57
+
58
+ http.request(request) do |response|
59
+ if response.code == "200"
60
+ logger.info("SSE stream connected successfully")
61
+
62
+ response.read_body do |chunk|
63
+ chunk.split("\n").each do |line|
64
+ if line.start_with?("data: ")
65
+ data = line[6..-1]
66
+ begin
67
+ logger.info("SSE data: #{data}")
68
+ rescue JSON::ParserError
69
+ logger.debug("Non-JSON SSE data: #{data}")
70
+ end
71
+ elsif line.start_with?(": ")
72
+ logger.debug("SSE keepalive received: #{line}")
73
+ end
74
+ end
75
+ end
76
+ else
77
+ logger.error("Failed to connect to SSE: #{response.code} #{response.message}")
78
+ end
79
+ end
80
+ end
81
+ rescue Interrupt
82
+ logger.info("SSE connection interrupted by user")
83
+ rescue => e
84
+ logger.error("SSE connection error: #{e.message}")
85
+ end
86
+
87
+ # Main client flow
88
+ def main
89
+ logger = Logger.new($stdout)
90
+ logger.formatter = proc do |severity, datetime, _progname, msg|
91
+ "[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
92
+ end
93
+
94
+ puts "=== MCP SSE Test Client ==="
95
+ puts ""
96
+
97
+ # Step 1: Initialize session
98
+ logger.info("Initializing session...")
99
+
100
+ init_response = make_request(
101
+ nil,
102
+ "initialize",
103
+ {
104
+ protocolVersion: PROTOCOL_VERSION,
105
+ capabilities: {},
106
+ clientInfo: {
107
+ name: "sse-test-client",
108
+ version: "1.0",
109
+ },
110
+ },
111
+ "init-1",
112
+ )
113
+
114
+ if init_response[:error]
115
+ logger.error("Failed to initialize: #{init_response[:error]}")
116
+ exit(1)
117
+ end
118
+
119
+ session_id = init_response[:headers]["mcp-session-id"]&.first
120
+
121
+ if session_id.nil?
122
+ logger.error("No session ID received")
123
+ exit(1)
124
+ end
125
+
126
+ logger.info("Session initialized: #{session_id}")
127
+ logger.info("Server info: #{init_response[:body]["result"]["serverInfo"]}")
128
+
129
+ # Step 2: Start SSE connection in a separate thread
130
+ sse_thread = Thread.new { connect_sse(session_id, logger) }
131
+
132
+ # Give SSE time to connect
133
+ sleep(1)
134
+
135
+ # Step 3: Interactive menu
136
+ loop do
137
+ puts "\n=== Available Actions ==="
138
+ puts "1. Send custom notification"
139
+ puts "2. Test echo"
140
+ puts "3. List tools"
141
+ puts "0. Exit"
142
+ puts ""
143
+ print("Choose an action: ")
144
+
145
+ choice = gets.chomp
146
+
147
+ case choice
148
+ when "1"
149
+ print("Enter notification message: ")
150
+ message = gets.chomp
151
+ print("Enter delay in seconds (0 for immediate): ")
152
+ delay = gets.chomp.to_f
153
+
154
+ response = make_request(
155
+ session_id,
156
+ "tools/call",
157
+ {
158
+ name: "notification_tool",
159
+ arguments: {
160
+ message: message,
161
+ delay: delay,
162
+ },
163
+ },
164
+ )
165
+ if response[:body]["accepted"]
166
+ logger.info("Notification sent successfully")
167
+ else
168
+ logger.error("Error: #{response[:body]["error"]}")
169
+ end
170
+
171
+ when "2"
172
+ print("Enter message to echo: ")
173
+ message = gets.chomp
174
+ make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } })
175
+
176
+ when "3"
177
+ make_request(session_id, "tools/list")
178
+
179
+ when "0"
180
+ logger.info("Exiting...")
181
+ break
182
+
183
+ else
184
+ puts "Invalid choice"
185
+ end
186
+ end
187
+
188
+ # Clean up
189
+ sse_thread.kill if sse_thread.alive?
190
+
191
+ # Close session
192
+ logger.info("Closing session...")
193
+ make_request(session_id, "close")
194
+ logger.info("Session closed")
195
+ rescue Interrupt
196
+ logger.info("Client interrupted by user")
197
+ rescue => e
198
+ logger.error("Client error: #{e.message}")
199
+ logger.error(e.backtrace.join("\n"))
200
+ end
201
+
202
+ # Run the client
203
+ if __FILE__ == $PROGRAM_NAME
204
+ main
205
+ end