Integrate DRS with an MCP server (Node / TypeScript)
Your MCP server runs on Node. Agents send tool-call requests with a
X-DRS-Bundle header. You want the bundle verified before your business
logic runs. This is the sidecar pattern.
No Go code, no forking DRS, no rebuilding containers.
Architecture
Agent (React Native, web, Node, etc.)
│
│ POST /tools/call
│ X-DRS-Bundle: eyJ...
│
▼
┌────────────────────────────┐ ┌───────────────────────┐
│ Your MCP server (Node) │──────▶│ drs-verify (Docker) │
│ 1. read bundle from header│ POST │ ghcr.io/okeyamy/ │
│ 2. POST /verify │ /verify│ drs-verify:latest │
│ 3. if valid → run tool │ │ │
│ 4. else → 403 │◀──────│ │
└────────────────────────────┘ └───────────────────────┘
Install nothing extra on your server
DRS verification in this pattern needs only the SDK's types — the
actual verification happens inside the drs-verify container. So:
# On your MCP server
pnpm add -D @okeyamy/drs-sdk # dev-only, for TypeScript types
The -D is intentional — you import types at compile time, but you do
not call any SDK function on the server.
Docker Compose for local dev
# docker-compose.yml at the root of YOUR project
services:
mcp-server:
build: .
ports:
- "3000:3000"
environment:
DRS_VERIFY_URL: http://drs-verify:8080
depends_on:
- drs-verify
drs-verify:
image: ghcr.io/okeyamy/drs-verify:latest
environment:
LISTEN_ADDR: ":8080"
LOG_FORMAT: json
# Optional: replay protection that survives restart and scales horizontally
NONCE_STORE_BACKEND: redis
REDIS_URL: redis://redis:6379/0
depends_on:
- redis
redis:
image: redis:7-alpine
Middleware for your MCP server
Express / Fastify / raw http.Server — the pattern is the same.
// drs-middleware.ts
import type { ChainBundle, VerificationResult } from "@okeyamy/drs-sdk";
const VERIFY_URL = process.env.DRS_VERIFY_URL ?? "http://localhost:8080";
export async function drsVerify(req, res, next) {
const bundleHeader = req.headers["x-drs-bundle"];
if (!bundleHeader || typeof bundleHeader !== "string") {
return res.status(401).json({
error: "Missing X-DRS-Bundle header — DRS verification is required.",
});
}
let bundle: ChainBundle;
try {
const json = Buffer.from(bundleHeader, "base64url").toString("utf8");
bundle = JSON.parse(json);
} catch {
return res.status(400).json({
error: "X-DRS-Bundle is not valid base64url JSON.",
});
}
const verifyRes = await fetch(`${VERIFY_URL}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(bundle),
});
const result = (await verifyRes.json()) as VerificationResult;
if (!result.valid) {
return res.status(403).json(result);
}
// Attach the verified context for downstream handlers.
(req as any).drs = result.context;
next();
}
Wiring it in Express
import express from "express";
import { drsVerify } from "./drs-middleware.js";
const app = express();
app.use(express.json());
app.post("/tools/call", drsVerify, async (req, res) => {
// req.drs is set — it contains RootPrincipal, LeafPolicy, etc.
const { tool, ...args } = req.body;
// Enforce policy at the tool layer. `drs-verify` has already checked
// attenuation; here you enforce execution-time limits.
const maxCost = req.drs.leaf_policy?.max_cost_usd;
if (maxCost != null && args.estimated_cost_usd > maxCost) {
return res.status(403).json({ error: "Exceeds policy.max_cost_usd" });
}
const result = await runTool(tool, args);
res.json(result);
});
app.listen(3000);
Wiring it in Fastify
import Fastify from "fastify";
import { drsVerify } from "./drs-middleware.js";
const app = Fastify();
app.post(
"/tools/call",
{
preHandler: async (req, reply) => {
// Adapt the Express-shaped middleware to Fastify.
const next = () => {};
const expressRes = {
status: (n: number) => ({ json: (x: unknown) => reply.code(n).send(x) }),
};
await drsVerify(req as any, expressRes as any, next);
},
},
async (req) => {
return { ok: true, drs: (req as any).drs };
},
);
app.listen({ port: 3000 });
Performance notes
drs-verifyhandles DID resolution caching, nonce replay checking, and revocation lookups in one round-trip. Typical /verify latency against a local container is 5–15 ms (single-digit when caches are warm).- If the 5–15 ms hop matters, switch to the embedded Go middleware pattern — but that forces your tool server to be in Go.
Request-binding caveat
The middleware above verifies the delegation chain. It does not
yet compare the signed invocation.args against the actual request
body. If an attacker replaces the body while keeping the header, DRS
will still say "valid" — because the header's invocation and the body
are decoupled.
Either:
- Execute from the signed args (preferred): trust
req.drs.invocation.argsinstead ofreq.bodywhen invoking the tool. This is the strongest guarantee. - Hash-compare: in your handler, canonicalise the parts of
req.bodythat correspond toinvocation.argsand reject if they differ.
The first option is the DRS-native answer. The invocation receipt is the authenticated payload.