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 the enforcement middleware
The secure default path is the reusable HTTP middleware in the workspace
@drs/mcp-server package. It extracts X-DRS-Bundle, sends the decoded bundle
plus the actual request body to drs-verify, rejects invalid chains, rejects
body-binding mismatches, and only then lets your handler run.
# On your MCP server
# Once published: pnpm add @drs/mcp-server
# Today: vendor packages/drs-mcp-server from this repository or use a workspace dependency.
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 { createDrsHttpMiddleware } from "@drs/mcp-server";
const VERIFY_URL = process.env.DRS_VERIFY_URL ?? "http://localhost:8080";
const drs = createDrsHttpMiddleware({ verifyUrl: VERIFY_URL });
export async function drsVerify(req, res, next) {
const result = await drs(
{
headers: req.headers,
body: req.body,
},
(verifiedReq) => {
req.drs = verifiedReq.drs;
next();
},
);
if (!result.ok) {
return res.status(result.status).json({ drs_error: result.error });
}
}
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 behavior
createDrsHttpMiddleware passes the actual parsed request body to /verify.
The verifier compares that body with the signed invocation.args using JCS. If
they differ, the middleware rejects the request before your handler runs.