SECURITY@mcpose/audit 2.0.3 — audit subkeys now derive from the signing secret, not the public key idChangelog →
Transparent MCP proxy · TypeScript

The audit and governance layer for MCP.

Drop mcpose between any LLM client and any MCP server. Intercept, transform, and govern every tool call through composable onion middleware — and log it in a tamper-evident, compliance-grade audit trail. Nothing upstream changes.

$npm install mcpose
v2.1.1 on npm·MIT·Node 20+·ESM, types included·semver-disciplined
proxy.tsTypeScript
// Wrap any upstream MCP server — unmodified
const backend = await createBackendClient({
  command: 'node',
  args: ['./es-server.mjs'],
});

const audit = createAuditMiddleware({
  signingKey: createDefaultSigningKeyProvider(process.env.AUDIT_SECRET!),
  onEvent: (e) => auditLog.append(e), // HMAC-chained, tamper-evident
});

await startHttpProxy(backend, {
  hiddenTools: ['delete_index'],        // rejections are audited too
  toolMiddleware: [redactPii, audit.middleware],
});                     // audit only ever sees redacted data
Why mcpose

The same three concerns, lifted out.

Nothing here is a competitor comparison, because there is no competitor to name. The alternative is the code you already have: auth, redaction, and logging written into the handlers of a server you control.

Hand-rolled in one serverwhere mcpose came from
server.tsTypeScript
server.setRequestHandler(CallToolRequestSchema, async (req) => {
  // auth, inline
  const id = await verifyJwt(req.params._meta?.token);
  if (!id.roles.includes('analyst')) throw new Error('denied');

  const out = await runTool(req.params.name, req.params.args);

  // redaction, inline
  const text = PII.reduce(
    (t, re) => t.replace(re, '[REDACTED]'),
    out.text,
  );

  // "audit", inline
  console.log(JSON.stringify({ tool: req.params.name }));

  return { content: [{ type: 'text', text }] };
});

// Welded to this server. Nothing chains the log lines,
// so nothing detects that one was edited or dropped.
// Copy the whole thing for the next server.
Wrapped with mcposeupstream unmodified
proxy.tsTypeScript
// The upstream tool handler is untouched.

const backend = await createBackendClient({
  url: UPSTREAM_URL,
});

await startHttpProxy(
  backend,
  { toolMiddleware: [redactPii, audit.middleware] },
  {
    port: 3000,
    resolveIdentity,
    onSessionClosed: audit.closeSession,
  },
);

// Ordering is the contract: redactPii is listed first,
// so audit never sees raw PII. Point it at the next
// server and it is the same three lines.
Onion middleware
Each layer runs before and after the inner pipeline. Array order is the contract.
Governance
Hide or gate tools per caller. Every blocked call carries a structured RejectionReason.
Identity
Resolve a caller once per session — JWT, mTLS, API key — then stamp the Identity on every request.
Production transport
HTTP/SSE with mTLS, session limits, and reconnect replay — or plain stdio. Abort signals and progress relay through.
What it looks like

Three shapes people actually ship.

Each one is a recipe in the docs, and each one is the same primitive: a function that sees the request on the way in and the response on the way out.

The origin use case: every upstream response is scrubbed before it reaches the LLM, and before it reaches the audit trail. Ordering is the whole point — the redaction middleware is listed first, so audit never sees raw data.

proxy.tsTypeScript
// recipes/pii-redaction-audit
const redactPii: ToolMiddleware = async (req, next) => {
  const result = await next(req);
  if (!hasToolContent(result)) return result;

  return {
    ...result,
    content: result.content.map((item) =>
      item.type === 'text'
        ? { ...item, text: redact(item.text) }
        : item,
    ),
  };
};

// redactPii is listed first, so audit only ever
// sees text that has already been scrubbed.
await startHttpProxy(backend, {
  toolMiddleware: [redactPii, audit.middleware],
});
Read the recipe →
Concept

One proxy in the middle.

mcpose mirrors the upstream MCP surface and routes supported calls through middleware. Capabilities, abort signals, progress, and list-changed notifications pass through intact.

LLM client
Claude · Cursor · any MCP client
MCP
http · sse · stdio
mcpose:3000/mcp
identity resolution
visibility filters
middleware pipelines
audit trail
MCP
stdio · http
Upstream server
any MCP server — unmodified
Three routing paths per tool or resource
Hidden
hiddenTools · hiddenResources

Omitted from list responses; rejected with TOOL_HIDDEN at call time. The rejection still hits the audit trail.

Pass-through
passThroughTools · passThroughResources

Forwarded raw. Transformers are skipped; observers wrapped in markPassThroughObserver() still run.

Middleware
everything else

Routed through the full toolMiddleware / resourceMiddleware pipeline.

@mcpose/audit

Audit trails an examiner can verify.

Built for DORA Art. 17 and SR 11-7. Every event chains to the last; every session closes with a signed Merkle manifest. Extracted from a production financial deployment.

HMAC-chained events
chainHash = HMAC(entry || prevChainHash) — truncation, reordering, and rewrites are detectable.
Replay manifest
A signed Merkle-proof document per session. A third party can verify a single event without access to the full log.
Sensitivity tiers
Classify every tool call; high-tier payloads are encrypted with AES-256-GCM and a per-event key.
Never in the hot path
Audit failures go to onAuditError — a tool call never fails because logging did.
e₀e₁e₂
manifest ✓
TierStored fields
lowinputRaw, outputRaw (plaintext)
mediuminputRaw, outputRaw (PII redacted upstream)
highinputEncrypted, outputEncrypted (AES-256-GCM)
Unknown tools always resolve to 'high'.
Packages

Three packages, one surface.

PackageWhat it doesVersion
mcposeProxy core — pipeline, transports, identity, governance.v2.1.1
@mcpose/auditTamper-evident HMAC audit chain + Merkle replay manifest.v2.0.3
@mcpose/testingRunner-agnostic compliance assertions for the audit chain.v2.0.3
Peer dependency: @modelcontextprotocol/sdk ≥ 1.0 — installed separately.

Drop it in front of any MCP server.

Ten lines of glue. Nothing upstream changes.

$npm install mcpose
Read the docs →