MCP

Any Model Context Protocol server’s tools can plug into an agent. A travel agent that books on a partner airline just points at the airline’s MCP server. The client speaks Streamable HTTP with zero new dependencies, negotiating the current MCP protocol revision (2025-11-25) down to whatever an older server supports. Auth is a token string, or a lambda that re-resolves once on a 401 so refresh logic lives in one place.

airline = Mistri::MCP::Client.new(
url: "https://mcp.flyacme.com/mcp",
token: -> { connection.bearer_token },
)
tools = Mistri::MCP.tools(airline, prefix: "acme",
gates: { "book_flight" => true })
agent = Mistri.agent("claude-opus-4-8", tools: tools)

prefix: namespaces the tools (acme__book_flight), allow: / deny: filter them by remote name, and gates: puts a human approval in front of the risky ones, exactly like a native tool. A gated third-party write tool parks the run before the server ever sees the call.

Names must not collide

Mistri::MCP.tools lists the server’s tools when it bridges them, so every name is known before the run starts. mistri refuses duplicate tool names at Mistri.agent with a ConfigurationError, which is exactly what you want the moment two servers both expose a search. Give each a prefix: and the collision is gone:

tools = Mistri::MCP.tools(airline, prefix: "acme") +
Mistri::MCP.tools(hotels, prefix: "stay")

That first listing is cached on the client. If a server’s tools change while your process is up, re-list with client.tools(refresh: true).

Local servers and browsers

Local stdio servers spawn as child processes with credentials in their environment. That is the whole “give the agent a browser” story:

browser = Mistri::MCP::Client.new(
command: ["npx", "-y", "@playwright/mcp@latest",
"--browser", "chrome", "--headless"],
)
browser_tools = Mistri::MCP.tools(
browser,
allow: %w[browser_navigate browser_snapshot],
)
agent = Mistri.agent("claude-opus-4-8", tools: browser_tools)

The HTTP wire refuses to send a bearer token over plain HTTP to anything but loopback, so a mistyped http:// URL fails loudly instead of leaking a credential.

OAuth, as your application

For user-connected servers, generate a connection model in Rails and name it whatever you like:

Terminal window
$ bin/rails generate mistri:mcp McpConnection

Each row is one server connection carrying its own OAuth state and encrypted tokens:

connection, authorize_url = McpConnection.connect(
name: "FlyAcme", url: params[:url],
client_name: "YourApp", redirect_uri: mcp_callback_url,
)
# redirect the user; then, in the callback:
connection = McpConnection.complete(state: params[:state],
code: params[:code])
agent = Mistri.agent("claude-opus-4-8",
tools: connection.tools(prefix: "acme"))

The services underneath (Mistri::MCP::OAuth.start, .complete, .refresh) are storage-agnostic, so the same flow works from a controller, a GraphQL mutation, or a job. Registration happens as your application, never as the harness: client_name: is yours.