mcp 0.7.1 → 0.9.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.
@@ -1,207 +0,0 @@
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
-
96
- # Step 1: Initialize session
97
- logger.info("Initializing session...")
98
-
99
- init_response = make_request(
100
- nil,
101
- "initialize",
102
- {
103
- protocolVersion: PROTOCOL_VERSION,
104
- capabilities: {},
105
- clientInfo: {
106
- name: "sse-test-client",
107
- version: "1.0",
108
- },
109
- },
110
- "init-1",
111
- )
112
-
113
- if init_response[:error]
114
- logger.error("Failed to initialize: #{init_response[:error]}")
115
- exit(1)
116
- end
117
-
118
- session_id = init_response[:headers]["mcp-session-id"]&.first
119
-
120
- if session_id.nil?
121
- logger.error("No session ID received")
122
- exit(1)
123
- end
124
-
125
- if init_response[:body].dig("result", "capabilities", "logging")
126
- make_request(session_id, "logging/setLevel", { level: "info" })
127
- end
128
-
129
- logger.info("Session initialized: #{session_id}")
130
- logger.info("Server info: #{init_response[:body]["result"]["serverInfo"]}")
131
-
132
- # Step 2: Start SSE connection in a separate thread
133
- sse_thread = Thread.new { connect_sse(session_id, logger) }
134
-
135
- # Give SSE time to connect
136
- sleep(1)
137
-
138
- # Step 3: Interactive menu
139
- loop do
140
- puts <<~MESSAGE.chomp
141
-
142
- === Available Actions ===
143
- 1. Send custom notification
144
- 2. Test echo
145
- 3. List tools
146
- 0. Exit
147
-
148
- Choose an action:#{" "}
149
- MESSAGE
150
-
151
- choice = gets.chomp
152
-
153
- case choice
154
- when "1"
155
- print("Enter notification message: ")
156
- message = gets.chomp
157
- print("Enter delay in seconds (0 for immediate): ")
158
- delay = gets.chomp.to_f
159
-
160
- response = make_request(
161
- session_id,
162
- "tools/call",
163
- {
164
- name: "notification_tool",
165
- arguments: {
166
- message: message,
167
- delay: delay,
168
- },
169
- },
170
- )
171
- if response[:body]["accepted"]
172
- logger.info("Notification sent successfully")
173
- else
174
- logger.error("Error: #{response[:body]["error"]}")
175
- end
176
- when "2"
177
- print("Enter message to echo: ")
178
- message = gets.chomp
179
- make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } })
180
- when "3"
181
- make_request(session_id, "tools/list")
182
- when "0"
183
- logger.info("Exiting...")
184
- break
185
- else
186
- puts "Invalid choice"
187
- end
188
- end
189
-
190
- # Clean up
191
- sse_thread.kill if sse_thread.alive?
192
-
193
- # Close session
194
- logger.info("Closing session...")
195
- make_request(session_id, "close")
196
- logger.info("Session closed")
197
- rescue Interrupt
198
- logger.info("Client interrupted by user")
199
- rescue => e
200
- logger.error("Client error: #{e.message}")
201
- logger.error(e.backtrace.join("\n"))
202
- end
203
-
204
- # Run the client
205
- if __FILE__ == $PROGRAM_NAME
206
- main
207
- end
@@ -1,172 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
- require "mcp"
5
- require "rackup"
6
- require "json"
7
- require "logger"
8
-
9
- # Create a logger for SSE-specific logging
10
- sse_logger = Logger.new($stdout)
11
- sse_logger.formatter = proc do |severity, datetime, _progname, msg|
12
- "[SSE] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
13
- end
14
-
15
- # Tool that returns a response that will be sent via SSE if a stream is active
16
- class NotificationTool < MCP::Tool
17
- tool_name "notification_tool"
18
- description "Returns a notification message that will be sent via SSE if stream is active"
19
- input_schema(
20
- properties: {
21
- message: { type: "string", description: "Message to send via SSE" },
22
- delay: { type: "number", description: "Delay in seconds before returning (optional)" },
23
- },
24
- required: ["message"],
25
- )
26
-
27
- class << self
28
- attr_accessor :logger
29
-
30
- def call(message:, delay: 0)
31
- sleep(delay) if delay > 0
32
-
33
- logger&.info("Returning notification message: #{message}")
34
-
35
- MCP::Tool::Response.new([{
36
- type: "text",
37
- text: "Notification: #{message} (timestamp: #{Time.now.iso8601})",
38
- }])
39
- end
40
- end
41
- end
42
-
43
- # Create the server
44
- server = MCP::Server.new(
45
- name: "sse_test_server",
46
- tools: [NotificationTool],
47
- prompts: [],
48
- resources: [],
49
- )
50
-
51
- # Set logger for tools
52
- NotificationTool.logger = sse_logger
53
-
54
- # Add a simple echo tool for basic testing
55
- server.define_tool(
56
- name: "echo",
57
- description: "Simple echo tool",
58
- input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
59
- ) do |message:|
60
- MCP::Tool::Response.new([{ type: "text", text: "Echo: #{message}" }])
61
- end
62
-
63
- # Create the Streamable HTTP transport
64
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
65
- server.transport = transport
66
-
67
- # Create a logger for MCP request/response logging
68
- mcp_logger = Logger.new($stdout)
69
- mcp_logger.formatter = proc do |_severity, _datetime, _progname, msg|
70
- "[MCP] #{msg}\n"
71
- end
72
-
73
- # Create the Rack application
74
- app = proc do |env|
75
- request = Rack::Request.new(env)
76
-
77
- # Log request details
78
- if request.post?
79
- body = request.body.read
80
- request.body.rewind
81
- begin
82
- parsed_body = JSON.parse(body)
83
- mcp_logger.info("Request: #{parsed_body["method"]} (id: #{parsed_body["id"]})")
84
-
85
- # Log SSE-specific setup
86
- if parsed_body["method"] == "initialize"
87
- sse_logger.info("New client initializing session")
88
- end
89
- rescue JSON::ParserError
90
- mcp_logger.warn("Invalid JSON in request")
91
- end
92
- elsif request.get?
93
- session_id = request.env["HTTP_MCP_SESSION_ID"] ||
94
- Rack::Utils.parse_query(request.env["QUERY_STRING"])["sessionId"]
95
- sse_logger.info("SSE connection request for session: #{session_id}")
96
- end
97
-
98
- # Handle the request
99
- response = transport.handle_request(request)
100
-
101
- # Log response details
102
- status, headers, body = response
103
- if body.is_a?(Array) && !body.empty? && request.post?
104
- begin
105
- parsed_response = JSON.parse(body.first)
106
- if parsed_response["error"]
107
- mcp_logger.error("Response error: #{parsed_response["error"]["message"]}")
108
- elsif parsed_response["accepted"]
109
- # Response was sent via SSE
110
- server.notify_log_message(data: { details: "Response accepted and sent via SSE" }, level: "info")
111
- sse_logger.info("Response sent via SSE stream")
112
- else
113
- mcp_logger.info("Response: success (id: #{parsed_response["id"]})")
114
-
115
- # Log session ID for initialization
116
- if headers["Mcp-Session-Id"]
117
- sse_logger.info("Session created: #{headers["Mcp-Session-Id"]}")
118
- end
119
- end
120
- rescue JSON::ParserError
121
- mcp_logger.warn("Invalid JSON in response")
122
- end
123
- elsif request.get? && status == 200
124
- sse_logger.info("SSE stream established")
125
- end
126
-
127
- response
128
- end
129
-
130
- # Build the Rack application with middleware
131
- rack_app = Rack::Builder.new do
132
- use(Rack::CommonLogger, Logger.new($stdout))
133
- use(Rack::ShowExceptions)
134
- run(app)
135
- end
136
-
137
- # Print usage instructions
138
- puts <<~MESSAGE
139
- === MCP Streaming HTTP Test Server ===
140
-
141
- Starting server on http://localhost:9393
142
-
143
- Available Tools:
144
- 1. NotificationTool - Returns messages that are sent via SSE when stream is active"
145
- 2. echo - Simple echo tool
146
-
147
- Testing SSE:
148
-
149
- 1. Initialize session:
150
- curl -i http://localhost:9393 \\
151
- --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sse-test","version":"1.0"}}}'
152
-
153
- 2. Connect SSE stream (use the session ID from step 1):"
154
- curl -i -N -H "Mcp-Session-Id: YOUR_SESSION_ID" http://localhost:9393
155
-
156
- 3. In another terminal, test tools (responses will be sent via SSE if stream is active):
157
-
158
- Echo tool:
159
- curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \\
160
- --json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"echo","arguments":{"message":"Hello SSE!"}}}'
161
-
162
- Notification tool (with 2 second delay):
163
- curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \\
164
- --json '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"notification_tool","arguments":{"message":"Hello SSE!", "delay": 2}}}'
165
-
166
- Note: When an SSE stream is active, tool responses will appear in the SSE stream and the POST request will return {"accepted": true}
167
-
168
- Press Ctrl+C to stop the server
169
- MESSAGE
170
-
171
- # Start the server
172
- Rackup::Handler.get("puma").run(rack_app, Port: 9393, Host: "localhost")
data/mcp.gemspec DELETED
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/mcp/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "mcp"
7
- spec.version = MCP::VERSION
8
- spec.authors = ["Model Context Protocol"]
9
- spec.email = ["mcp-support@anthropic.com"]
10
-
11
- spec.summary = "The official Ruby SDK for Model Context Protocol servers and clients"
12
- spec.description = spec.summary
13
- spec.homepage = "https://github.com/modelcontextprotocol/ruby-sdk"
14
- spec.license = "Apache-2.0"
15
-
16
- # Since this library is used by a broad range of users, it does not align its support policy with Ruby's EOL.
17
- spec.required_ruby_version = ">= 2.7.0"
18
-
19
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
- spec.metadata["changelog_uri"] = "https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v#{spec.version}"
21
- spec.metadata["homepage_uri"] = spec.homepage
22
- spec.metadata["source_code_uri"] = spec.homepage
23
- spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
24
- spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/mcp"
25
-
26
- spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
27
- %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
- end
29
-
30
- spec.bindir = "exe"
31
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
33
-
34
- spec.add_dependency("json-schema", ">= 4.1")
35
- end