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

  1. A tool call arrives at your MCP server with a user_id in its arguments.
  2. The middleware looks up the Allowly authorization ID for that user via your authorization_id_fn.
  3. It calls POST /v1/check with the authorization ID and the tool name as a single requested scope.
  4. On allow — the tool runs normally.
  5. On deny — the tool is blocked and an error is returned with the reason.
  6. On confirm — the tool is blocked and the confirmation nonce + prompt hint are returned so your client can surface the approval flow.
  7. 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

DecisionWhat the middleware returns
allowTool executes normally
denyisError: true, content contains { decision, reason }
confirmisError: true, content contains { decision, reason, confirm_nonce, confirm_prompt_hint }
escalateisError: 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",
});