MCP

Any Model Context Protocol server’s tools can plug into an agent. The client speaks Streamable HTTP with zero new dependencies. Auth is a token string, or a lambda that re-resolves once on a 401 so refresh logic lives in one place.

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

prefix: namespaces the tools, allow: / deny: filter them, and gates: puts a human approval in front of the risky ones, exactly like a native tool.

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)

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: "Linear", 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: "linear"))

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.