Class: Mistri::MCP::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/mcp/client.rb

Overview

A Model Context Protocol client: the initialize handshake, tools/list with pagination, and tools/call, over one of two wires. url: speaks Streamable HTTP on the same persistent transport the providers use; command: spawns a local stdio server with credentials in its environment.

Mistri::MCP::Client.new(url: "https://mcp.linear.app/mcp",
                      token: -> { connection.bearer_token })
Mistri::MCP::Client.new(command: ["npx", "-y", "some-mcp-server"],
                      env: { "API_KEY" => key })

HTTP auth is a headers hash or token: a string or a callable. A callable resolves per request, and a 401 retries once after re-resolving, so a host's refresh logic lives in one lambda. A session the server expires (404 with a session attached) transparently re-initializes, per spec.

One client serializes its calls; parallel tool calls against one server queue rather than interleave.

Constant Summary collapse

PROTOCOL_VERSION =
"2025-11-25"
SUPPORTED_VERSIONS =
%w[2025-11-25 2025-06-18 2025-03-26 2024-11-05].freeze
LOOPBACK =
%w[localhost 127.0.0.1 ::1].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url: nil, command: nil, env: {}, token: nil, headers: {}, client_name: "mistri", open_timeout: 15, read_timeout: 120) ⇒ Client

Returns a new instance of Client.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/mistri/mcp/client.rb', line 33

def initialize(url: nil, command: nil, env: {}, token: nil, headers: {},
               client_name: "mistri", open_timeout: 15, read_timeout: 120)
  if [url, command].compact.length != 1
    raise ConfigurationError, "pass exactly one of url: or command:"
  end

  if url && token && URI(url).scheme == "http" && !LOOPBACK.include?(URI(url).host)
    raise ConfigurationError,
          "refusing to send a bearer token over plain HTTP to #{URI(url).host}"
  end

  @wire = if url
            Wires::Http.new(url: url, token: token, headers: headers,
                            open_timeout: open_timeout, read_timeout: read_timeout)
          else
            Wires::Stdio.new(command: command, env: env, read_timeout: read_timeout)
          end
  @client_name = client_name
  @mutex = Mutex.new
  @serial = 0
  @connected = false
end

Instance Attribute Details

#server_infoObject (readonly)

Returns the value of attribute server_info.



31
32
33
# File 'lib/mistri/mcp/client.rb', line 31

def server_info
  @server_info
end

Instance Method Details

#call_tool(name, arguments = {}) ⇒ Object



65
66
67
# File 'lib/mistri/mcp/client.rb', line 65

def call_tool(name, arguments = {})
  @mutex.synchronize { request("tools/call", { name: name, arguments: arguments }) }
end

#closeObject



74
75
76
77
78
# File 'lib/mistri/mcp/client.rb', line 74

def close
  @wire.close
  @connected = false
  nil
end

#connectObject



69
70
71
72
# File 'lib/mistri/mcp/client.rb', line 69

def connect
  @mutex.synchronize { ensure_connected }
  self
end

#tools(refresh: false) ⇒ Object

The server's tools as it describes them: hashes with "name", "description", and "inputSchema". Cached; refresh: true re-lists.



58
59
60
61
62
63
# File 'lib/mistri/mcp/client.rb', line 58

def tools(refresh: false)
  @mutex.synchronize do
    @tools = nil if refresh
    @tools ||= list_tools
  end
end