mcp 0.4.0 → 0.11.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +216 -0
  3. data/README.md +550 -63
  4. data/lib/json_rpc_handler.rb +171 -0
  5. data/lib/mcp/annotations.rb +21 -0
  6. data/lib/mcp/client/http.rb +23 -7
  7. data/lib/mcp/client/stdio.rb +222 -0
  8. data/lib/mcp/client.rb +109 -34
  9. data/lib/mcp/configuration.rb +11 -9
  10. data/lib/mcp/content.rb +29 -2
  11. data/lib/mcp/icon.rb +22 -0
  12. data/lib/mcp/instrumentation.rb +1 -1
  13. data/lib/mcp/logging_message_notification.rb +30 -0
  14. data/lib/mcp/methods.rb +3 -0
  15. data/lib/mcp/progress.rb +24 -0
  16. data/lib/mcp/prompt/message.rb +1 -1
  17. data/lib/mcp/prompt/result.rb +1 -1
  18. data/lib/mcp/prompt.rb +22 -5
  19. data/lib/mcp/resource/contents.rb +2 -2
  20. data/lib/mcp/resource/embedded.rb +2 -1
  21. data/lib/mcp/resource.rb +7 -2
  22. data/lib/mcp/resource_template.rb +4 -2
  23. data/lib/mcp/server/transports/stdio_transport.rb +41 -4
  24. data/lib/mcp/server/transports/streamable_http_transport.rb +456 -85
  25. data/lib/mcp/server/transports.rb +10 -0
  26. data/lib/mcp/server.rb +403 -67
  27. data/lib/mcp/server_context.rb +58 -0
  28. data/lib/mcp/server_session.rb +107 -0
  29. data/lib/mcp/string_utils.rb +3 -3
  30. data/lib/mcp/tool/annotations.rb +1 -1
  31. data/lib/mcp/tool/input_schema.rb +6 -55
  32. data/lib/mcp/tool/output_schema.rb +3 -54
  33. data/lib/mcp/tool/response.rb +1 -1
  34. data/lib/mcp/tool/schema.rb +48 -0
  35. data/lib/mcp/tool.rb +39 -5
  36. data/lib/mcp/transport.rb +15 -2
  37. data/lib/mcp/version.rb +1 -1
  38. data/lib/mcp.rb +12 -31
  39. metadata +21 -42
  40. data/.gitattributes +0 -4
  41. data/.github/dependabot.yml +0 -6
  42. data/.github/workflows/ci.yml +0 -33
  43. data/.github/workflows/release.yml +0 -25
  44. data/.gitignore +0 -10
  45. data/.rubocop.yml +0 -12
  46. data/AGENTS.md +0 -119
  47. data/CHANGELOG.md +0 -87
  48. data/CODE_OF_CONDUCT.md +0 -74
  49. data/Gemfile +0 -27
  50. data/LICENSE.txt +0 -21
  51. data/Rakefile +0 -17
  52. data/bin/console +0 -15
  53. data/bin/rake +0 -31
  54. data/bin/setup +0 -8
  55. data/dev.yml +0 -31
  56. data/examples/README.md +0 -197
  57. data/examples/http_client.rb +0 -184
  58. data/examples/http_server.rb +0 -170
  59. data/examples/stdio_server.rb +0 -94
  60. data/examples/streamable_http_client.rb +0 -203
  61. data/examples/streamable_http_server.rb +0 -172
  62. data/mcp.gemspec +0 -32
@@ -1,203 +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
- logger.info("Session initialized: #{session_id}")
126
- logger.info("Server info: #{init_response[:body]["result"]["serverInfo"]}")
127
-
128
- # Step 2: Start SSE connection in a separate thread
129
- sse_thread = Thread.new { connect_sse(session_id, logger) }
130
-
131
- # Give SSE time to connect
132
- sleep(1)
133
-
134
- # Step 3: Interactive menu
135
- loop do
136
- puts <<~MESSAGE.chomp
137
-
138
- === Available Actions ===
139
- 1. Send custom notification
140
- 2. Test echo
141
- 3. List tools
142
- 0. Exit
143
-
144
- Choose an action:#{" "}
145
- MESSAGE
146
-
147
- choice = gets.chomp
148
-
149
- case choice
150
- when "1"
151
- print("Enter notification message: ")
152
- message = gets.chomp
153
- print("Enter delay in seconds (0 for immediate): ")
154
- delay = gets.chomp.to_f
155
-
156
- response = make_request(
157
- session_id,
158
- "tools/call",
159
- {
160
- name: "notification_tool",
161
- arguments: {
162
- message: message,
163
- delay: delay,
164
- },
165
- },
166
- )
167
- if response[:body]["accepted"]
168
- logger.info("Notification sent successfully")
169
- else
170
- logger.error("Error: #{response[:body]["error"]}")
171
- end
172
- when "2"
173
- print("Enter message to echo: ")
174
- message = gets.chomp
175
- make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } })
176
- when "3"
177
- make_request(session_id, "tools/list")
178
- when "0"
179
- logger.info("Exiting...")
180
- break
181
- else
182
- puts "Invalid choice"
183
- end
184
- end
185
-
186
- # Clean up
187
- sse_thread.kill if sse_thread.alive?
188
-
189
- # Close session
190
- logger.info("Closing session...")
191
- make_request(session_id, "close")
192
- logger.info("Session closed")
193
- rescue Interrupt
194
- logger.info("Client interrupted by user")
195
- rescue => e
196
- logger.error("Client error: #{e.message}")
197
- logger.error(e.backtrace.join("\n"))
198
- end
199
-
200
- # Run the client
201
- if __FILE__ == $PROGRAM_NAME
202
- main
203
- 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 "rack"
6
- require "rackup"
7
- require "json"
8
- require "logger"
9
-
10
- # Create a logger for SSE-specific logging
11
- sse_logger = Logger.new($stdout)
12
- sse_logger.formatter = proc do |severity, datetime, _progname, msg|
13
- "[SSE] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
14
- end
15
-
16
- # Tool that returns a response that will be sent via SSE if a stream is active
17
- class NotificationTool < MCP::Tool
18
- tool_name "notification_tool"
19
- description "Returns a notification message that will be sent via SSE if stream is active"
20
- input_schema(
21
- properties: {
22
- message: { type: "string", description: "Message to send via SSE" },
23
- delay: { type: "number", description: "Delay in seconds before returning (optional)" },
24
- },
25
- required: ["message"],
26
- )
27
-
28
- class << self
29
- attr_accessor :logger
30
-
31
- def call(message:, delay: 0)
32
- sleep(delay) if delay > 0
33
-
34
- logger&.info("Returning notification message: #{message}")
35
-
36
- MCP::Tool::Response.new([{
37
- type: "text",
38
- text: "Notification: #{message} (timestamp: #{Time.now.iso8601})",
39
- }])
40
- end
41
- end
42
- end
43
-
44
- # Create the server
45
- server = MCP::Server.new(
46
- name: "sse_test_server",
47
- tools: [NotificationTool],
48
- prompts: [],
49
- resources: [],
50
- )
51
-
52
- # Set logger for tools
53
- NotificationTool.logger = sse_logger
54
-
55
- # Add a simple echo tool for basic testing
56
- server.define_tool(
57
- name: "echo",
58
- description: "Simple echo tool",
59
- input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
60
- ) do |message:|
61
- MCP::Tool::Response.new([{ type: "text", text: "Echo: #{message}" }])
62
- end
63
-
64
- # Create the Streamable HTTP transport
65
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
66
- server.transport = transport
67
-
68
- # Create a logger for MCP request/response logging
69
- mcp_logger = Logger.new($stdout)
70
- mcp_logger.formatter = proc do |_severity, _datetime, _progname, msg|
71
- "[MCP] #{msg}\n"
72
- end
73
-
74
- # Create the Rack application
75
- app = proc do |env|
76
- request = Rack::Request.new(env)
77
-
78
- # Log request details
79
- if request.post?
80
- body = request.body.read
81
- request.body.rewind
82
- begin
83
- parsed_body = JSON.parse(body)
84
- mcp_logger.info("Request: #{parsed_body["method"]} (id: #{parsed_body["id"]})")
85
-
86
- # Log SSE-specific setup
87
- if parsed_body["method"] == "initialize"
88
- sse_logger.info("New client initializing session")
89
- end
90
- rescue JSON::ParserError
91
- mcp_logger.warn("Invalid JSON in request")
92
- end
93
- elsif request.get?
94
- session_id = request.env["HTTP_MCP_SESSION_ID"] ||
95
- Rack::Utils.parse_query(request.env["QUERY_STRING"])["sessionId"]
96
- sse_logger.info("SSE connection request for session: #{session_id}")
97
- end
98
-
99
- # Handle the request
100
- response = transport.handle_request(request)
101
-
102
- # Log response details
103
- status, headers, body = response
104
- if body.is_a?(Array) && !body.empty? && request.post?
105
- begin
106
- parsed_response = JSON.parse(body.first)
107
- if parsed_response["error"]
108
- mcp_logger.error("Response error: #{parsed_response["error"]["message"]}")
109
- elsif parsed_response["accepted"]
110
- # Response was sent via SSE
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,32 +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 = "MIT"
15
-
16
- spec.required_ruby_version = ">= 3.2.0"
17
-
18
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
- spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = spec.homepage
21
-
22
- spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
23
- %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
- end
25
-
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
- spec.require_paths = ["lib"]
29
-
30
- spec.add_dependency("json_rpc_handler", "~> 0.1")
31
- spec.add_dependency("json-schema", ">= 4.1")
32
- end