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.2.0 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 grant of authority
Delegus-Proof: eyJ… the agent's proof of grant for 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 of grant and that action. Every check 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 of grant is signed by the agent the Grant names, is addressed to this server, is fresh, and hasn't been used before;
- the proof of grant 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 of grant 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#
Out of the box, the check covers 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". Since @delegus/sdk 0.3.0, a tool map can bind arguments too: it says which arguments become the action's resource and amount, so a Grant can allow pushes to feature branches only. Arguments the map doesn't name still belong to the tool's own validation. The MCP docs show how.
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 of grant 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