Class: Clacky::Mcp::Client
- Inherits:
-
Object
- Object
- Clacky::Mcp::Client
- Defined in:
- lib/clacky/mcp/client.rb
Overview
JSON-RPC 2.0 client for a single MCP server.
Lifecycle: open transport on #start, send initialize handshake, then any
number of tools/list / tools/call requests, then #stop closes the
transport. Transport is selected by spec: "stdio" (default) or "http".
Defined Under Namespace
Classes: McpError, ProtocolError
Constant Summary collapse
- TransportError =
Mcp::Transport::TransportError
- DEFAULT_TIMEOUT =
30- INIT_TIMEOUT =
15- PROTOCOL_VERSION =
"2024-11-05"
Instance Attribute Summary collapse
-
#last_used_at ⇒ Object
readonly
Returns the value of attribute last_used_at.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#server_info ⇒ Object
readonly
Returns the value of attribute server_info.
-
#started_at ⇒ Object
readonly
Returns the value of attribute started_at.
-
#tools ⇒ Object
readonly
Returns the value of attribute tools.
Class Method Summary collapse
-
.from_spec(name, spec) ⇒ Object
Build a Client from an mcp.json spec hash.
Instance Method Summary collapse
- #call_tool(tool_name, arguments = {}) ⇒ Object
-
#initialize(name:, transport: nil, command: nil, args: [], env: {}, cwd: nil) ⇒ Client
constructor
A new instance of Client.
- #start ⇒ Object
- #started? ⇒ Boolean
- #stderr_tail(bytes: 4096) ⇒ Object
- #stop ⇒ Object
- #tool_definitions ⇒ Object
Constructor Details
#initialize(name:, transport: nil, command: nil, args: [], env: {}, cwd: nil) ⇒ Client
Returns a new instance of Client.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/clacky/mcp/client.rb', line 77 def initialize(name:, transport: nil, command: nil, args: [], env: {}, cwd: nil) @name = name @transport = transport || StdioTransport.new(name: name, command: command, args: args, env: env, cwd: cwd) @pending = {} @next_id = 0 @lock = Monitor.new @started = false @tools = [] @server_info = nil @started_at = nil @last_used_at = nil @transport. do |msg| if msg["__transport_closed__"] @lock.synchronize do @pending.each_value { |q| q.push({ "error" => { "code" => -32000, "message" => "transport closed" } }) } @pending.clear end next end id = msg["id"] if id && (queue = @lock.synchronize { @pending.delete(id) }) queue.push(msg) end end end |
Instance Attribute Details
#last_used_at ⇒ Object (readonly)
Returns the value of attribute last_used_at.
31 32 33 |
# File 'lib/clacky/mcp/client.rb', line 31 def last_used_at @last_used_at end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
31 32 33 |
# File 'lib/clacky/mcp/client.rb', line 31 def name @name end |
#server_info ⇒ Object (readonly)
Returns the value of attribute server_info.
31 32 33 |
# File 'lib/clacky/mcp/client.rb', line 31 def server_info @server_info end |
#started_at ⇒ Object (readonly)
Returns the value of attribute started_at.
31 32 33 |
# File 'lib/clacky/mcp/client.rb', line 31 def started_at @started_at end |
#tools ⇒ Object (readonly)
Returns the value of attribute tools.
31 32 33 |
# File 'lib/clacky/mcp/client.rb', line 31 def tools @tools end |
Class Method Details
.from_spec(name, spec) ⇒ Object
Build a Client from an mcp.json spec hash. Recognized fields:
stdio: command (required), args, env, cwd
http: type: "http", url (required), headers, auth
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/clacky/mcp/client.rb', line 37 def self.from_spec(name, spec) type = (spec["type"] || (spec["url"] ? "http" : "stdio")).to_s case type when "stdio" new( name: name, transport: StdioTransport.new( name: name, command: spec["command"], args: Array(spec["args"]), env: spec["env"] || {}, cwd: spec["cwd"] ) ) when "http", "streamable-http" oauth_config = OAuth::Config.from_server_spec(spec) = nil if oauth_config.enabled? store = OAuth::CredentialStore.new(server_name: name) manager = OAuth::AuthorizationManager.new( server_name: name, config: oauth_config, store: store ) = OAuth::Session.new(store: store, manager: manager) end new( name: name, transport: HttpTransport.new( name: name, url: spec["url"], headers: spec["headers"] || {}, authorization: ) ) else raise McpError, "unsupported MCP transport type '#{type}' for server '#{name}'" end end |
Instance Method Details
#call_tool(tool_name, arguments = {}) ⇒ Object
153 154 155 156 157 |
# File 'lib/clacky/mcp/client.rb', line 153 def call_tool(tool_name, arguments = {}) ensure_started! @last_used_at = Time.now request("tools/call", { name: tool_name, arguments: arguments || {} }) end |
#start ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/clacky/mcp/client.rb', line 111 def start already_started = false @lock.synchronize do if @started already_started = true else @transport.start end end return self if already_started handshake fetch_tools @lock.synchronize do @started = true @started_at = Time.now @last_used_at = @started_at end self end |
#started? ⇒ Boolean
107 108 109 |
# File 'lib/clacky/mcp/client.rb', line 107 def started? @started end |
#stderr_tail(bytes: 4096) ⇒ Object
159 160 161 |
# File 'lib/clacky/mcp/client.rb', line 159 def stderr_tail(bytes: 4096) @transport.stderr_tail(bytes: bytes) end |
#stop ⇒ Object
133 134 135 136 137 138 |
# File 'lib/clacky/mcp/client.rb', line 133 def stop @lock.synchronize do @transport.stop rescue nil @started = false end end |
#tool_definitions ⇒ Object
140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/clacky/mcp/client.rb', line 140 def tool_definitions @tools.map do |t| { type: "function", function: { name: t["name"], description: t["description"].to_s, parameters: t["inputSchema"] || { type: "object", properties: {} } } } end end |