MCP Tools
This page explains how Quater exposes selected backend operations as MCP tools for AI agents.
Prerequisites
Read the Quickstart and create an app with an AuthConfig covering "mcp". You should decide the handler-level authorization rules before exposing sensitive tools.
What MCP Means Here
MCP (Model Context Protocol) is a protocol that lets AI clients discover tools and call them with structured arguments. Quater exposes route-backed tools over HTTP so an agent can call backend operations without a separate tool server. Read the protocol background at modelcontextprotocol.io.
The important idea is directness with boundaries. An agent should not need to click through a frontend to fetch an order, update a workflow, or run an approved backend action. It should call a described tool with typed inputs, and your app should decide whether that call is allowed.
Quater does not make every route a tool. You opt in with tool=True, write a description, and protect the MCP surface with an AuthConfig covering "mcp".
A Runnable Tool
from quater import AuthConfig, AuthContext, Quater, Request
async def authenticate(ctx: Request) -> AuthContext | None:
if ctx.headers.get("authorization") != "Bearer mcp-token":
return None
return AuthContext(subject="agent_123")
app = Quater(
auth=[AuthConfig(authenticate, surfaces=["mcp"])],
mcp_allowed_origins=["https://client.example"],
)
@app.get(
"/orders/{order_id}",
tool=True,
description="Fetch one order by id.",
)
async def get_order(order_id: str, request: Request) -> dict[str, object]:
assert request.auth is not None
return {"order_id": order_id, "subject": request.auth.subject}The route still works as HTTP:
GET /orders/ord_1001It also appears in MCP tools/list.
Auth Layering
MCP auth and authorization have separate jobs:
- The
mcpAuthConfigprotectsinitialize,tools/list,tools/call, and/mcp/docs. - Handler or service authorization decides whether the authenticated caller may run the selected operation.
No MCP AuthConfig means public MCP
If no AuthConfig covers "mcp", Quater does not challenge MCP requests. The MCP surface is public, including /mcp, /mcp/docs, initialize, tools/list, and tools/call. tools/list reveals every route exposed with tool=True, and tools/call can run those tools. Quater logs the exposed tool names at startup so the public surface is visible.
Quater checks MCP auth on each HTTP request. It does not authenticate once during initialize and then reuse that result for later tool calls.
If the mcp authenticator returns None, the call fails before tool dispatch. Authorization inside the handler can still reject the selected operation after arguments are bound.
Endpoint And Client Config
The MCP endpoint is fixed:
POST /mcpFor a hosted app at https://api.example.com, configure the MCP URL as:
https://api.example.com/mcpWhen the MCP surface is protected, bearer auth must go on every HTTP request, not only on initialize:
{
"mcpServers": {
"store": {
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer mcp-token"
}
}
}
}initialize is not a login. Quater does not create a server-side session from it. If the protected surface's token expires, the next request fails with 401 Unauthorized.
Keep authorization close to the data
The mcp AuthConfig decides whether the caller may use the MCP surface. Authorization decides whether that caller can run the selected backend operation. Keep roles, ownership, and other domain checks in the handler or service.
Request Flow
Browser MCP clients also need mcp_allowed_origins. If you omit it and CORS has exact origins, Quater reuses those exact origins. A CORS wildcard does not allow browser-based MCP calls.
Tool Schemas
Quater generates inputSchema from the route's path, query, header, cookie, and body parameters. Form fields appear as scalar tool arguments. It excludes injected Resource parameters because those values belong to the app, not the caller.
For a tool call, Quater builds the handler request from the MCP tool arguments. Handler-level Header() and Cookie() parameters only see values passed in params.arguments. The outer MCP transport headers, such as Authorization, Cookie, Content-Length, Mcp-Protocol-Version, Origin, and request ids, are used by the MCP endpoint and are not copied into the handler request. Use request.auth for the authenticated caller.
{
"name": "get_order",
"description": "Fetch one order by id.",
"inputSchema": {
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false
}
}Descriptions are required for tool=True routes. Use description= or the first line of the handler docstring. Tool descriptions are visible to agents, so write them as instructions about when the tool should be used.
Routes with File parameters cannot be MCP tools in this release. File upload through an agent needs a separate file-reference design and tighter trust rules, so Quater fails at startup instead of exposing a tool schema that cannot run safely.
Approval-Protected Tools
Use needs_approval=True when auth alone should not run an operation.
from quater import ApprovalRequest, AuthConfig, AuthContext, Quater
async def authenticate(ctx: Request) -> AuthContext | None:
if ctx.headers.get("authorization") != "Bearer mcp-token":
return None
return AuthContext(subject="agent_123")
async def approve_action(ctx: ApprovalRequest) -> bool:
return ctx.token == "approve-ord_1001"
app = Quater(auth=[AuthConfig(authenticate, surfaces=["mcp"])], action_approval=approve_action)
@app.patch(
"/orders/{order_id}/status",
tool=True,
needs_approval=True,
description="Update an order status.",
)
async def update_order_status(order_id: str, status: str) -> dict[str, str]:
return {"order_id": order_id, "status": status}Send the approval token in MCP _meta:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "update_order_status",
"arguments": {
"order_id": "ord_1001",
"status": "shipped"
},
"_meta": {
"approvalToken": "approve-ord_1001"
}
}
}If the token is missing, Quater returns a JSON-RPC error with data.code == "approval_required" and includes arguments_hash.
MCP Docs
GET /mcp/docs renders a human-readable page with:
- tool name
- description
- route method and path
- pretty JSON input and output schema
- example
tools/callpayload
MCP clients should use tools/list. Humans should use /mcp/docs.
Disable the page while keeping /mcp available:
app = Quater(auth=[AuthConfig(authenticate, surfaces=["mcp"])], mcp_docs_path=None)Auditing
Pass mcp_audit to receive redacted tool-call events:
from quater import ToolAuditEvent
async def audit(event: ToolAuditEvent) -> None:
print(event.tool_name, event.subject, event.success)
app = Quater(auth=[AuthConfig(authenticate, surfaces=["mcp"])], mcp_audit=audit)Quater redacts argument values before the hook sees them. If the audit hook raises, Quater returns a JSON-RPC internal error for that tool call. It does not silently hide audit failures.
What Can Go Wrong
No AuthConfig covers the 'mcp' surface; exposed routes are public: ... (startup warning) : At least one MCP tool is exposed while the mcp surface has no AuthConfig, so /mcp, /mcp/docs, initialize, tools/list, and tools/call are available without authentication. Cover the surface with AuthConfig(fn, surfaces=["mcp"]), or keep it public deliberately.
Invalid MCP Origin : Add the browser origin to mcp_allowed_origins.
Unsupported protocol version : Send a supported MCP-Protocol-Version header or omit it and let Quater use its default.
Tool not found : Check the route has tool=True and a description.
Routes with File parameters cannot be exposed as MCP tools : Keep upload routes HTTP-only today, or split the upload from the operation an agent should call.
approval_required : Send _meta.approvalToken or remove needs_approval=True from that route.
Also See
- Actions and CLI: use the same approval hook for CLI.
- Security: review MCP origin validation and token rules.
- Testing: test tools with
client.mcp. - Reference: Auth: inspect
AuthConfig,AuthContext, andApprovalRequest.