Where it sits#
MCP leaves the authorization server deliberately open. Okta Cross App Access (XAA) covers the enterprise-managed side: which of your own users and apps may connect to your MCP server. Delegus covers the cross-organization side: what a stranger's organization authorized its agent to do, answered at the enforcement point in one signed answer the server can re-check for years, without asking anyone to become an authorization server.
Add it to your MCP server#
Use
@delegus/sdk0.1.3 or later withdelegus.mcp(). Earlier versions could pass a JSON-RPC batch, a request with no parsed body, or a tool call without a string tool name to the tool unchecked. 0.1.3 refuses all three.
Make any MCP server authorization-aware in one line. Every JSON-RPC tools/call is verified; nothing runs without an ALLOW, and each verified call returns a signed Decision Receipt you keep. Fail closed. initialize, tools/list, ping and notifications pass through.
Install#
npm install @delegus/sdkOne line#
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()); // every tools/call is now verifiedOn ALLOW the receipt is on req. and Delegus- is set; on DENY the middleware returns HTTP 403 with error. (JSON-RPC error code -32040) and the tool never runs. A missing-credentials response adds Delegus- and Delegus- for discovery. Options: mcp({ tools, onDecision, relyingParty }) — tools limits enforcement to an allow-list or predicate (unlisted tools pass through unverified).
Anything the middleware cannot read is refused with the same 403, never passed through: a POST with no parsed body (mount express.json() before it), a body that is not JSON or not a JSON-RPC message (REQUEST_), a JSON-RPC batch that carries an enforced tools/call (BATCH_; batches left MCP in 2025-06-18), and a tools/call without a string tool name (TOOL_). These are the middleware's own refusals, like a missing Delegus-Grant: Delegus is never called, so they carry no signed receipt (receipt_id is null). A batch passes only when every message in it provably invokes no enforced tool. A raw string or byte body is parsed and enforced; hand your MCP server the same parsed body the middleware saw rather than re-parsing the raw bytes, so both read one message. GET (the SSE stream), HEAD, OPTIONS, a DELETE without a body (session end) and JSON-RPC responses pass; a body on any other verb is read like a POST.
The agent side#
The agent presents two headers on the tools/call POST, bound to the same tool URI:
const headers = await agent.mcpHeaders({ grant, endpoint: "https://tools.acme.com/mcp", tool: "get_customer", rpDid });
// { "Delegus-Grant": "eyJ…", "Delegus-Proof": "eyJ…" }What it binds (v0.2)#
The middleware binds the tool (an api:call on the tool URI): a Grant's resources decide which tools an agent may call. It does not bind tool arguments in v0.2.
What was measured#
Our example MCP server runs a scripted session against a local Delegus: a delegated create_ticket call is allowed and receives a signed receipt; the Grant is revoked; the same call, with a fresh Proof, is then denied with AUTHORITY_ before the tool runs.
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.Nothing here is a performance figure or a guarantee.
Read next#
- Integrate as a relying party: the same
/verifycall, outside MCP. - Receipts & offline verification: what the receipt contains and how to re-check it without calling Delegus.
- SDK and CLI reference: every export, including
mcp()andmcpHeaders().