Skip to main content
sec0-sdk/middleware is the in-process enforcement layer for tool servers. It wraps any McpServerLike server, evaluates policy on every tool invocation, emits signed audit envelopes, and can delegate final allow or deny decisions to either the local runtime adapter or a remote runtime service. Use it when you want to secure a tool server inside the same process as your application. For outbound messages, raw API calls, or other application-level checks that are not naturally wrapped as server calls, use Guard API.

Choose an Entrypoint

The public middleware surface has three integration styles:
APIUse it when
sec0SecurityMiddleware(options)You want full control over adapters and runtime wiring
sec0LocalMiddleware(options)You want a zero-control-plane integration for local policy, local runtime decisions, and local audit
sec0HostedMiddleware(options)You want control-plane policy fetch, approval verification, escalation creation, and optional remote runtime evaluation
The preset builders are also exported when you want to inspect or extend the generated options before wrapping the server:
  • createLocalSec0Preset(...)
  • createHostedSec0Preset(...)

Local Preset

Use the local preset first unless you already need hosted policy or approvals:
import fs from "node:fs";
import { parsePolicyYaml } from "sec0-sdk/policy";
import { LocalDevSigner } from "sec0-sdk/signer";
import { sec0LocalMiddleware } from "sec0-sdk/middleware";

const server = createYourMcpServer();
const policy = parsePolicyYaml(fs.readFileSync("./policy.yaml", "utf8"));

sec0LocalMiddleware({
  policy,
  signer: LocalDevSigner.fromKeyRef(policy.signing.key_ref),
  otel: {
    endpoint: policy.observability.otlp_endpoint,
    serviceName: "orders-mcp",
    serviceVersion: "1.0.0",
    environment: "dev",
    tenant: policy.tenant,
  },
  sec0: { dir: ".sec0", retentionDays: 30 },
  telemetry: { enabled: false },
})(server);
This preset wires:
  • a static policy provider
  • a local runtime adapter
  • a local audit sink
  • no-op approval verification and escalation reporting

Hosted Preset

Switch to the hosted preset when policy should come from the control plane or when denies need approval verification and escalation creation:
import { sec0HostedMiddleware } from "sec0-sdk/middleware";
import { LocalDevSigner } from "sec0-sdk/signer";

sec0HostedMiddleware({
  policy: {
    source: "control-plane",
    level: "middleware",
    scope: "auto",
    refreshTtlMs: 60_000,
  },
  auth: { apiKey: process.env.SEC0_API_KEY! },
  signer: LocalDevSigner.fromKeyRef("file://./.sec0/keys/ed25519.key"),
  otel: {
    endpoint: "http://127.0.0.1:4318",
    serviceName: "orders-mcp",
    serviceVersion: "1.0.0",
    environment: "prod",
    tenant: "my-app",
  },
  sec0: { dir: ".sec0", retentionDays: 30 },
  runtime: {
    enforcement: {
      mode: "remote",
      failureMode: "local",
      remote: {
        endpoint: process.env.SEC0_RUNTIME_URL!,
        apiKey: process.env.SEC0_RUNTIME_API_KEY,
        timeoutMs: 3000,
      },
    },
  },
})(server);
Hosted mode keeps the same wrapping behavior, but the preset changes the adapter wiring:
  • policy can be fetched from the control plane
  • approval verification can be delegated to the control plane
  • escalation creation can be delegated to the control plane
  • runtime decisions can stay local or be delegated to a remote runtime endpoint
auth is the credential used for policy and approval APIs. The top-level apiKey option is separate and only helps auto-wire audit uploads when you are not supplying sec0.presign yourself.

Full-Control Wiring with sec0SecurityMiddleware

Use the base entrypoint when you need custom adapter implementations:
import { sec0SecurityMiddleware, type MiddlewareOptions } from "sec0-sdk/middleware";
import type {
  PolicyProvider,
  ApprovalVerifier,
  EscalationReporter,
  AuditSink,
  RuntimeInvoker,
} from "sec0-sdk/core";

const policyProvider: PolicyProvider = {
  async getPolicy() {
    return { policy, hash: "static-policy-hash" };
  },
};

const approvalVerifier: ApprovalVerifier = { async verify() { return null; } };
const escalationReporter: EscalationReporter = {
  async create(input) {
    return { id: `esc-${input.violation}`, status: "pending" };
  },
};
const auditSink: AuditSink = { async append() { /* write to your sink */ } };
const runtimeInvoker: RuntimeInvoker = {
  async evaluate() {
    return {
      protocolVersion: "2026-02-01",
      adapterMode: "local",
      evaluationSource: "local",
      decision: "allow",
      reasons: [],
      obligations: [],
      auditRefs: [],
    };
  },
};

const options: MiddlewareOptions = {
  policy,
  signer,
  otel,
  sec0: { dir: ".sec0" },
  adapters: {
    policyProvider,
    approvalVerifier,
    escalationReporter,
    auditSink,
    runtimeInvoker,
  },
};

sec0SecurityMiddleware(options)(server);
The contracts above are exported from sec0-sdk/core.

What the Middleware Enforces

When the wrapper is installed, the middleware:
  1. Freezes the tool registry and blocks handler swaps after wrapping.
  2. Hashes server and handler source to detect version or code drift.
  3. Evaluates policy on every callTool.
  4. Applies optional Agent Guard, compliance, skill scan, SAST, and DAST hooks.
  5. Emits a signed audit envelope for allow and deny outcomes.
  6. Adds escalation metadata when the deny reason is configured for escalation.
Common deny reasons include:
  • tool_not_in_allowlist
  • version_unpinned
  • missing_idempotency_for_side_effect
  • agent_guard_failed
  • server_code_changed
  • tool_code_changed
  • skill_scan_failed
  • sast_failed
  • dast_failed
See Policy Schema Reference for the full deny and escalate reason set.

Policy Sources

policy accepts three shapes:
  • a parsed PolicyObject
  • a YAML string
  • a control-plane source descriptor
sec0SecurityMiddleware({
  policy: {
    source: "control-plane",
    level: "middleware",
    scope: "agent",
    nodeId: "payments-agent",
    fallbackToBase: true,
    refreshTtlMs: 60_000,
  },
  auth: { apiKey: process.env.SEC0_API_KEY },
  signer,
  otel,
  sec0: { dir: ".sec0" },
})(server);
Use scope: "base" when the same middleware policy applies to every node. Use scope: "agent" or scope: "auto" when the executing nodeId should influence policy lookup.

Remote Runtime Decisions

By default, middleware decisions are evaluated locally. To move final allow or deny resolution behind a remote service:
sec0SecurityMiddleware({
  policy,
  signer,
  otel,
  sec0: { dir: ".sec0" },
  runtime: {
    enforcement: {
      mode: "remote",
      protocolVersion: "2026-02-01",
      failureMode: "local",
      remote: {
        endpoint: process.env.SEC0_RUNTIME_URL!,
        apiKey: process.env.SEC0_RUNTIME_API_KEY,
        timeoutMs: 3000,
        maxRetries: 2,
      },
    },
  },
})(server);
failureMode: "local" keeps a local fallback when the remote runtime is unavailable. allow and deny are available when you want fail-open or fail-closed behavior instead.

Crossing a Network Boundary

When tool execution crosses a network boundary, use the bridge function:
import { callToolViaGateway } from "sec0-sdk/middleware";

const result = await callToolViaGateway({
  gatewayBaseUrl: "https://your-gateway:8088",
  server: "vision-mcp",
  toolAtVersion: "fetch@1.0",
  args: { url: "https://api.example.com/data" },
  authHeader: `Bearer ${userAccessToken ?? process.env.SVC_TOKEN}`,
  cause: { traceId, spanId },
  agentState: manager.agent.snapshot(),
});
If you already have agent metadata but not a full agentState payload, use callToolViaGatewayWithAgent(...) instead.

Reference