1. The Problem
Your MCP server runs tools. Those tools make decisions, access data, and take actions on behalf of users. When an auditor asks "what did the AI do?", the answer is usually application logs.
Application logs were never designed to be compliance evidence. They can be incomplete, modified, or lost. They lack cryptographic integrity. They do not map to regulatory controls. They are not independently verifiable.
The Witness Middleware creates a cryptographic witness record for every tool call your server processes (or a deterministic fraction, if you configure sampling). Each record proves which tool ran, how long it took, and whether it succeeded. The record is anchored by a SHA-256 fingerprint that anyone can independently recompute. If a single bit changes, the hash breaks. That is the difference between a log entry and evidence.
2. One Line of Code
import { withSWT3 } from "@tenova/swt3-mcp/middleware";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = withSWT3(new StdioServerTransport(), {
apiKey: process.env.SWT3_API_KEY,
});
await server.connect(transport);
// Drain buffered evidence before exit
process.on("SIGTERM", async () => {
await transport.flush();
process.exit(0);
});
This wraps the communication layer between your server and the MCP client. Your tool handlers run exactly as before. The middleware buffers witness payloads and flushes them in batches. The flush() call on shutdown ensures no evidence is lost when the process exits. See Batching and Graceful Shutdown for details.
No API key? Omit it entirely. The middleware runs in demo mode and logs witness records to stderr. No network calls, no account required. Upgrade to a free account when you want persistent evidence.
3. What Happens When a Tool Runs
- Your tool response is already sent before the witness fires. The middleware cannot slow down, block, or fail your tool calls.
- If the witness network call fails, your tool call is completely unaffected. Failed batches are automatically retried with exponential backoff. If all retries are exhausted, error receipts are fired and the batch is dropped. Evidence delivery is best-effort; tool execution is never compromised.
- The middleware sees tool names and execution time. It does not see prompt content, response content, or anything inside your tool handlers.
Each witness record is anchored to the AI-TOOL.1 procedure in the Universal Control Taxonomy. This maps directly to EU AI Act Art. 12 (record-keeping), NIST AI RMF MEASURE 2.5, and OWASP Agentic Top 10 controls for tool abuse prevention.
4. When to Use This
SWT3 offers three integration levels. Choose based on how much compliance depth your system requires.
| Integration Level | Components | Choose This If | Regulatory Coverage |
|---|---|---|---|
| Level 1: Passive Observation | withSWT3(transport) middleware only |
Existing MCP server, need an audit trail fast, zero code changes to tools | EU AI Act Art. 12, CMMC AU-2/AU-3, NIST MEASURE 2.5 |
| Level 2: Active Governance | SDK Witness class with strict: true |
Need pre-inference gates, gatekeeper blocking on failure, full procedure coverage (drift, HITL, model integrity) | Full AI RMF, EU AI Act Art. 9/14, SR 11-7 |
| Level 3: Full Compliance Stack | Middleware + SDK + MCP server (33 tools) | Regulated industry, multi-framework requirements, auditor-facing evidence packages, sovereign deployment | All 36 frameworks, full UCT taxonomy (114 procedures) |
This guide covers Level 1 (the middleware). For framework-specific configuration recipes that combine all three levels into copy-paste regulatory compliance configs, see the Regulatory Configuration Recipes guide.
5. Configuration
All settings are optional. The middleware runs with zero configuration in demo mode.
| Setting | Default | When You Need It |
|---|---|---|
| Core | ||
endpoint | sovereign.tenova.io | Only change if you are self-hosting the Axiom platform |
apiKey | demo mode | Omit to evaluate locally. Set an axm_ key to persist anchors to the ledger |
tenantId | auto-resolved | Set explicitly if your API key serves multiple tenants |
clearingLevel | 1 (standard) | Raise to 2 or 3 to reduce how much metadata leaves the wire |
agentId | none | Set to identify which agent produced the anchor (required for multi-agent chains) |
signingKey | none | Set for HMAC non-repudiation. If someone disputes the evidence, the signature proves who minted it |
maxBuffer | 100 | Raise if your server handles more than 100 concurrent in-flight tool calls |
onWitness | none | Set a callback to pipe witness receipts to your logging or metrics system |
| Batching | ||
batchSize | 10 | Number of witness payloads buffered before a batch flush. Set to 1 for per-call behavior |
flushIntervalMs | 5000 | Maximum time (ms) before the buffer is flushed, even if batchSize has not been reached |
| Sampling | ||
samplingRate | 1.0 | Global sampling rate (0.0 to 1.0). Set below 1.0 to witness a fraction of tool calls |
samplingRates | none | Per-tool overrides: { "critical_tool": 1.0, "health_check": 0.01 } |
| Multi-Tenant | ||
resolveTenant | none | Callback to resolve tenant per tool call. For multi-tenant MCP servers |
| Retry | ||
maxRetries | 3 | Number of retry attempts for failed batch submissions (5xx or network errors) |
maxRetryBuffer | 500 | Maximum payloads held in the retry queue. Oldest batches evicted first |
6. Batching and Graceful Shutdown
By default, the middleware buffers witness payloads and flushes them in batches of 10 (or every 5 seconds, whichever comes first). This reduces network overhead from one HTTP call per tool invocation to one call per batch.
- The buffer reaches
batchSizepayloads (default: 10) - The flush timer fires (default: every 5000ms)
- You call
transport.flush()manually - You call
transport.close()(best-effort flush before teardown)
Graceful Shutdown
withSWT3() returns the transport with a flush() method attached. Call it before your process exits to drain any remaining payloads.
const transport = withSWT3(new StdioServerTransport(), {
apiKey: process.env.SWT3_API_KEY,
});
await server.connect(transport);
// On shutdown:
process.on("SIGTERM", async () => {
await transport.flush();
process.exit(0);
});
If you call transport.close() instead (the standard MCP transport lifecycle), the middleware flushes automatically before closing. If the flush fails, the close still completes. Evidence delivery is best-effort; transport teardown is never blocked.
The flush timer uses .unref() so it will not keep a Node.js process alive after all other work is done. You do not need to explicitly clean up the timer.
Tuning for Real-Time vs. Throughput
// Real-time evidence (every tool call flushed immediately)
withSWT3(transport, { batchSize: 1 });
// High-throughput server (batch up to 50, flush every 10s)
withSWT3(transport, { batchSize: 50, flushIntervalMs: 10_000 });
batchSize: 1. All other settings are backward-compatible.
7. Sampling
High-volume servers may not need to witness every single tool call. The middleware supports deterministic sampling: given the same fingerprint input, the same witness/skip decision is made every time. This is not random. It is reproducible.
// Witness 10% of tool calls globally
withSWT3(transport, {
apiKey: process.env.SWT3_API_KEY,
samplingRate: 0.1,
});
Per-Tool Overrides
Critical tools can be witnessed at a higher rate than routine ones. Per-tool rates override the global rate.
withSWT3(transport, {
apiKey: process.env.SWT3_API_KEY,
samplingRate: 0.01, // 1% for everything else
samplingRates: {
"execute_trade": 1.0, // 100% for trades
"modify_account": 1.0, // 100% for account changes
"health_check": 0.0, // never witness health checks
},
});
Why Deterministic Sampling Matters to Assessors
- Random sampling can be gamed. If sampling were random, an adversary could retry or resend a tool call until the unfavorable evidence lands outside the sample. Selective exclusion of damaging evidence would be undetectable.
- SWT3 sampling is deterministic. The witness/skip decision is derived from the SHA-256 hash of the fingerprint input itself. The first two bytes of the hash are interpreted as a uint16 threshold and compared against the configured rate. The same payload always produces the same decision.
- Adversaries cannot selectively exclude evidence. Changing the payload to alter the sampling decision changes the fingerprint, which breaks the cryptographic chain. The sampling decision is bound to the evidence it governs.
- Assessors can independently verify. Given any payload, an assessor can recompute the SHA-256 hash, extract the first two bytes, and confirm the sampling decision was correct. No trust in the system operator is required.
The sampling algorithm is identical across all SWT3 implementations (Python, TypeScript, Go, Rust, C#, Ruby, Swift), producing the same sampling decisions for the same input across all languages.
samplingRate: 0.1 in demo mode, all calls appear in stderr. When you switch to a live API key, only 10% will produce anchors. This is intentional: demo mode is for validation, not volume simulation.
8. Multi-Tenant
If your MCP server handles requests from multiple tenants, the middleware can resolve the correct tenant for each tool call at runtime.
withSWT3(transport, {
apiKey: process.env.SWT3_API_KEY,
tenantId: "DEFAULT_TENANT",
resolveTenant: (toolName, params) => {
// Extract tenant from the tool call parameters
return params.tenant_id;
},
});
The callback receives the tool name and the full JSON-RPC params object. Return a tenant ID string, or undefined to fall back to the configured tenantId.
Each payload in the batch carries its own tenant_id field. A single batch can contain payloads for multiple tenants. The server routes each payload to the correct tenant ledger.
The resolved tenant is also used in the fingerprint formula, so fingerprints are always scoped to the correct tenant. Cross-tenant fingerprint collisions are not possible.
9. Retry
Network failures and server errors do not lose evidence. The middleware maintains an in-memory retry queue for failed batches.
| Response | Behavior |
|---|---|
| 2xx (success) | Receipts fired, payloads cleared |
| 4xx (client error) | Dropped immediately. Error receipts fired. No retry (the request itself is invalid) |
| 5xx (server error) | Queued for retry with exponential backoff (1s, 2s, 4s) |
| Network error | Queued for retry with exponential backoff |
Retries piggyback on the flush interval timer. There are no extra timers or background threads. Each flush cycle checks the retry queue first, sends any eligible batches (past their backoff delay), then flushes new payloads.
The retry queue is bounded at maxRetryBuffer payloads (default: 500). If the queue overflows, the oldest batches are evicted first. After maxRetries attempts (default: 3), the batch is dropped and error receipts are fired via onWitness.
10. Real-World Patterns
Production MCP Server
A typical production configuration combines batching, sampling, signing, and retry. This is the recommended starting point for regulated environments.
const transport = withSWT3(new StdioServerTransport(), {
apiKey: process.env.SWT3_API_KEY,
agentId: "claims-processor-v3",
signingKey: process.env.SWT3_SIGNING_KEY,
clearingLevel: 2,
// Batching: flush every 20 payloads or 10 seconds
batchSize: 20,
flushIntervalMs: 10_000,
// Sampling: witness all trades, 10% of reads, skip health checks
samplingRate: 0.1,
samplingRates: {
"execute_trade": 1.0,
"modify_account": 1.0,
"health_check": 0.0,
},
// Retry: 3 attempts with exponential backoff
maxRetries: 3,
// Observability: pipe receipts to your logging stack
onWitness: (receipt) => {
logger.info("swt3.witness", {
tool: receipt.toolName,
fingerprint: receipt.fingerprint,
error: receipt.error ?? null,
});
},
});
await server.connect(transport);
process.on("SIGTERM", async () => {
await transport.flush();
process.exit(0);
});
Observability Pipeline
Pipe witness receipts into your existing observability stack. Every tool call becomes a structured event in Datadog, Splunk, CloudWatch, or any system that accepts structured logs.
withSWT3(transport, {
apiKey: process.env.SWT3_API_KEY,
onWitness: (receipt) => {
logger.info("swt3.tool.witnessed", {
tool: receipt.toolName,
fingerprint: receipt.fingerprint,
latency_ms: receipt.latencyMs,
error: receipt.error ?? null,
});
},
});
The callback fires for every witnessed tool call when the batch is flushed, regardless of whether the POST succeeded or failed. If the POST failed, receipt.error tells you why. Sampled-out calls do not fire onWitness (they appear in AI-SAMPLE.1 summaries instead). The tool call itself is never affected.
Signed Anchors for Regulated Environments
When evidence may be disputed, HMAC-SHA256 signatures bind each anchor to the holder of the signing key. The signature proves the anchor was minted by an authorized system, not reconstructed after the fact.
withSWT3(transport, {
apiKey: process.env.SWT3_API_KEY,
signingKey: process.env.SWT3_SIGNING_KEY,
agentId: "claims-processor-v3",
});
Register the signing key server-side to enable verification. Every anchor minted by this agent carries a tamper-evident signature.
Minimal Metadata for Classified Environments
Clearing level 3 strips all descriptive metadata. Tool names are hashed. Only numeric factors (call count, latency, success/failure) and the cryptographic fingerprint leave the wire. The evidence proves something happened without revealing what.
withSWT3(transport, {
apiKey: process.env.SWT3_API_KEY,
clearingLevel: 3,
});
11. Common Questions
"Can the middleware break my server?"
No. The tool response is committed to the wire before the witness fires. If the witness fails, your server never knows. This behavior is verified by 358 automated tests covering error injection, network failure, concurrent load, batching, sampling, multi-tenant routing, and retry scenarios.
"Can I use this with any MCP transport?"
Yes. Stdio, SSE, HTTP, WebSocket, or any custom transport that implements the standard MCP Transport interface. The middleware does not import the MCP SDK. It works with the interface shape, not a specific implementation.
"What if I already use the full SWT3 MCP server?"
They coexist without conflict. The middleware witnesses at the transport layer (tool names and latency). The server's 33 tools witness at the handler layer (detailed compliance metadata). Two independent evidence streams that complement each other.
"What about high-volume servers?"
The middleware batches witness payloads (default: 10 per batch, flush every 5 seconds) to reduce network overhead. For servers processing thousands of tool calls per minute, use sampling to witness a configurable fraction of calls while emitting AI-SAMPLE.1 summaries that document exactly what was excluded. Failed batches are automatically retried with exponential backoff. The pending call tracker holds up to 100 concurrent in-flight calls (configurable via maxBuffer).
"Does this work without an account?"
Yes. Omit the API key and the middleware runs in demo mode. Witness records log to stderr with a (demo) tag. No network calls. When you are ready for persistent evidence, create a free account and set the API key.
"What regulatory frameworks does this cover?"
Every AI-TOOL.1 anchor maps to EU AI Act Art. 12 (record-keeping), NIST AI RMF MEASURE 2.5 (AI system monitoring), OWASP Agentic Top 10 (tool abuse detection), and CMMC AU-2/AU-3 (audit event generation). The UCT Registry documents the full mapping.
12. What This Does Not Do
- Does not enforce policy. The middleware witnesses. It cannot block, reject, or modify tool calls. If you need authorization gates, guardrail enforcement, or pre-inference policy checks, use the full MCP server.
- Does not see prompt or response content. It operates at the transport layer. Tool names and execution time are visible. What your tools receive and return is not.
- Does not require the MCP SDK as a dependency. The middleware works with the Transport interface shape. No class imports from
@modelcontextprotocol/sdk. - Does not replace the full MCP server. The middleware adds an evidence layer. The server adds governance. They serve different purposes and can run together.
Getting Started
Install the package:
npm install @tenova/swt3-mcp
Import the middleware, wrap your transport, and connect. Every tool call your server processes now produces a cryptographic witness record (or a deterministic fraction, if you configure sampling). Start in demo mode, verify the output, then connect to the ledger when you are ready.