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:
$ bin/rails generate mistri:mcp McpConnectionEach 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.