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. Remote URLs default to public HTTPS. allow_non_public: is consulted only for otherwise blocked addresses and plain HTTP remains loopback-only. max_record_bytes bounds one JSON body, SSE line, or stdio record without imposing a lifetime limit on an SSE stream.

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

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, allow_non_public: nil, max_tool_pages: 100, max_tools: 10_000, max_record_bytes: DEFAULT_MAX_RECORD_BYTES) ⇒ 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
55
56
57
58
59
60
# 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,
               allow_non_public: nil, max_tool_pages: 100, max_tools: 10_000,
               max_record_bytes: DEFAULT_MAX_RECORD_BYTES)
  if [url, command].compact.length != 1
    raise ConfigurationError, "pass exactly one of url: or command:"
  end

  validate_limit(:max_tool_pages, max_tool_pages)
  validate_limit(:max_tools, max_tools)
  validate_limit(:max_record_bytes, max_record_bytes)

  @wire = if url
            Wires::Http.new(url: url, token: token, headers: headers,
                            open_timeout: open_timeout, read_timeout: read_timeout,
                            allow_non_public: allow_non_public,
                            max_record_bytes: max_record_bytes)
          else
            Wires::Stdio.new(command: command, env: env, read_timeout: read_timeout,
                             max_record_bytes: max_record_bytes)
          end
  @client_name = client_name
  @mutex = Mutex.new
  @serial = 0
  @connected = false
  @max_tool_pages = max_tool_pages
  @max_tools = max_tools
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



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

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

#closeObject



80
81
82
83
# File 'lib/mistri/mcp/client.rb', line 80

def close
  @mutex.synchronize { reset_wire }
  nil
end

#connectObject



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

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.



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

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