mcp 0.2.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.
@@ -50,7 +50,7 @@ class MCPHTTPClient
50
50
  },
51
51
  })
52
52
  puts "Response: #{JSON.pretty_generate(result)}"
53
- puts
53
+
54
54
  result
55
55
  end
56
56
 
@@ -58,7 +58,7 @@ class MCPHTTPClient
58
58
  puts "=== Sending ping ==="
59
59
  result = send_request("ping")
60
60
  puts "Response: #{JSON.pretty_generate(result)}"
61
- puts
61
+
62
62
  result
63
63
  end
64
64
 
@@ -66,7 +66,7 @@ class MCPHTTPClient
66
66
  puts "=== Listing tools ==="
67
67
  result = send_request("tools/list")
68
68
  puts "Response: #{JSON.pretty_generate(result)}"
69
- puts
69
+
70
70
  result
71
71
  end
72
72
 
@@ -77,7 +77,7 @@ class MCPHTTPClient
77
77
  arguments: arguments,
78
78
  })
79
79
  puts "Response: #{JSON.pretty_generate(result)}"
80
- puts
80
+
81
81
  result
82
82
  end
83
83
 
@@ -85,7 +85,7 @@ class MCPHTTPClient
85
85
  puts "=== Listing prompts ==="
86
86
  result = send_request("prompts/list")
87
87
  puts "Response: #{JSON.pretty_generate(result)}"
88
- puts
88
+
89
89
  result
90
90
  end
91
91
 
@@ -96,7 +96,7 @@ class MCPHTTPClient
96
96
  arguments: arguments,
97
97
  })
98
98
  puts "Response: #{JSON.pretty_generate(result)}"
99
- puts
99
+
100
100
  result
101
101
  end
102
102
 
@@ -104,7 +104,7 @@ class MCPHTTPClient
104
104
  puts "=== Listing resources ==="
105
105
  result = send_request("resources/list")
106
106
  puts "Response: #{JSON.pretty_generate(result)}"
107
- puts
107
+
108
108
  result
109
109
  end
110
110
 
@@ -114,7 +114,7 @@ class MCPHTTPClient
114
114
  uri: uri,
115
115
  })
116
116
  puts "Response: #{JSON.pretty_generate(result)}"
117
- puts
117
+
118
118
  result
119
119
  end
120
120
 
@@ -131,7 +131,6 @@ class MCPHTTPClient
131
131
  response = http.request(request)
132
132
  result = JSON.parse(response.body)
133
133
  puts "Response: #{JSON.pretty_generate(result)}"
134
- puts
135
134
 
136
135
  @session_id = nil
137
136
  result
@@ -140,10 +139,11 @@ end
140
139
 
141
140
  # Main script
142
141
  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
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
147
 
148
148
  client = MCPHTTPClient.new
149
149
 
@@ -61,8 +61,9 @@ server = MCP::Server.new(
61
61
  prompts: [ExamplePrompt],
62
62
  resources: [
63
63
  MCP::Resource.new(
64
- uri: "test_resource",
65
- name: "Test resource",
64
+ uri: "https://test_resource.invalid",
65
+ name: "test-resource",
66
+ title: "Test Resource",
66
67
  description: "Test resource that echoes back the uri as its content",
67
68
  mime_type: "text/plain",
68
69
  ),
@@ -153,15 +154,17 @@ rack_app = Rack::Builder.new do
153
154
  end
154
155
 
155
156
  # 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"
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
165
168
 
166
169
  # Run the server
167
170
  # Use Rackup to run the server
@@ -58,8 +58,9 @@ server = MCP::Server.new(
58
58
  prompts: [ExamplePrompt],
59
59
  resources: [
60
60
  MCP::Resource.new(
61
- uri: "test_resource",
62
- name: "Test resource",
61
+ uri: "https://test_resource.invalid",
62
+ name: "test-resource",
63
+ title: "Test Resource",
63
64
  description: "Test resource that echoes back the uri as its content",
64
65
  mime_type: "text/plain",
65
66
  ),
@@ -85,7 +86,7 @@ server.resources_read_handler do |params|
85
86
  [{
86
87
  uri: params[:uri],
87
88
  mimeType: "text/plain",
88
- text: "Hello, world!",
89
+ text: "Hello, world! URI: #{params[:uri]}",
89
90
  }]
90
91
  end
91
92
 
@@ -92,7 +92,6 @@ def main
92
92
  end
93
93
 
94
94
  puts "=== MCP SSE Test Client ==="
95
- puts ""
96
95
 
97
96
  # Step 1: Initialize session
98
97
  logger.info("Initializing session...")
@@ -134,13 +133,16 @@ def main
134
133
 
135
134
  # Step 3: Interactive menu
136
135
  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: ")
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
144
146
 
145
147
  choice = gets.chomp
146
148
 
@@ -167,19 +169,15 @@ def main
167
169
  else
168
170
  logger.error("Error: #{response[:body]["error"]}")
169
171
  end
170
-
171
172
  when "2"
172
173
  print("Enter message to echo: ")
173
174
  message = gets.chomp
174
175
  make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } })
175
-
176
176
  when "3"
177
177
  make_request(session_id, "tools/list")
178
-
179
178
  when "0"
180
179
  logger.info("Exiting...")
181
180
  break
182
-
183
181
  else
184
182
  puts "Invalid choice"
185
183
  end
@@ -136,37 +136,38 @@ rack_app = Rack::Builder.new do
136
136
  end
137
137
 
138
138
  # Print usage instructions
139
- puts "=== MCP Streaming HTTP Test Server ==="
140
- puts ""
141
- puts "Starting server on http://localhost:9393"
142
- puts ""
143
- puts "Available Tools:"
144
- puts "1. NotificationTool - Returns messages that are sent via SSE when stream is active"
145
- puts "2. echo - Simple echo tool"
146
- puts ""
147
- puts "Testing SSE:"
148
- puts ""
149
- puts "1. Initialize session:"
150
- puts " curl -i http://localhost:9393 \\"
151
- puts ' --json \'{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sse-test","version":"1.0"}}}\''
152
- puts ""
153
- puts "2. Connect SSE stream (use the session ID from step 1):"
154
- puts ' curl -i -N -H "Mcp-Session-Id: YOUR_SESSION_ID" http://localhost:9393'
155
- puts ""
156
- puts "3. In another terminal, test tools (responses will be sent via SSE if stream is active):"
157
- puts ""
158
- puts " Echo tool:"
159
- puts ' curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \\'
160
- puts ' --json \'{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"echo","arguments":{"message":"Hello SSE!"}}}\''
161
- puts ""
162
- puts " Notification tool (with 2 second delay):"
163
- puts ' curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \\'
164
- puts ' --json \'{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"notification_tool","arguments":{"message":"Hello SSE!", "delay": 2}}}\''
165
- puts ""
166
- puts "Note: When an SSE stream is active, tool responses will appear in the SSE stream and the POST request will return {\"accepted\": true}"
167
- puts ""
168
- puts "Press Ctrl+C to stop the server"
169
- puts ""
139
+ puts <<~MESSAGE
140
+ === MCP Streaming HTTP Test Server ===
141
+
142
+ Starting server on http://localhost:9393
143
+
144
+ Available Tools:
145
+ 1. NotificationTool - Returns messages that are sent via SSE when stream is active"
146
+ 2. echo - Simple echo tool
147
+
148
+ Testing SSE:
149
+
150
+ 1. Initialize session:
151
+ curl -i http://localhost:9393 \\
152
+ --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sse-test","version":"1.0"}}}'
153
+
154
+ 2. Connect SSE stream (use the session ID from step 1):"
155
+ curl -i -N -H "Mcp-Session-Id: YOUR_SESSION_ID" http://localhost:9393
156
+
157
+ 3. In another terminal, test tools (responses will be sent via SSE if stream is active):
158
+
159
+ Echo tool:
160
+ curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \\
161
+ --json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"echo","arguments":{"message":"Hello SSE!"}}}'
162
+
163
+ Notification tool (with 2 second delay):
164
+ curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \\
165
+ --json '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"notification_tool","arguments":{"message":"Hello SSE!", "delay": 2}}}'
166
+
167
+ Note: When an SSE stream is active, tool responses will appear in the SSE stream and the POST request will return {"accepted": true}
168
+
169
+ Press Ctrl+C to stop the server
170
+ MESSAGE
170
171
 
171
172
  # Start the server
172
173
  Rackup::Handler.get("puma").run(rack_app, Port: 9393, Host: "localhost")
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Client
5
+ class HTTP
6
+ attr_reader :url
7
+
8
+ def initialize(url:, headers: {})
9
+ @url = url
10
+ @headers = headers
11
+ end
12
+
13
+ def send_request(request:)
14
+ method = request[:method] || request["method"]
15
+ params = request[:params] || request["params"]
16
+
17
+ client.post("", request).body
18
+ rescue Faraday::BadRequestError => e
19
+ raise RequestHandlerError.new(
20
+ "The #{method} request is invalid",
21
+ { method:, params: },
22
+ error_type: :bad_request,
23
+ original_error: e,
24
+ )
25
+ rescue Faraday::UnauthorizedError => e
26
+ raise RequestHandlerError.new(
27
+ "You are unauthorized to make #{method} requests",
28
+ { method:, params: },
29
+ error_type: :unauthorized,
30
+ original_error: e,
31
+ )
32
+ rescue Faraday::ForbiddenError => e
33
+ raise RequestHandlerError.new(
34
+ "You are forbidden to make #{method} requests",
35
+ { method:, params: },
36
+ error_type: :forbidden,
37
+ original_error: e,
38
+ )
39
+ rescue Faraday::ResourceNotFound => e
40
+ raise RequestHandlerError.new(
41
+ "The #{method} request is not found",
42
+ { method:, params: },
43
+ error_type: :not_found,
44
+ original_error: e,
45
+ )
46
+ rescue Faraday::UnprocessableEntityError => e
47
+ raise RequestHandlerError.new(
48
+ "The #{method} request is unprocessable",
49
+ { method:, params: },
50
+ error_type: :unprocessable_entity,
51
+ original_error: e,
52
+ )
53
+ rescue Faraday::Error => e # Catch-all
54
+ raise RequestHandlerError.new(
55
+ "Internal error handling #{method} request",
56
+ { method:, params: },
57
+ error_type: :internal_error,
58
+ original_error: e,
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :headers
65
+
66
+ def client
67
+ require_faraday!
68
+ @client ||= Faraday.new(url) do |faraday|
69
+ faraday.request(:json)
70
+ faraday.response(:json)
71
+ faraday.response(:raise_error)
72
+
73
+ headers.each do |key, value|
74
+ faraday.headers[key] = value
75
+ end
76
+ end
77
+ end
78
+
79
+ def require_faraday!
80
+ require "faraday"
81
+ rescue LoadError
82
+ raise LoadError, "The 'faraday' gem is required to use the MCP client HTTP transport. " \
83
+ "Add it to your Gemfile: gem 'faraday', '>= 2.0'" \
84
+ "See https://rubygems.org/gems/faraday for more details."
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Client
5
+ class Tool
6
+ attr_reader :name, :description, :input_schema, :output_schema
7
+
8
+ def initialize(name:, description:, input_schema:, output_schema: nil)
9
+ @name = name
10
+ @description = description
11
+ @input_schema = input_schema
12
+ @output_schema = output_schema
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/mcp/client.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Client
5
+ # Initializes a new MCP::Client instance.
6
+ #
7
+ # @param transport [Object] The transport object to use for communication with the server.
8
+ # The transport should be a duck type that responds to `send_request`. See the README for more details.
9
+ #
10
+ # @example
11
+ # transport = MCP::Client::HTTP.new(url: "http://localhost:3000")
12
+ # client = MCP::Client.new(transport: transport)
13
+ def initialize(transport:)
14
+ @transport = transport
15
+ end
16
+
17
+ # The user may want to access additional transport-specific methods/attributes
18
+ # So keeping it public
19
+ attr_reader :transport
20
+
21
+ # Returns the list of tools available from the server.
22
+ # Each call will make a new request – the result is not cached.
23
+ #
24
+ # @return [Array<MCP::Client::Tool>] An array of available tools.
25
+ #
26
+ # @example
27
+ # tools = client.tools
28
+ # tools.each do |tool|
29
+ # puts tool.name
30
+ # end
31
+ def tools
32
+ response = transport.send_request(request: {
33
+ jsonrpc: JsonRpcHandler::Version::V2_0,
34
+ id: request_id,
35
+ method: "tools/list",
36
+ })
37
+
38
+ response.dig("result", "tools")&.map do |tool|
39
+ Tool.new(
40
+ name: tool["name"],
41
+ description: tool["description"],
42
+ input_schema: tool["inputSchema"],
43
+ )
44
+ end || []
45
+ end
46
+
47
+ # Calls a tool via the transport layer.
48
+ #
49
+ # @param tool [MCP::Client::Tool] The tool to be called.
50
+ # @param arguments [Object, nil] The arguments to pass to the tool.
51
+ # @return [Object] The result of the tool call, as returned by the transport.
52
+ #
53
+ # @example
54
+ # tool = client.tools.first
55
+ # result = client.call_tool(tool: tool, arguments: { foo: "bar" })
56
+ #
57
+ # @note
58
+ # The exact requirements for `arguments` are determined by the transport layer in use.
59
+ # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
60
+ def call_tool(tool:, arguments: nil)
61
+ response = transport.send_request(request: {
62
+ jsonrpc: JsonRpcHandler::Version::V2_0,
63
+ id: request_id,
64
+ method: "tools/call",
65
+ params: { name: tool.name, arguments: arguments },
66
+ })
67
+
68
+ response.dig("result", "content")
69
+ end
70
+
71
+ private
72
+
73
+ def request_id
74
+ SecureRandom.uuid
75
+ end
76
+
77
+ class RequestHandlerError < StandardError
78
+ attr_reader :error_type, :original_error, :request
79
+
80
+ def initialize(message, request, error_type: :internal_error, original_error: nil)
81
+ super(message)
82
+ @request = request
83
+ @error_type = error_type
84
+ @original_error = original_error
85
+ end
86
+ end
87
+ end
88
+ end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module MCP
4
4
  class Configuration
5
- DEFAULT_PROTOCOL_VERSION = "2024-11-05"
5
+ DEFAULT_PROTOCOL_VERSION = "2025-06-18"
6
+ SUPPORTED_PROTOCOL_VERSIONS = [DEFAULT_PROTOCOL_VERSION, "2025-03-26", "2024-11-05"]
6
7
 
7
8
  attr_writer :exception_reporter, :instrumentation_callback, :protocol_version, :validate_tool_call_arguments
8
9
 
@@ -11,6 +12,10 @@ module MCP
11
12
  @exception_reporter = exception_reporter
12
13
  @instrumentation_callback = instrumentation_callback
13
14
  @protocol_version = protocol_version
15
+ if protocol_version && !SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
16
+ message = "protocol_version must be #{SUPPORTED_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_PROTOCOL_VERSIONS[-1]}"
17
+ raise ArgumentError, message
18
+ end
14
19
  unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
15
20
  raise ArgumentError, "validate_tool_call_arguments must be a boolean"
16
21
  end
data/lib/mcp/prompt.rb CHANGED
@@ -6,6 +6,7 @@ module MCP
6
6
  class << self
7
7
  NOT_SET = Object.new
8
8
 
9
+ attr_reader :title_value
9
10
  attr_reader :description_value
10
11
  attr_reader :arguments_value
11
12
 
@@ -14,12 +15,13 @@ module MCP
14
15
  end
15
16
 
16
17
  def to_h
17
- { name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
18
+ { name: name_value, title: title_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
18
19
  end
19
20
 
20
21
  def inherited(subclass)
21
22
  super
22
23
  subclass.instance_variable_set(:@name_value, nil)
24
+ subclass.instance_variable_set(:@title_value, nil)
23
25
  subclass.instance_variable_set(:@description_value, nil)
24
26
  subclass.instance_variable_set(:@arguments_value, nil)
25
27
  end
@@ -36,6 +38,14 @@ module MCP
36
38
  @name_value || StringUtils.handle_from_class_name(name)
37
39
  end
38
40
 
41
+ def title(value = NOT_SET)
42
+ if value == NOT_SET
43
+ @title_value
44
+ else
45
+ @title_value = value
46
+ end
47
+ end
48
+
39
49
  def description(value = NOT_SET)
40
50
  if value == NOT_SET
41
51
  @description_value
@@ -52,9 +62,10 @@ module MCP
52
62
  end
53
63
  end
54
64
 
55
- def define(name: nil, description: nil, arguments: [], &block)
65
+ def define(name: nil, title: nil, description: nil, arguments: [], &block)
56
66
  Class.new(self) do
57
67
  prompt_name name
68
+ title title
58
69
  description description
59
70
  arguments arguments
60
71
  define_singleton_method(:template) do |args, server_context: nil|
data/lib/mcp/resource.rb CHANGED
@@ -3,21 +3,23 @@
3
3
 
4
4
  module MCP
5
5
  class Resource
6
- attr_reader :uri, :name, :description, :mime_type
6
+ attr_reader :uri, :name, :title, :description, :mime_type
7
7
 
8
- def initialize(uri:, name:, description: nil, mime_type: nil)
8
+ def initialize(uri:, name:, title: nil, description: nil, mime_type: nil)
9
9
  @uri = uri
10
10
  @name = name
11
+ @title = title
11
12
  @description = description
12
13
  @mime_type = mime_type
13
14
  end
14
15
 
15
16
  def to_h
16
17
  {
17
- uri: @uri,
18
- name: @name,
19
- description: @description,
20
- mimeType: @mime_type,
18
+ uri: uri,
19
+ name: name,
20
+ title: title,
21
+ description: description,
22
+ mimeType: mime_type,
21
23
  }.compact
22
24
  end
23
25
  end
@@ -3,21 +3,23 @@
3
3
 
4
4
  module MCP
5
5
  class ResourceTemplate
6
- attr_reader :uri_template, :name, :description, :mime_type
6
+ attr_reader :uri_template, :name, :title, :description, :mime_type
7
7
 
8
- def initialize(uri_template:, name:, description: nil, mime_type: nil)
8
+ def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil)
9
9
  @uri_template = uri_template
10
10
  @name = name
11
+ @title = title
11
12
  @description = description
12
13
  @mime_type = mime_type
13
14
  end
14
15
 
15
16
  def to_h
16
17
  {
17
- uriTemplate: @uri_template,
18
- name: @name,
19
- description: @description,
20
- mimeType: @mime_type,
18
+ uriTemplate: uri_template,
19
+ name: name,
20
+ title: title,
21
+ description: description,
22
+ mimeType: mime_type,
21
23
  }.compact
22
24
  end
23
25
  end
@@ -34,7 +34,13 @@ module MCP
34
34
  end
35
35
  end
36
36
 
37
- def send_notification(notification, session_id: nil)
37
+ def send_notification(method, params = nil, session_id: nil)
38
+ notification = {
39
+ jsonrpc: "2.0",
40
+ method:,
41
+ }
42
+ notification[:params] = params if params
43
+
38
44
  @mutex.synchronize do
39
45
  if session_id
40
46
  # Send to specific session
@@ -102,6 +108,8 @@ module MCP
102
108
 
103
109
  if body["method"] == "initialize"
104
110
  handle_initialization(body_string, body)
111
+ elsif body["method"] == MCP::Methods::NOTIFICATIONS_INITIALIZED
112
+ handle_notification_initialized
105
113
  else
106
114
  handle_regular_request(body_string, session_id)
107
115
  end
@@ -179,6 +187,10 @@ module MCP
179
187
  [200, headers, [response]]
180
188
  end
181
189
 
190
+ def handle_notification_initialized
191
+ [202, {}, []]
192
+ end
193
+
182
194
  def handle_regular_request(body_string, session_id)
183
195
  # If session ID is provided, but not in the sessions hash, return an error
184
196
  if session_id && !@sessions.key?(session_id)