mcp 0.2.0 → 0.4.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
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
4
  require "mcp"
5
- require "mcp/server/transports/streamable_http_transport"
6
5
  require "rack"
7
6
  require "rackup"
8
7
  require "json"
@@ -61,8 +60,9 @@ server = MCP::Server.new(
61
60
  prompts: [ExamplePrompt],
62
61
  resources: [
63
62
  MCP::Resource.new(
64
- uri: "test_resource",
65
- name: "Test resource",
63
+ uri: "https://test_resource.invalid",
64
+ name: "test-resource",
65
+ title: "Test Resource",
66
66
  description: "Test resource that echoes back the uri as its content",
67
67
  mime_type: "text/plain",
68
68
  ),
@@ -153,15 +153,17 @@ rack_app = Rack::Builder.new do
153
153
  end
154
154
 
155
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"
156
+ puts <<~MESSAGE
157
+ Starting MCP HTTP server on http://localhost:9292
158
+ Use POST requests to initialize and send JSON-RPC commands
159
+ Example initialization:
160
+ 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"}}}'
161
+
162
+ The server will return a session ID in the Mcp-Session-Id header.
163
+ Use this session ID for subsequent requests.
164
+
165
+ Press Ctrl+C to stop the server
166
+ MESSAGE
165
167
 
166
168
  # Run the server
167
169
  # Use Rackup to run the server
@@ -2,7 +2,6 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
4
  require "mcp"
5
- require "mcp/server/transports/stdio_transport"
6
5
 
7
6
  # Create a simple tool
8
7
  class ExampleTool < MCP::Tool
@@ -58,8 +57,9 @@ server = MCP::Server.new(
58
57
  prompts: [ExamplePrompt],
59
58
  resources: [
60
59
  MCP::Resource.new(
61
- uri: "test_resource",
62
- name: "Test resource",
60
+ uri: "https://test_resource.invalid",
61
+ name: "test-resource",
62
+ title: "Test Resource",
63
63
  description: "Test resource that echoes back the uri as its content",
64
64
  mime_type: "text/plain",
65
65
  ),
@@ -85,7 +85,7 @@ server.resources_read_handler do |params|
85
85
  [{
86
86
  uri: params[:uri],
87
87
  mimeType: "text/plain",
88
- text: "Hello, world!",
88
+ text: "Hello, world! URI: #{params[:uri]}",
89
89
  }]
90
90
  end
91
91
 
@@ -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
@@ -2,7 +2,6 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
4
  require "mcp"
5
- require "mcp/server/transports/streamable_http_transport"
6
5
  require "rack"
7
6
  require "rackup"
8
7
  require "json"
@@ -136,37 +135,38 @@ rack_app = Rack::Builder.new do
136
135
  end
137
136
 
138
137
  # 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 ""
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
170
 
171
171
  # Start the server
172
172
  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,116 @@
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
+ # Returns the list of resources available from the server.
48
+ # Each call will make a new request – the result is not cached.
49
+ #
50
+ # @return [Array<Hash>] An array of available resources.
51
+ def resources
52
+ response = transport.send_request(request: {
53
+ jsonrpc: JsonRpcHandler::Version::V2_0,
54
+ id: request_id,
55
+ method: "resources/list",
56
+ })
57
+
58
+ response.dig("result", "resources") || []
59
+ end
60
+
61
+ # Calls a tool via the transport layer and returns the full response from the server.
62
+ #
63
+ # @param tool [MCP::Client::Tool] The tool to be called.
64
+ # @param arguments [Object, nil] The arguments to pass to the tool.
65
+ # @return [Hash] The full JSON-RPC response from the transport.
66
+ #
67
+ # @example
68
+ # tool = client.tools.first
69
+ # response = client.call_tool(tool: tool, arguments: { foo: "bar" })
70
+ # structured_content = response.dig("result", "structuredContent")
71
+ #
72
+ # @note
73
+ # The exact requirements for `arguments` are determined by the transport layer in use.
74
+ # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
75
+ def call_tool(tool:, arguments: nil)
76
+ transport.send_request(request: {
77
+ jsonrpc: JsonRpcHandler::Version::V2_0,
78
+ id: request_id,
79
+ method: "tools/call",
80
+ params: { name: tool.name, arguments: arguments },
81
+ })
82
+ end
83
+
84
+ # Reads a resource from the server by URI and returns the contents.
85
+ #
86
+ # @param uri [String] The URI of the resource to read.
87
+ # @return [Array<Hash>] An array of resource contents (text or blob).
88
+ def read_resource(uri:)
89
+ response = transport.send_request(request: {
90
+ jsonrpc: JsonRpcHandler::Version::V2_0,
91
+ id: request_id,
92
+ method: "resources/read",
93
+ params: { uri: uri },
94
+ })
95
+
96
+ response.dig("result", "contents") || []
97
+ end
98
+
99
+ private
100
+
101
+ def request_id
102
+ SecureRandom.uuid
103
+ end
104
+
105
+ class RequestHandlerError < StandardError
106
+ attr_reader :error_type, :original_error, :request
107
+
108
+ def initialize(message, request, error_type: :internal_error, original_error: nil)
109
+ super(message)
110
+ @request = request
111
+ @error_type = error_type
112
+ @original_error = original_error
113
+ end
114
+ end
115
+ end
116
+ end
@@ -2,18 +2,32 @@
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
- attr_writer :exception_reporter, :instrumentation_callback, :protocol_version, :validate_tool_call_arguments
8
+ attr_writer :exception_reporter, :instrumentation_callback
8
9
 
9
10
  def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil,
10
11
  validate_tool_call_arguments: true)
11
12
  @exception_reporter = exception_reporter
12
13
  @instrumentation_callback = instrumentation_callback
13
14
  @protocol_version = protocol_version
14
- unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
15
- raise ArgumentError, "validate_tool_call_arguments must be a boolean"
15
+ if protocol_version
16
+ validate_protocol_version!(protocol_version)
16
17
  end
18
+ validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
19
+
20
+ @validate_tool_call_arguments = validate_tool_call_arguments
21
+ end
22
+
23
+ def protocol_version=(protocol_version)
24
+ validate_protocol_version!(protocol_version)
25
+
26
+ @protocol_version = protocol_version
27
+ end
28
+
29
+ def validate_tool_call_arguments=(validate_tool_call_arguments)
30
+ validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
17
31
 
18
32
  @validate_tool_call_arguments = validate_tool_call_arguments
19
33
  end
@@ -78,6 +92,19 @@ module MCP
78
92
 
79
93
  private
80
94
 
95
+ def validate_protocol_version!(protocol_version)
96
+ unless SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
97
+ message = "protocol_version must be #{SUPPORTED_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_PROTOCOL_VERSIONS[-1]}"
98
+ raise ArgumentError, message
99
+ end
100
+ end
101
+
102
+ def validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
103
+ unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
104
+ raise ArgumentError, "validate_tool_call_arguments must be a boolean"
105
+ end
106
+ end
107
+
81
108
  def default_exception_reporter
82
109
  @default_exception_reporter ||= ->(exception, server_context) {}
83
110
  end
data/lib/mcp/content.rb CHANGED
@@ -1,4 +1,3 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -1,20 +1,24 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
5
4
  class Prompt
6
5
  class Argument
7
- attr_reader :name, :description, :required, :arguments
6
+ attr_reader :name, :title, :description, :required
8
7
 
9
- def initialize(name:, description: nil, required: false)
8
+ def initialize(name:, title: nil, description: nil, required: false)
10
9
  @name = name
10
+ @title = title
11
11
  @description = description
12
12
  @required = required
13
- @arguments = arguments
14
13
  end
15
14
 
16
15
  def to_h
17
- { name:, description:, required: }.compact
16
+ {
17
+ name: name,
18
+ title: title,
19
+ description: description,
20
+ required: required,
21
+ }.compact
18
22
  end
19
23
  end
20
24
  end
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP
@@ -1,4 +1,3 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module MCP