Skip to main content

delegus-mcp

Delegus for Python MCP servers. An ASGI middleware checks every MCP tools/call against the calling agent's delegated authority before your tool runs. It works with the official Python MCP SDK's MCPServer over streamable HTTP, with Starlette, and with FastAPI. It follows the same rules as the Node middleware, delegus.mcp() in @delegus/sdk.

pip install delegus-mcp
import os
from mcp.server.mcpserver import MCPServer
from delegus_mcp import DelegusMCPMiddleware

mcp = MCPServer("tickets")

@mcp.tool()
def create_ticket(title: str) -> str:
    return f"created ticket: {title}"

app = DelegusMCPMiddleware(
    mcp.streamable_http_app(),
    api_key=os.environ["DELEGUS_RP_KEY"],
    public_base_url="https://tools.example.com",
)

Run it with any ASGI server, for example uvicorn server:app. public_base_url is the address agents call and sign for; behind a proxy, it's the public one. For FastAPI or Starlette, wrap the app the same way, or use app.add_middleware(DelegusMCPMiddleware, api_key=…, public_base_url=…).

Options:

  • tools: a list or a predicate, to enforce only some tools (others pass unchecked);
  • relying_party: skips a lookup when credentials are missing;
  • on_decision: called with each decision, for logs;
  • max_body_bytes: default 1 MiB; a larger body is refused;
  • api_base_url;
  • timeout_seconds;
  • protocol: "auto" (the default), "v0.2" or "v0.3" (see below);
  • requires: v0.3 only, the prior decisions every checked call must rest on, as [{"fingerprint": …, "mode": …}];
  • tool_map: the tool map (a parsed JSON object), so a call's arguments become what is checked (see below).

Checking the arguments (tool map)

By default a permission names the tool: an agent may call push_branch, or not. With a tool map, it can name what the call does: push_branch to feature/* only, or payment.send to Acme up to $25,000.

TOOL_MAP = {"version": 1, "tools": {
    "push_branch": {"action": "api:call", "resource": "refs/heads/{arg:/branch}"},
    "payment.send": {"action": "commerce:purchase", "resource": "payee:{arg:/recipient}",
                     "amount": {"value": "/amount_cents", "currency": "/currency", "unit": "minor"}},
}}
app = DelegusMCPMiddleware(server_app, api_key=…, public_base_url=…, tool_map=TOOL_MAP)
  • {arg:/branch} is a JSON pointer into the call's arguments. A tool without an entry is checked as before, by name only.
  • The map is checked when the middleware starts; a mistake stops it, naming the tool and the field.
  • It is published beside the endpoint at <path>/delegus-tools.json, with its hash, and named in the Delegus-Tools header on a refusal, so agents build the same request they sign. An agent that ignores it is refused with PROOF_ACTION_MISMATCH and told where the map is.
  • Arguments that cannot be turned into what the map describes (missing, the wrong type, a path that climbs with .., including percent-encoded and look-alike forms, an amount with too many decimals) are refused with ARGUMENTS_UNMAPPABLE before Delegus is asked. Nothing is ever allowed because an argument was missing.
  • Only the fields the map names are checked. An argument it does not name (a force flag) is not part of the permission.
  • Several tools that map to the same purchase shape share one permission: a permission to pay Acme covers every tool that pays Acme.
  • Write permission patterns in NFC Unicode; café typed two ways is two different names.

Every receipt is checked

Each relying party receives receipts in its own profile's format: v0.2 (delegus-base-v1) or v0.3 (delegus-base-v3, whose receipts carry the matched permission as a commitment, with the permission and the key to open it returned beside the receipt). With the default, protocol="auto", the middleware accepts either, chosen by the version the receipt was signed with, and checks it with delegus-core before the tool runs:

  • the signature, against Delegus's published receipt keys (<api_base_url>/.well-known/did.json, which must name did:web:delegus.ai in production), fetched once and reused;
  • that the receipt is for this request: its Grant, Proof and action hashes match what was sent;
  • that the decision, the reason and the individual checks agree;
  • for v0.3, that the permission opens its commitment, and that the dependency results and requires match.

protocol="v0.2" or "v0.3" pins one format. An answer that fails any check, or comes in the other format under a pin, is refused with SERVICE_UNAVAILABLE, and on_decision gets an error field naming the cause. There is one check per call, never a retry: a second check with the same Proof would be refused as a replay.

What happens to each request

  • tools/call: the agent's Delegus-Grant and Delegus-Proof headers are checked, including that the Proof was signed for this tool at this address.

    • On ALLOW, the tool runs. The receipt is in scope["state"]["delegus"] (in Starlette, request.state.delegus) and the response carries Delegus-Receipt-Id.
    • On DENY, the middleware answers HTTP 403 with JSON-RPC error -32040 and the Delegus reason, and the tool never runs.
  • Anything it can't read is refused:

    • a missing, unparseable or oversized body, or one with duplicate keys or keys that differ only by case (REQUEST_UNREADABLE);
    • a JSON-RPC batch carrying a checked tool call (BATCH_NOT_SUPPORTED);
    • a tool call without a string tool name (TOOL_NAME_INVALID).

    These are the middleware's own refusals and carry no signed receipt.

  • Delegus can't be reached, or its answer doesn't check: 403 SERVICE_UNAVAILABLE. It fails closed.

  • Everything that can't run a tool passes through: initialize, tools/list, ping, notifications, JSON-RPC responses, GET (the SSE stream), HEAD, OPTIONS, and a DELETE that ends a session.

  • The app receives exactly the bytes that were checked.

Limits

  • No stdio. It checks HTTP requests, so it covers MCP servers served over HTTP, not stdio-only servers.
  • Arguments are checked only through a tool map. Without one, a permission names the tool, not its arguments. With one, only the fields the map names are checked; validate everything else in the tool.
  • Server-to-client messages aren't checked. Requests your server sends to the client on an SSE stream (sampling, elicitation) pass as they are.
  • Only requests that reach this app are checked. If the same tools are exposed some other way, those routes aren't protected.

Tested against

The shared MCP request-shape suite in @delegus/conformance: the same rows the Node middleware and the Delegus MCP gateway run. It asserts that a refused request never reaches the tool, and that a forwarded one arrives byte for byte. End to end, with the official Python MCP SDK (mcp 2.x) and a local Delegus, for a relying party on each profile:

  • a delegated call runs with a receipt;
  • after a revoke, the same call is refused with AUTHORITY_REVOKED;
  • a batch is refused.

Python 3.9 or later. Apache-2.0.

The agent side: mcp_headers

For an agent written in Python (an Anthropic SDK loop that calls MCP tools), delegus_mcp.mcp_headers signs the two request headers for one tools/call, the counterpart of the Node SDK's Agent.mcpHeaders:

from delegus_core.memory import MemorySigner
from delegus_mcp import mcp_headers

signer = MemorySigner(agent_did, agent_private_key)   # the agent's Ed25519 key
headers = mcp_headers(signer.sign, agent_did, grant_jws,
                      endpoint="https://tools.example.com/mcp",   # the gateway's --endpoint
                      tool="create_ticket", rp_did=relying_party_did,
                      args={"queue": "it-helpdesk"}, tool_map=tool_map)  # tool_map optional
# headers == {"Delegus-Grant": grant_jws, "Delegus-Proof": "<compact JWS>"}

The Proof binds the tool URI <endpoint>/tools/<name> (POST); with the endpoint's tool map the Action binds the arguments the map names (mcp_action_for shows what will be checked), and an argument the map cannot apply raises ArgumentsUnmappable before anything is signed, as the server would refuse it. The wrong key for the Grant raises ValueError.

Metadata

Release files for delegus-mcp 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for delegus-mcp 0.3.1
File Size Uploaded Provenance Actions
delegus_mcp-0.3.1.tar.gz 27.6 kB Without provenance Details for delegus_mcp-0.3.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for delegus-mcp 0.3.1
File Interpreter ABI Platform Actions
delegus_mcp-0.3.1-py3-none-any.whl Python 3 none any Details for delegus_mcp-0.3.1-py3-none-any.whl

Total release size: 46.5 kB

Release files / delegus_mcp-0.3.1.tar.gz

Download URL delegus_mcp-0.3.1.tar.gz
Size 27.6 kB
Tags Source
SHA-256 checksum
How to use checksums
145309b6cba8e7909823b5d8c742da12c843bab2b6b72b8ca1e30a5c7df0fe49
BLAKE2b-256 checksum
How to use checksums
31af40c0323bdbc1b88cf71a3a261d02a6bfdb4133c11089fa1939985fdc10f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / delegus_mcp-0.3.1-py3-none-any.whl

Download URL delegus_mcp-0.3.1-py3-none-any.whl
Size 18.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bc13aae3cb3f305af9a07dafdd58bd20bcd747d0e60d65270c50cce13d5f4ea4
BLAKE2b-256 checksum
How to use checksums
276b0dd0e3556ed5b9e81cea5fd0d203f67f06c2377f9c5048aafed40a122a11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Security for delegus-mcp 0.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page