Module: Mistri::MCP

Defined in:
lib/mistri/mcp.rb,
lib/mistri/mcp/oauth.rb,
lib/mistri/mcp/wires.rb,
lib/mistri/mcp/client.rb

Overview

Bridge Model Context Protocol servers into Mistri tools: list a server's tools, hand them to an agent, and everything the harness already does composes — approval gates on third-party write tools, retries, sub-agent pools, the ui channel.

client = Mistri::MCP::Client.new(url: "https://mcp.linear.app/mcp",
                               token: -> { connection.fresh_token })
agent = Mistri.agent("claude-opus-4-8",
                   tools: Mistri::MCP.tools(client, prefix: "linear"))

The bridge is duck-typed: any client responding to tools (an array of "description", "inputSchema" hashes) and call_tool(name, args) bridges the same way, so the official mcp gem's client plugs in too.

Defined Under Namespace

Modules: OAuth, Wires Classes: Client, Error, SessionExpired

Class Method Summary collapse

Class Method Details

.answer(result) ⇒ Object

An MCP result becomes model-readable content: text joins, images ride as image blocks, and isError answers in band so the model can react.



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/mistri/mcp.rb', line 62

def answer(result)
  blocks = Array(result["content"]).map { |block| convert(block) }
  if result["isError"]
    text = blocks.grep(String).join("\n")
    return "MCP tool error: #{text.empty? ? "unknown error" : text}"
  end
  if blocks.empty? && result["structuredContent"]
    return JSON.generate(result["structuredContent"])
  end
  return blocks.join("\n") if blocks.all?(String)

  blocks
end

.bridge(client, spec, prefix: nil, gate: false) ⇒ Object



50
51
52
53
54
55
56
57
58
# File 'lib/mistri/mcp.rb', line 50

def bridge(client, spec, prefix: nil, gate: false)
  remote = spec.fetch("name")
  local = prefix ? "#{prefix}__#{remote}" : remote
  Tool.define(local, spec["description"].to_s,
              input_schema: spec["inputSchema"] || Tool::EMPTY_SCHEMA,
              needs_approval: gate) do |args|
    answer(client.call_tool(remote, args || {}))
  end
end

.convert(block) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
# File 'lib/mistri/mcp.rb', line 76

def convert(block)
  case block["type"]
  when "text" then block["text"].to_s
  when "image"
    Content::Image.from_bytes(block["data"].to_s.unpack1("m"),
                              mime_type: block["mimeType"] || "image/png")
  when "resource" then resource_text(block["resource"] || {})
  when "resource_link" then "[resource: #{block["uri"]}]"
  else "[unsupported #{block["type"]} content]"
  end
end

.resource_text(resource) ⇒ Object



88
89
90
# File 'lib/mistri/mcp.rb', line 88

def resource_text(resource)
  resource["text"] || "[resource: #{resource["uri"]}]"
end

.tools(client, allow: nil, deny: [], prefix: nil, needs_approval: false, gates: {}) ⇒ Object

The server's tools as Mistri tools. allow/deny filter by remote name, prefix namespaces local names ("linear__create_issue") against collisions, and gates marks tools needing human approval (gates: { "create_issue" => true }, or needs_approval: for all).



41
42
43
44
45
46
47
48
# File 'lib/mistri/mcp.rb', line 41

def tools(client, allow: nil, deny: [], prefix: nil, needs_approval: false, gates: {})
  listed = client.tools
  listed = listed.select { |tool| allow.include?(tool["name"]) } if allow
  listed = listed.reject { |tool| deny.include?(tool["name"]) }
  listed.map do |tool|
    bridge(client, tool, prefix: prefix, gate: gates.fetch(tool["name"], needs_approval))
  end
end