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
-
.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.
- .bridge(client, spec, prefix: nil, gate: false) ⇒ Object
- .convert(block) ⇒ Object
- .resource_text(resource) ⇒ Object
-
.tools(client, allow: nil, deny: [], prefix: nil, needs_approval: false, gates: {}) ⇒ Object
The server's tools as Mistri tools.
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 |