MCP's authorization spec covers who the client is, using OAuth. It doesn't tell the server whether the agent's organization approved a particular call, so that answer has to come from somewhere else.
The situation#
Inside one company, the tools for this exist. Enterprise sign-in and cross-app access decide which of your own users and applications may connect to your own MCP server.
The harder case is the one agents are creating: a tool server receiving a call from an agent that works for another organization. There's no shared identity provider, no shared authorization server, and no person in the loop. The server knows who is connecting. It doesn't know what that agent's organization allowed.
One line#
app.use("/mcp", delegus.mcp())That mounts the @delegus/sdk middleware (version 0.1.3 or later) in front of the MCP endpoint of an Express-style server. The full setup is only a little longer:
import express from "express";
import { Delegus } from "@delegus/sdk";
const app = express();
app.use(express.json());
const delegus = new Delegus({ apiKey: process.env.DELEGUS_RP_KEY, publicBaseUrl: "https://tools.acme.com" });
app.use("/mcp", delegus.mcp());publicBaseUrl is the address agents actually call. The tool's identity is built from it, so a server behind a proxy still checks the URL the agent signed.
What happens on each tool call#
The middleware acts only on JSON-RPC tools/call. initialize, tools/list, ping and notifications pass straight through, so discovery works as it always did.
For a tools/call, the agent sends two headers with the request:
Delegus-Grant: eyJ… the organization's signed permission
Delegus-Proof: eyJ… the agent's signature on this one callThe middleware turns the call into the action the check understands: an api:call with method POST on the tool's own address, <publicBaseUrl>/mcp/tools/<name>. It then makes one call to Delegus with the Grant, the Proof and that action. Every one of the twenty-four checks applies, in the fixed order the specification sets:
- the Grant is signed by a registered organization with a verified domain, is within its dates and hasn't been revoked;
- the Proof is signed by the agent the Grant names, is addressed to this server, is fresh, and hasn't been used before;
- the Proof was signed for this tool on this server, so it can't be moved to another tool or another server;
- the Grant's
resourcesinclude this tool.
So an organization delegates tools by listing them. A Grant whose resources name only …/mcp/tools/create_ticket lets its agent create tickets and nothing else on that server.
On ALLOW, the tool runs. The signed Decision Receipt is attached to the request (req.delegus.receipt) and its id is returned in a Delegus-Receipt-Id header, so the server can store it next to whatever the tool did.
On DENY, the tool never runs. The server answers HTTP 403 with a JSON-RPC error, code -32040, whose data.delegus carries the decision, the reason (for example AUTHORITY_REVOKED or RESOURCE_NOT_AUTHORIZED) and the id of the signed receipt for the refusal.
Without credentials, the tool never runs either. The 403 adds Delegus-Verify: required and a Delegus-Relying-Party header naming the server, which is what an agent needs to sign a Proof for it. If Delegus can't be reached, the answer is SERVICE_UNAVAILABLE, and the tool doesn't run.
Anything it can't read is refused too. A request body that is missing, unparseable or not a JSON-RPC message gets REQUEST_UNREADABLE. A tool call without a proper tool name gets TOOL_NAME_INVALID. A JSON-RPC batch that carries a tool call the middleware enforces is refused whole with BATCH_NOT_SUPPORTED, never split. These refusals, and the ones for missing credentials or an unreachable Delegus, are made by the middleware itself before any check runs, so they come without a signed receipt (receipt_id is null). Mount a JSON body parser before the middleware, and let the server act on the same parsed body the middleware saw.
What it doesn't do#
In v0.2 the check binds the tool, not the tool's arguments. A Grant can say an agent may call create_ticket. It can't say "only tickets under a certain priority". Argument-level limits would need a richer action vocabulary than v0.2 defines, so today they belong to the tool's own validation.
The middleware also has an escape hatch. mcp({ tools }) can limit enforcement to a list of tools, and tools outside that list pass through unchecked. That is for servers mixing public read-only tools with consequential ones. Leave it unset and every tool is checked.
The scripted run#
Our example server runs the whole flow end to end. It boots the Delegus service in-process, with an in-memory store, next to a minimal MCP server with two ordinary tools that know nothing about Delegus.
One organization delegates exactly the create_ticket tool to its agent. The agent calls it on another organization's server and is allowed, and the response carries a receipt. The organization revokes the Grant. The agent calls again with a fresh Proof and is refused before the tool runs. This is the output of a run on 2026-09-23:
1 create_ticket (delegated) HTTP 200 ALLOW drc_…
2 revoke
3 create_ticket after revoke HTTP 403 DENY AUTHORITY_REVOKED drc_…
PASS: the example MCP server verified a delegated tool call and received a receipt, and denied the revoked Grant.Both decisions, the allow and the refusal, come with signed receipts.
That's the whole integration: one line on the server, two headers from the agent, and a signed answer for every tool call that matters.
MCP servers: one-line middleware · SDK reference · The specification