MCP Middleware
Drop-in middleware for MCP servers. Intercepts every tool call, runs a /check against the user's authorization, and blocks or allows the invocation based on the decision.
# Python
pip install "allowly[mcp]"
# TypeScript
npm install @allowly/sdk @modelcontextprotocol/sdk
How it works
- A tool call arrives at your MCP server with a
user_idin its arguments. - The middleware looks up the Allowly authorization ID for that user via your
authorization_id_fn. - It calls
POST /v1/checkwith the authorization ID and the tool name as a single requested scope. - On
allow— the tool runs normally. - On
deny— the tool is blocked and an error is returned with the reason. - On
confirm— the tool is blocked and the confirmation nonce + prompt hint are returned so your client can surface the approval flow. - On
escalate— the tool is blocked and the escalation ID + target are returned so your client can route the approval.
Python — FastMCP
from mcp.server.fastmcp import FastMCP
from allowly import AllowlyMCPMiddleware
mcp = FastMCP("my-agent")
mcp.add_middleware(AllowlyMCPMiddleware(
api_key=os.environ["ALLOWLY_KEY"],
authorization_id_fn=lambda user_id: db.get_authorization_id(user_id),
))
authorization_id_fn is called with the user_id from the tool arguments. It may be sync or async and must return the Allowly authorization ID for that user, or None to deny immediately.
Python — low-level Server
from mcp.server import Server
from allowly import AllowlyMCPMiddleware
server = Server("my-agent")
AllowlyMCPMiddleware.wrap(
server,
api_key=os.environ["ALLOWLY_KEY"],
authorization_id_fn=lambda user_id: db.get_authorization_id(user_id),
)
TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { AllowlyMCPMiddleware } from "@allowly/sdk/mcp";
const mcp = new McpServer({ name: "my-agent", version: "1.0" });
const allowly = new AllowlyMCPMiddleware({
apiKey: process.env.ALLOWLY_KEY!,
authorizationIdFn: (userId) => db.getAuthorizationId(userId),
});
allowly.attach(mcp.server);
Decision behavior
| Decision | What the middleware returns |
|---|---|
allow | Tool executes normally |
deny | isError: true, content contains { decision, reason } |
confirm | isError: true, content contains { decision, reason, confirm_nonce, confirm_prompt_hint } |
escalate | isError: true, content contains { decision, reason, escalation_id, escalation_to, escalation_expires_at } |
On confirm, your MCP client should surface the prompt hint to the user, collect approval, call POST /v1/confirmations/{nonce}, then retry the tool call — the next check will return allow.
On escalate, your MCP client should route the request to the approver shown by escalation_to, call POST /v1/escalations/{escalation_id}/resolve, then retry the tool call if the escalation was approved.
Handling confirm in your client
import json
result = await server.call_tool("send_email", {"user_id": user_id, ...})
if result.get("decision") == "confirm":
nonce = result["confirm_nonce"]
hint = result["confirm_prompt_hint"]
approved = await ask_user(hint) # your UI / chat loop
await allowly_client.confirmations.approve(nonce, approved=approved)
if approved:
result = await server.call_tool("send_email", {"user_id": user_id, ...})
Self-hosted
AllowlyMCPMiddleware(
api_key=os.environ["ALLOWLY_KEY"],
authorization_id_fn=...,
base_url="https://allowly.your-domain.com",
)
new AllowlyMCPMiddleware({
apiKey: process.env.ALLOWLY_KEY!,
authorizationIdFn: ...,
baseUrl: "https://allowly.your-domain.com",
});