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'sarguments. 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 theDelegus-Toolsheader on a refusal, so agents build the same request they sign. An agent that ignores it is refused withPROOF_ACTION_MISMATCHand 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 withARGUMENTS_UNMAPPABLEbefore 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
forceflag) 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 namedid:web:delegus.aiin 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
requiresmatch.
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'sDelegus-GrantandDelegus-Proofheaders 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 carriesDelegus-Receipt-Id. - On DENY, the middleware answers HTTP 403 with JSON-RPC error
-32040and the Delegus reason, and the tool never runs.
- On ALLOW, the tool runs. The receipt is in
-
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.
- a missing, unparseable or oversized body, or one with duplicate keys or
keys that differ only by case (
-
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 aDELETEthat 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.