Integrate DRS with an A2A agent (Node / TypeScript)
Agent-to-Agent (A2A) differs from MCP in shape — both agents sit at equal standing and exchange tasks — but the DRS integration story is the same: the caller attaches a receipt bundle, the receiver verifies it before acting.
This guide covers the receiver side in Node. The caller side is the
same as React Native issuance and
MCP Node integration — you issue an invocation with
issueInvocation.
Architecture
Agent A (initiator) Agent B (receiver)
│
│ POST /a2a/task
│ X-DRS-Bundle: eyJ...
▼
┌──────────────────┐ ┌─────────────────────┐
│ Agent B (Node) │────────▶│ drs-verify sidecar │
│ 1. extract hdr │ POST │ ghcr.io/okeyamy/... │
│ 2. /verify │ /verify │ │
│ 3. if valid → │◀────────│ │
│ run task │ └─────────────────────┘
└──────────────────┘
Agent B is structurally the same as an MCP tool server — both verify an inbound bundle before executing. If you've already set up the MCP integration the code here is almost identical.
Install
# Once published: pnpm add @drs/mcp-server
# Today: vendor packages/drs-mcp-server from this repository or use a workspace dependency.
The actual cryptographic verification happens in the drs-verify container.
The workspace Node package gives your receiver a secure enforcement point that
rejects invalid chains and body-binding mismatches before task execution.
Compose with Redis for shared replay protection
If Agent B is horizontally scaled across multiple instances, you need shared replay protection or an attacker can submit the same bundle to each replica in turn.
services:
agent-b:
build: .
deploy:
replicas: 3
environment:
DRS_VERIFY_URL: http://drs-verify:8080
drs-verify:
image: ghcr.io/okeyamy/drs-verify:latest
environment:
NONCE_STORE_BACKEND: redis
REDIS_URL: redis://redis:6379/0
deploy:
replicas: 2 # drs-verify itself can also scale — state is in Redis
redis:
image: redis:7-alpine
A2A middleware
// a2a-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 drsA2A(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 });
}
Task handler
import express from "express";
import { drsA2A } from "./a2a-middleware.js";
const app = express();
app.use(express.json({ limit: "65kb" })); // drs-verify binding middleware enforces 64 KiB
app.post("/a2a/task", drsA2A, async (req, res) => {
// req.drs.root_principal is the original human/organisation
// req.drs.leaf_policy is the effective policy AFTER attenuation
const { task_type, payload } = req.body;
// A2A-specific: enforce that the task matches what's allowed by policy.
const allowedTools = req.drs.leaf_policy?.allowed_tools ?? [];
if (allowedTools.length > 0 && !allowedTools.includes(task_type)) {
return res.status(403).json({
error: "task_type not in allowed_tools",
allowed: allowedTools,
});
}
const result = await runA2ATask(task_type, payload, {
onBehalfOf: req.drs.root_principal,
});
res.json(result);
});
app.listen(3000);
JSON-RPC variant
Some A2A deployments use JSON-RPC instead of plain HTTP. The DRS
spec allows the bundle to live in _meta["X-DRS-Bundle"] instead of
a header.
app.post("/a2a/rpc", express.json(), async (req, res) => {
const bundleStr = req.body?._meta?.["X-DRS-Bundle"];
if (!bundleStr) return res.status(401).json({ error: "missing bundle" });
const bundle = JSON.parse(
Buffer.from(bundleStr, "base64url").toString("utf8"),
);
const r = await fetch(`${VERIFY_URL}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(bundle),
});
const result = await r.json();
if (!result.valid) {
return res.json({
jsonrpc: "2.0",
id: req.body.id,
error: { code: -32001, message: "DRS verification failed", data: result.error },
});
}
// dispatch on req.body.method ...
});