Module: Mistri::MCP
- Defined in:
- lib/mistri/mcp.rb,
lib/mistri/mcp/oauth.rb,
lib/mistri/mcp/wires.rb,
lib/mistri/mcp/client.rb,
lib/mistri/mcp/egress.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: Egress, OAuth, Wires Classes: Client, Error, SessionExpired, UnsafeURLError, WireError
Constant Summary collapse
- EMPTY_INPUT_SCHEMA =
{ "type" => "object", "properties" => {}.freeze, "additionalProperties" => false }.freeze
Class Method Summary collapse
-
.answer(result) ⇒ Object
An MCP result becomes model-readable content: text joins, images ride as image blocks, and isError remains structured failure data end to end.
- .bridge(client, spec, prefix: nil, gate: false, complete_argument_validator: nil) ⇒ Object
- .convert(block) ⇒ Object
- .resource_text(resource) ⇒ Object
-
.tools(client, allow: nil, deny: [], prefix: nil, needs_approval: false, gates: {}, complete_argument_validator: nil) ⇒ 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 remains structured failure data end to end.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/mistri/mcp.rb', line 85 def answer(result) blocks = Array(result["content"]).map { |block| convert(block) } if result["isError"] text_blocks = blocks.grep(String) if result["structuredContent"] structured = JSON.generate(result["structuredContent"]) text_blocks << structured unless text_blocks.include?(structured) end text = text_blocks.join("\n") content = "MCP tool error: #{text.empty? ? "unknown error" : text}" non_text = blocks.grep_v(String) content = [content, *non_text] unless non_text.empty? return ToolResult.new(content:, error: true) 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, complete_argument_validator: nil) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/mistri/mcp.rb', line 62 def bridge(client, spec, prefix: nil, gate: false, complete_argument_validator: nil) remote = spec.fetch("name") local = prefix ? "#{prefix}__#{remote}" : remote if spec.key?("inputSchema") && spec["inputSchema"].nil? raise ConfigurationError, "inputSchema must be an object, not null" end raw_schema = spec.fetch("inputSchema", EMPTY_INPUT_SCHEMA) input_schema = Schema.validate_mcp!( raw_schema, complete: !complete_argument_validator.nil? ) Tool.define(local, spec["description"].to_s, input_schema: input_schema, needs_approval: gate, complete_argument_validator: complete_argument_validator) do |args| answer(client.call_tool(remote, args || {})) end rescue ConfigurationError => e raise ConfigurationError, "MCP tool #{remote.inspect}: #{e.}" end |
.convert(block) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/mistri/mcp.rb', line 107 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
119 120 121 |
# File 'lib/mistri/mcp.rb', line 119 def resource_text(resource) resource["text"] || "[resource: #{resource["uri"]}]" end |
.tools(client, allow: nil, deny: [], prefix: nil, needs_approval: false, gates: {}, complete_argument_validator: nil) ⇒ 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).
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/mistri/mcp.rb', line 51 def tools(client, allow: nil, deny: [], prefix: nil, needs_approval: false, gates: {}, complete_argument_validator: nil) 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), complete_argument_validator: complete_argument_validator) end end |