diff --git a/lib/mcp/client.rb b/lib/mcp/client.rb index 90095d3..2ee5f7c 100644 --- a/lib/mcp/client.rb +++ b/lib/mcp/client.rb @@ -6,17 +6,64 @@ class Client # # @param transport [Object] The transport object to use for communication with the server. # The transport should be a duck type that responds to `send_request`. See the README for more details. + # @param client_info [Hash] Information about the client (name and version) + # @param protocol_version [String] The MCP protocol version to use # # @example # transport = MCP::Client::HTTP.new(url: "http://localhost:3000") - # client = MCP::Client.new(transport: transport) - def initialize(transport:) + # client = MCP::Client.new( + # transport: transport, + # client_info: { name: "my-client", version: "1.0.0" } + # ) + def initialize(transport:, client_info: nil, protocol_version: "2025-06-18") @transport = transport + @client_info = client_info || { name: "ruby-mcp-client", version: MCP::VERSION } + @protocol_version = protocol_version + @initialized = false + @session_info = nil end # The user may want to access additional transport-specific methods/attributes # So keeping it public - attr_reader :transport + attr_reader :transport, :client_info, :protocol_version, :session_info + + # Initializes the MCP session with the server. + # This method performs the MCP handshake: sends an initialize request, + # receives session information, and sends the initialized notification. + # + # @return [Hash] The session information including server_info + # + # @example + # client.init + # # => { server_info: { name: "my-server", version: "1.0.0" }, ... } + def init + return @session_info if @initialized + + response = transport.send_request(request: { + jsonrpc: JsonRpcHandler::Version::V2_0, + id: request_id, + method: "initialize", + params: { + protocolVersion: protocol_version, + capabilities: {}, + clientInfo: { + name: client_info[:name] || client_info["name"], + version: client_info[:version] || client_info["version"], + }, + }, + }) + + @session_info = response.dig("result") + @initialized = true + + # Send the initialized notification + transport.send_notification(notification: { + jsonrpc: JsonRpcHandler::Version::V2_0, + method: "notifications/initialized", + }) + + @session_info + end # Returns the list of tools available from the server. # Each call will make a new request – the result is not cached. @@ -29,6 +76,8 @@ def initialize(transport:) # puts tool.name # end def tools + init unless @initialized + response = transport.send_request(request: { jsonrpc: JsonRpcHandler::Version::V2_0, id: request_id, @@ -49,6 +98,8 @@ def tools # # @return [Array] An array of available resources. def resources + init unless @initialized + response = transport.send_request(request: { jsonrpc: JsonRpcHandler::Version::V2_0, id: request_id, @@ -73,6 +124,8 @@ def resources # The exact requirements for `arguments` are determined by the transport layer in use. # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details. def call_tool(tool:, arguments: nil) + init unless @initialized + transport.send_request(request: { jsonrpc: JsonRpcHandler::Version::V2_0, id: request_id, @@ -86,6 +139,8 @@ def call_tool(tool:, arguments: nil) # @param uri [String] The URI of the resource to read. # @return [Array] An array of resource contents (text or blob). def read_resource(uri:) + init unless @initialized + response = transport.send_request(request: { jsonrpc: JsonRpcHandler::Version::V2_0, id: request_id, diff --git a/lib/mcp/client/http.rb b/lib/mcp/client/http.rb index 7b065a8..da4ee23 100644 --- a/lib/mcp/client/http.rb +++ b/lib/mcp/client/http.rb @@ -3,18 +3,29 @@ module MCP class Client class HTTP - attr_reader :url + attr_reader :url, :session_id def initialize(url:, headers: {}) @url = url @headers = headers + @session_id = nil end def send_request(request:) method = request[:method] || request["method"] params = request[:params] || request["params"] - client.post("", request).body + # Update session header if we have one + update_session_header! + + response = client.post("", request) + + # Store session ID from response headers if present + if response.headers["Mcp-Session-Id"] + @session_id = response.headers["Mcp-Session-Id"] + end + + response.body rescue Faraday::BadRequestError => e raise RequestHandlerError.new( "The #{method} request is invalid", @@ -76,6 +87,17 @@ def client end end + # Updates the session header on the Faraday client + def update_session_header! + return unless @client + + if @session_id + @client.headers["Mcp-Session-Id"] = @session_id + else + @client.headers.delete("Mcp-Session-Id") + end + end + def require_faraday! require "faraday" rescue LoadError