@adastracomputing/aer-mcp-guard
v0.1.0
Published
Admission control for HTTP MCP servers: only AER-attested agents reach your tools; rogue agents get a JSON-RPC deny.
Readme
@adastracomputing/aer-mcp-guard
Admission control for HTTP MCP servers. Only agents carrying a valid AER Attestation token reach your tools; a rogue agent (one that doesn't run the AER collector, so has no token) is denied with a JSON-RPC 2.0 error.
Built on @adastracomputing/aer-resource-node
(offline verify against cached JWKS, optional revocation introspection). No
dependency on @modelcontextprotocol/sdk — mount it in front of any MCP HTTP
endpoint.
Scope: HTTP MCP transports only — Streamable HTTP and SSE.
stdioMCP runs locally with no network admission point, so it is out of scope. v1 gates the whole transport: every MCP HTTP request must carry a valid attestation (clients that can't attach headers duringinitializearen't compatible yet).
Hono
import { honoMcpGuard } from '@adastracomputing/aer-mcp-guard/hono';
app.use('/mcp', honoMcpGuard({ audience: 'mcp://payments-prod' }));
// claims available at c.get('aerAttestation'); mount your MCP handler after.Express
import { expressMcpGuard } from '@adastracomputing/aer-mcp-guard/express';
app.use('/mcp', expressMcpGuard({ audience: 'mcp://payments-prod' }));
// claims attached at req.aerAttestationFramework-neutral core
import { guardMcpRequest } from '@adastracomputing/aer-mcp-guard';
const result = await guardMcpRequest((name) => req.headers.get(name), {
audience: 'mcp://payments-prod',
});
if (!result.ok) {
// result.status (401/403/503) + result.jsonRpcError — send as-is
return new Response(JSON.stringify(result.jsonRpcError), { status: result.status });
}
// result.claims.agent_id / agent_session_id / tenant_id …Revocation (optional)
Pass introspect to also honor revocation within the token's lifetime (see
@adastracomputing/aer-resource-node). The guard caches positive verdicts briefly and is
fail-closed by default if introspection is unreachable.
honoMcpGuard({
audience: 'mcp://payments-prod',
introspect: {
url: 'https://aer-api.adastra.computer/v1/attestations/introspect',
verifierKey: process.env.AER_VERIFIER_KEY!, // aerv_… ; mint via admin
},
});Deny contract
On denial the guard returns an HTTP status plus a JSON-RPC 2.0 error
(code: -32001, message: "attestation required", data.reason):
| status | when | | --- | --- | | 401 | no token / malformed / bad signature / expired / wrong audience or issuer | | 403 | token valid but revoked (introspection reports inactive) | | 503 | introspection unreachable and fail-closed (token liveness unknown) |
The request body is never consumed — SSE and streaming Streamable-HTTP
requests pass through untouched. The JSON-RPC id is echoed only when your
framework already parsed the body (Express req.body); otherwise it is null.
What it proves
That the request was made with a fresh token minted for a running AER session and intended for this audience. It does not prove the agent host is uncompromised, nor bind the token to the transport (no mTLS/DPoP); the short TTL plus optional introspection bound replay. See ADR-010.
