Documentation · v0.2

MCPdef configuration reference

Every knob MCPdef reads, in one place. MCPdef is configured by one TOML file (default mcpdef.toml, overridable per command with --config) plus a handful of CLI flags. MCPdef reads no environment variables — the only env interaction is the variables it injects into stdio upstreams via [server.env] (the token broker). Source of truth: the serde structs in crates/mcpdef/src/config.rs and the clap derive in crates/mcpdef/src/main.rs. A commented, runnable example, mcpdef.example.toml, is reproduced in full under Reference files.

mcpdef validate --config <file> checks everything below structurally (unknown transport, missing command/url/wasm, dead rate-limit buckets, incomplete [gateway.auth], unknown profile references, malformed egress entries) without starting the gateway.

[gateway] #

Struct: GatewayConfig (config.rs).

KeyTypeDefaultWhat it does
listenstring host:port"127.0.0.1:7878"Bind address for the downstream Streamable HTTP listener (mcpdef run --http / mcpdef up). Loopback by default — binding wider is a deliberate act.
auditpath"./mcpdef-audit/audit.log"The append-only, hash-linked audit ledger (JSONL, one record per governed call). Parent dirs are created on first run.
policypathunsetReserved for a policy directory. Parsed but unused today — declare rules inline with [[policy]].
pinspathunset (pinning off)Tool-def pin store (TOML). When set, MCPdef pins each upstream tool's definition (trust-on-first-use) and denies + audits a rug-pull if a definition later drifts. Managed with mcpdef pin / mcpdef diff-tools.
profilestringunsetThe active gateway profile — a [profile.<name>] layered over every server, scoping the whole tool surface an agent sees. Overridable at launch with --profile.
upstream_timeout_msint (ms)unset / 0 = no timeoutPer-call upstream response bound. A wedged upstream is failed as an audited upstream-timeout instead of hanging the gateway; also bounds the connect handshake. A spec = "auto" upstream gets the era probe's 5s on top of this for its opening, since the probe measures something else — see WIRE.md.
allowed_originsarray of strings[]Extra Origin values accepted on the HTTP listener beyond loopback (localhost/127.0.0.1/::1, any port, always allowed; requests with no Origin always pass). Anything else → 403 (DNS-rebinding defense).
max_inflightintunset = unlimitedMax concurrent in-flight HTTP requests; excess is shed with 503 + Retry-After: 1 (fail-fast load-shedding).
wirelegacy | dual | modernlegacyWhich MCP revisions the HTTP listener serves. legacy = 2025-11-25 only (an initialize handshake); modern = the stateless 2026-07-28 only; dual = both on the one endpoint, chosen per request. Defaults to legacy so an upgrade never changes what a deployment accepts. See WIRE.md.

[gateway.rate_limit] #

Struct: RateLimitConfig. Token buckets on the tools/call hot path (checked after the authorization gates, so a flood of denied calls cannot drain the buckets). Over-limit calls get a tool-error result + a rate-limited audit record — the stdio analog of a 429.

KeyTypeDefaultWhat it does
per_tool_per_secfloat > 0unset = per-tool scope offRefill rate (tokens/sec) for each tool's own bucket.
per_tool_burstfloat ≥ 1= per_tool_per_secPer-tool bucket capacity (max burst).
global_per_secfloat > 0unset = global scope offRefill rate for the gateway-wide bucket (checked before the per-tool one).
global_burstfloat ≥ 1= global_per_secGateway-wide bucket capacity.

Validation rejects non-positive rates, bursts < 1, and a rate < 1 without an explicit burst (the defaulted sub-1 bucket could never admit a whole token, so every call would be denied).

[gateway.egress] #

Struct: EgressConfig. The SSRF guard for HTTP upstreams (streamable-http / sse) and the jwks_uri fetch. Cloud-metadata (169.254.169.254), link-local (169.254.0.0/16, fe80::/10), and the unspecified address are always blocked — there is no knob to allow them. Resolved IPs are DNS-pinned to defeat rebinding. Inspect the effective policy with mcpdef egress show.

KeyTypeDefaultWhat it does
allow_privatebooltrueAllow private / loopback / unique-local upstream addresses (MCPdef commonly fronts internal or localhost MCP servers). Set false to reach only public upstreams.
require_httpsbooltrueRequire HTTPS for public destinations (private/loopback may be plain HTTP).

[gateway.auth] — OAuth 2.1 Resource Server #

Struct: AuthConfig. Applies to the HTTP listener only: when enabled, every POST /mcp must carry a valid Authorization: Bearer <JWT>, validated per request (signature against the JWKS, aud == resource, iss == issuer, exp/nbf; asymmetric algorithms only — none/HMAC are rejected). Running mcpdef run without --http while auth is enabled prints a warning: stdio has no per-request transport identity, so auth is not enforced there.

KeyTypeDefaultWhat it does
enabledboolfalseTurn bearer validation on. When true, issuer, resource, and one of jwks/jwks_uri are required (blank strings count as unset).
issuerstring (URL)unsetThe authorization server's iss claim value.
resourcestring (URL)unsetThis gateway's canonical URI — the audience tokens must carry (RFC 8707/9068). Also the origin used to build the WWW-Authenticate metadata URL (never derived from request headers).
jwksinline JSON or pathunsetPinned keys: inline JWKS JSON (a value starting with {) or a path to a JWKS file. Preferred for hardened deploys — no startup network dependency.
jwks_uristring (URL)unsetFetch the JWKS at startup, through the egress/SSRF guard. jwks wins if both are set.

[gateway.admin] — OSS read-only admin / observability server #

Struct: AdminConfig. Prometheus /metrics, a small JSON API, and the built-in status UI — see API.md § Admin listener. Off by default; runs on a separate port from the MCP data path so scraping it never touches the governed hot path. Carries no auth of its own (that's the EE control plane's job) — bind it to loopback or keep it behind your own network policy, same as the audit ledger.

KeyTypeDefaultWhat it does
enabledboolfalseStart the admin server alongside the gateway.
listenstring host:port"127.0.0.1:7879"Bind address for the admin server. Loopback by default, and a different port than [gateway] listen.

[gateway.inspect] — injection / secret-exfil scanning #

Struct: InspectConfig. Inline scanning of the two untrusted-content surfaces an in-path gateway straddles. Tool descriptions are scanned at connect — a "line-jumping" / tool-poisoning attempt hides the tool from tools/list and denies its tools/call. Tool-call results are scanned per call — a response that leaks a credential or carries injected instructions is refused before it reaches the model. A finding is audited under an injection or secret-exfil rule. The built-in pack is a curated, high-precision starter set: prompt-injection phrasing plus secret patterns (AWS, Slack, GitHub and Stripe keys, PEM private-key blocks). It is opt-in — start with warn before enforce.

KeyTypeDefaultWhat it does
mode"off" | "warn" | "enforce""off"off: no scanning. warn: log and audit findings only. enforce: hide poisoned tools and refuse results that leak a secret or carry injected instructions. Any other value fails mcpdef validate.
injection_phrasesarray of strings[]Extra prompt-injection phrases on top of the built-in pack, matched case-insensitively (e.g. "exfiltrate the").
secret_substringsarray of strings[]Extra secret substrings, matched case-sensitively (e.g. an internal credential prefix such as "acme_sk_").

[[role]] — RBAC #

Struct: RoleConfig. Layered over the allowlist for authenticated HTTP callers; with no [[role]] defined the gate is off (the allowlist alone decides). A caller holds a role when the role's name appears among the token's scopes or roles claim.

KeyTypeWhat it does
namestring (non-empty)The role name matched against token scopes/roles.
grantsarray of stringsGrants as "server-glob:tool-glob"; a bare "tool-glob" (no colon) grants it on any server.

[[policy]] — policy-as-code rules #

Struct: PolicyRuleConfig. A richer gate than the allowlist, evaluated after it. Where the allowlist decides on the tool name alone, a rule matches on the caller (agent), the server, the tool (all globs) and the tool-call arguments (per field) — so it can express "deny delete_* on github when args.name is a prod-* repo", or "only agent:ci-* may call deploy".

Rules run top to bottom and the first match wins, so put a specific allow before a broad deny. No match means allowed (the allowlist already gated). A denial is audited under the rule's own name.

KeyTypeDefaultWhat it does
namestringrequiredIdentifies the rule; it is the rule written to the audit record on a deny.
effect"allow" | "deny"requiredCase-insensitive. Anything else fails mcpdef validate.
serversarray of globsomitted = any serverMatch on the governed server id.
toolsarray of globsomitted = any toolMatch on the tool name.
agentsarray of globsomitted = any callerMatch on the audit identity (e.g. agent:ci-*).
argsarray of predicates[]Per-argument predicates, AND-ed. See below.

Match conditions are AND-ed. Omit a condition to match anything for that dimension; an explicit empty list (servers = []) is rejected by mcpdef validate, because it would silently match nothing — a fail-open for a deny.

Each args predicate is a table with a dotted path into the arguments (e.g. "target.env") and exactly one operator:

OperatorTypeMatches when
equalsstringthe value at path equals the string.
globstringthe value at path matches the glob.
containsstringthe value at path contains the substring.
existsboolthe path is present (true) or absent (false).

A predicate with no operator, or with more than one, fails mcpdef validate rather than silently matching nothing.

[[policy]]
name    = "no-delete-prod"
effect  = "deny"
servers = ["github"]
tools   = ["delete_*"]
args    = [ { path = "name", glob = "prod-*" } ]

The transform effect — rewriting arguments or results rather than only allowing or denying — is not built. The separate top-level policy key under [gateway] is reserved and unused (see above).

[profile.<name>] #

Struct: ProfileConfig. A named, reusable allow/deny set — referenced by a server (profile = "<name>") or applied gateway-wide ([gateway] profile).

KeyTypeWhat it does
toolsarray of globsAllowlist (deny-by-default). Unset = allow all, subject to deny.
denyarray of globsDenies; deny wins over allow.

Resolution when a server references a profile: inline tools replaces the profile's allowlist; inline deny is appended (denies accumulate).

[[server]] — the governed upstreams #

Struct: ServerConfig. At least one is required.

KeyApplies toTypeDefaultWhat it does
idallstring (unique, non-empty)—The server's name in policy, audit records, and servers list.
transportall"stdio" | "streamable-http" | "sse" | "wasm" | "wasm-component"—How MCPdef reaches the upstream (SUPPORTED_TRANSPORTS). streamable-http auto-falls back to the legacy SSE bridge on a 400/404/405; sse forces it.
specall2025-11-25 | 2026-07-28 | auto (stdio only)2025-11-25Which MCP revision this server speaks. 2026-07-28 has no initialize, so MCPdef opens it with server/discover and puts the version, its identity and its capabilities on every request. auto probes the server with server/discover and uses what it turns out to speak. Otherwise a fact about the server, not a preference — set it wrong and startup fails naming this knob. See WIRE.md.
commandstdioargv array— (required)The child process to spawn.
envstdiotable of string→string{}Env vars injected into the child — the token-broker path: MCPdef holds the upstream's credential and the client's bearer is never passed through.
urlstreamable-http, ssestring (URL)— (required)The upstream endpoint. Goes through the egress/SSRF guard.
toolsallarray of globsunset = all (subject to deny)Allowlist: only these tools are exposed/callable (deny-by-default once set).
denyallarray of globs[]Glob denies (e.g. delete_*); deny wins over allow.
profileallstringunsetInherit allow/deny from [profile.<name>] (see resolution rule above).
resources—arrayunsetReserved: resource-URI allowlist. Parsed but not enforced — Phase 1 governs tools.
wasmwasm, wasm-componentpath— (required)The .wasm core module / wasm32-wasip2 component to run in-path under Wasmtime.
wasm_fuelwasm, wasm-componentint200_000_000 (SandboxLimits::default, mcpdef-sandbox)Fuel per call (≈ one unit per instruction); exceeding it traps out of fuel.
wasm_max_memory_mbwasm, wasm-componentint (MiB)64Linear-memory ceiling for the module.
wasm_deadline_mswasm, wasm-componentint (ms)unset / 0 = no boundPer-call wall-clock deadline via epoch interruption (20 ms tick granularity), on top of the fuel (CPU) bound.
wasm_allow_egresswasm-componentarray of "ip:port"[] = deny allOutbound TCP allowlist for the sandboxed component. A connect is permitted only if listed and the IP passes the egress classification (metadata/special-use always blocked). Hostnames are not accepted.

CLI flags #

Clap derive: Cli/Cmd in crates/mcpdef/src/main.rs. Every subcommand that reads config takes --config <file> (default mcpdef.toml).

CommandFlags beyond --configWhat it does
mcpdef run--profile <name>, --httpRun the gateway; serves the client over stdio, or over the Streamable HTTP listener with --http. --profile overrides [gateway] profile for this run.
mcpdef up--profile <name>Shorthand for run --http.
mcpdef call <tool>--args <json-object> (default {}), --json, --profileOne-shot governed tool call: allowlist/profile/pin/rate-limit gates + audit apply; RBAC does not (no bearer on this trusted local path). Exits non-zero on a denial or tool error.
mcpdef validate—Structurally validate the config; exit non-zero listing every problem.
mcpdef servers list—Static, config-level view of the governed servers and their resolved allow/deny (profiles applied). Does not connect upstreams.
mcpdef audit verify--path <ledger>, --head <hash> --count <n> (together)Offline hash-chain check; with --head/--count, also checks against a seal recorded out-of-band (catches tail-truncation). Exit non-zero on a break.
mcpdef audit tail--path <ledger>, -n/--lines <n> (default 20), --format json|ocsf|cef|syslogPrint the last N records in a SIEM-ready format, one line per record.
mcpdef egress show—Print the effective SSRF/egress policy.
mcpdef pin—Pin the current tool definitions of all upstreams as approved (writes [gateway] pins; overwrites — this is the re-approval command).
mcpdef diff-tools—Diff current tool definitions against the pin store (read-only); exit non-zero if any pinned tool changed (rug-pull).
mcpdef version—Print version, MCP spec target, and phase.

Data formats & compatibility #

There is no format_version field in any of these files yet (pre-1.0); the shapes below are what mcpdef 0.1.0 reads and writes.

Reference files #

The files this page refers to, exactly as they ship in the source tree.

mcpdef.example.toml #

# mcpdef.example.toml — an MCPdef config (Phase 1 + Phase 1.5).
#
# Implemented upstream transports:
#   stdio            child-process MCP server (Phase 1)
#   streamable-http  current Streamable HTTP, with automatic fallback to the
#                    legacy HTTP+SSE bridge on a 400/404/405 (Phase 1 / 1.5)
#   sse              forced legacy 2024-11-05 HTTP+SSE bridge (Phase 1.5)
#   wasm             an untrusted .wasm core module run in-path under the Wasmtime
#                    sandbox — fuel + memory capped, zero ambient capability (Phase 4)
#   wasm-component   an untrusted wasm32-wasip2 component (the mcpdef:server WIT world)
#                    run under capability-scoped WASI with a per-destination egress
#                    allowlist — same caps, plus opt-in outbound TCP (Phase 4)
#
#   mcpdef validate --config mcpdef.example.toml
#   mcpdef run        --config mcpdef.example.toml    # serve the client over stdio
#   mcpdef run --http --config mcpdef.example.toml    # serve clients over Streamable HTTP
#   mcpdef up         --config mcpdef.example.toml    # shorthand for `run --http`
#   mcpdef call list_issues --args '{}' --config mcpdef.example.toml   # one-shot governed tool call

[gateway]
listen = "127.0.0.1:7878"          # bind addr for `mcpdef run --http` (loopback by default)
audit  = "./mcpdef-audit/audit.log"  # append-only, hash-linked ledger
# profile = "readonly"             # active gateway profile (defined below): layers over EVERY
                                   #   server so an agent sees only that slice. `mcpdef run --profile
                                   #   <name>` overrides this. Must live HERE, under [gateway].
# allowed_origins = ["https://app.example.com"]  # extra Origins beyond loopback (DNS-rebind defense)
# max_inflight = 512               # cap concurrent HTTP requests; excess → 503 + Retry-After
# Which MCP revisions the listener serves. Default `legacy` (2025-11-25 only), so
# upgrading never changes what an existing deployment accepts. `dual` adds the
# stateless 2026-07-28 wire on the same endpoint; `modern` serves only it.
# wire = "dual"
# pins = "./mcpdef-pins.toml"        # enable tool-def pinning + rug-pull detection:
                                   #   on connect MCPdef pins each tool's definition (TOFU); if a
                                   #   server later changes a tool, the call is denied + audited
                                   #   as `rug-pull`. Manage with `mcpdef pin` / `mcpdef diff-tools`.
# upstream_timeout_ms = 30000      # fail a tools/call whose upstream doesn't reply in time
                                   #   (audited as `upstream-timeout`) instead of hanging the gateway.

# Availability: token-bucket rate limits on tools/call (ARCHITECTURE §5b). Over-limit
# calls get a tool-error result + a `rate-limited` audit event (the stdio analog of a 429).
# A scope is active only if its *_per_sec is set; *_burst defaults to the per-sec rate.
[gateway.rate_limit]
per_tool_per_sec = 5               # each tool refills 5 tokens/sec…
per_tool_burst   = 10              # …with a 10-call burst
global_per_sec   = 100             # gateway-wide cap so one agent can't starve others
global_burst     = 200

# Egress / SSRF guard for HTTP upstreams. Cloud-metadata (169.254.169.254),
# link-local (fe80::/10), and the unspecified address are ALWAYS blocked — there
# is no knob to allow them. Resolved IPs are DNS-pinned to defeat rebinding.
# Inspect the effective policy with `mcpdef egress show`.
[gateway.egress]
allow_private = true               # default: MCPdef commonly fronts internal/localhost MCP servers
require_https = true               # default: public destinations must use HTTPS (private/loopback may be http)

# Inline injection / secret-exfil scanning of untrusted content. Tool DESCRIPTIONS
# are scanned at connect (a "line-jumping" / tool-poisoning attempt hides the tool)
# and tool-call RESULTS are scanned per call (a leaked credential or injected
# instruction is refused before it reaches the model). A curated, high-precision
# starter rule pack (prompt-injection phrases + secret patterns: AWS/Slack/GitHub/
# Stripe keys, PEM private keys). Opt-in — start with `warn` (log only) before
# `enforce` (block).
[gateway.inspect]
mode = "off"                       # off (default) · warn (log findings) · enforce (hide/deny)
# Extend the built-in rule pack with your own patterns (both default empty):
injection_phrases = []             # extra injection phrases, matched case-INsensitively (e.g. "exfiltrate the")
secret_substrings = []             # extra secret substrings, matched case-sensitively (e.g. an internal token prefix "acme_sk_")

# OSS read-only admin / observability server: Prometheus `/metrics`, a small JSON
# API (/api/v1/status|servers|stats|audit), and a built-in status UI at `/`. Runs
# on a SEPARATE port from the MCP data path, and carries NO auth of its own — bind
# it to loopback or keep it behind your own network policy / an authenticating
# proxy. Off by default.
[gateway.admin]
enabled = false                    # set true to serve /metrics + the status UI
listen  = "127.0.0.1:7879"         # bind wider (e.g. "0.0.0.0:7879") only behind a network boundary

# OAuth 2.1 termination (HTTP listener only). When enabled, every POST to /mcp
# must carry a valid `Authorization: Bearer <JWT>`; MCPdef validates it per request
# as an OAuth 2.1 Resource Server (signature against the JWKS, `aud == resource`,
# `iss == issuer`, exp/nbf — no `none`/HMAC). A missing/invalid token gets 401 with
# a WWW-Authenticate challenge pointing at the RFC 9728 Protected Resource Metadata
# document served at /.well-known/oauth-protected-resource. The token's `sub` is the
# audit identity; its scopes/roles drive the [[role]] RBAC gate below.
# [gateway.auth]
# enabled  = true
# issuer   = "https://auth.example.com"               # the authorization server's `iss`
# resource = "https://mcpdef.acme.internal/mcp"         # this gateway's canonical URI (the token audience)
# jwks     = "./jwks.json"                            # pinned keys: a JWKS file (or inline JSON starting with `{`)
# jwks_uri = "https://auth.example.com/.well-known/jwks.json"  # …or fetch at startup (egress-guarded). Prefer `jwks`.

# RBAC roles layered over the allowlist for authenticated callers (HTTP + auth).
# A caller "holds" a role when its `name` appears among the token's scopes or
# `roles` claim. A tools/call is allowed only if some held role grants it; grants
# are `"server-glob:tool-glob"` (a bare `"tool"` grants it on any server). With no
# [[role]] defined the gate is off (the allowlist alone decides).
# [[role]]
# name   = "reader"
# grants = ["github:get_*", "github:list_*", "files:read_*"]
# [[role]]
# name   = "github-writer"
# grants = ["github:create_issue", "github:*"]

# Policy-as-code RULES: a richer gate than the allowlist, evaluated AFTER it.
# Where the allowlist decides on the tool name alone, a rule matches on the caller
# (agent), server, tool (all globs), and the tool-call ARGUMENTS (per-field), so it
# can express "deny delete_* on github when args.name is a prod-* repo". Rules run
# top-to-bottom, FIRST MATCH WINS (put a specific allow before a broad deny); no
# match = allowed. A deny is audited under the rule's own `name`. Each arg predicate
# names exactly one of: equals / glob / contains / exists.
# [[policy]]
# name    = "no-delete-prod"
# effect  = "deny"                    # allow | deny
# servers = ["github"]               # optional; globs. omit = any server
# tools   = ["delete_*"]             # optional; globs. omit = any tool
# agents  = ["agent:ci-*"]           # optional; globs on the audit identity. omit = any caller
# args    = [ { path = "name", glob = "prod-*" } ]   # AND-ed; dotted paths (e.g. "target.env")

# Named, reusable allow/deny PROFILES. Define an allow/deny set once and apply it
# across servers (DRY), or scope the whole gateway with one as the active profile.
# Allow + deny entries are globs (`get_*`, `*_secret`).
[profile.readonly]
tools = ["get_*", "list_*", "search", "read_*"]
deny  = ["*_secret"]

[profile.ci-bot]
tools = ["list_issues", "get_file_contents", "create_issue"]

# To make one of the profiles above the active gateway profile, set
# `profile = "..."` under the [gateway] table near the top of this file (NOT here —
# a key after `[profile.ci-bot]` would belong to that profile), or pass
# `mcpdef run --profile <name>` at launch.

# A stdio upstream: only the two listed tools are exposed, and any `delete_*`
# tool is denied even if listed (deny wins). A `tools/call` to anything else is
# denied-by-default and audited.
[[server]]
id        = "github"
transport = "stdio"
command   = ["mcp-server-github"]
tools     = ["list_issues", "get_file_contents"]
deny      = ["delete_*"]
# Which MCP revision this server speaks. Default 2025-11-25 (the `initialize`
# handshake). Set "2026-07-28" for a stateless server: MCPdef then opens it with
# server/discover and stamps every request with the version, its identity and its
# capabilities. "auto" probes the server and uses whichever it turns out to be.
# spec      = "auto"
# Token broker (stdio): MCPdef injects this upstream's brokered credential into the
# child's environment (the spec's mechanism for stdio servers). The credential
# lives in MCPdef's config, not the server's — and a downstream client's bearer is
# never passed through to the upstream. Use real secret management in production;
# the inert placeholder below is deliberately NOT a real-looking token.
[server.env]
GITHUB_TOKEN = "set-me-from-your-secret-store"

# A second stdio upstream that inherits the `readonly` profile (get_*/list_*/…),
# and appends one more deny on top of the profile's `*_secret`.
[[server]]
id        = "files"
transport = "stdio"
command   = ["mcp-server-filesystem", "--root", "./data"]
profile   = "readonly"
deny      = ["write_*"]             # appended to the profile's deny set

# A Streamable HTTP upstream. mcpdef POSTs to this endpoint and, if the server
# only speaks the legacy 2024-11-05 transport (405 on POST), automatically falls
# back to the GET-SSE bridge.
[[server]]
id        = "remote-tools"
transport = "streamable-http"
url       = "https://mcp.internal.example/mcp"
tools     = ["search", "fetch"]

# A forced legacy HTTP+SSE upstream (no probe): GET opens the SSE stream, the
# server's `endpoint` event names the POST URL, and responses return over SSE.
[[server]]
id        = "legacy-tools"
transport = "sse"
url       = "https://legacy.internal.example/sse"
deny      = ["delete_*", "*_secret"]

# An in-path WASM upstream (Phase 4): MCPdef runs the untrusted `.wasm` MCP server
# itself, under Wasmtime, with no child process and no ambient capability. The
# module is instantiated against an EMPTY linker — it cannot touch the filesystem,
# network, or clock (a module importing WASI fails to load, by design). Each call
# is bounded by a fuel budget (CPU) and a linear-memory ceiling. The module speaks
# the gateway's normal MCP envelope over a tiny `alloc`/`handle` ABI (see the
# `mcpdef-sandbox` crate docs). Governance (allowlist, RBAC, scanning, audit) applies
# exactly as it does to a stdio or HTTP upstream.
[[server]]
id        = "sandboxed"
transport = "wasm"
wasm      = "./servers/echo.wasm"  # path to the module to run in the sandbox
tools     = ["echo"]
# wasm_fuel          = 200000000   # fuel granted per call (≈ instructions); unset = default
# wasm_max_memory_mb = 64          # max linear memory the module may grow to; unset = default
# wasm_deadline_ms   = 5000        # per-call WALL-CLOCK deadline: a call running longer than this
                                   #   traps even with fuel to spare (defense-in-depth on top of the
                                   #   fuel/CPU bound). Unset or 0 = no wall-clock bound.

# An in-path WASM *component* upstream (Phase 4). Unlike `wasm` (a core module with
# zero ambient capability), this runs a `wasm32-wasip2` component exporting the
# `mcpdef:server` WIT world under a capability-scoped WASI: it still has no filesystem
# or clock, but it MAY be granted outbound TCP — and only to the destinations listed
# in `wasm_allow_egress` (default: none = deny all). A connection is allowed only if
# its resolved ip:port is listed AND passes the egress IP classification, so
# cloud-metadata / special-use addresses are blocked even if mistakenly listed. Fuel,
# memory, and the wall-clock deadline apply exactly as for `wasm`.
#
# NOTE: entries are `ip:port` (hostnames are a follow-up), and the address must be a
# real destination that is not in an always-blocked class. Private/loopback (allowed
# by default) and public addresses are both fine; cloud-metadata, link-local, and the
# special-use/documentation ranges (e.g. 203.0.113.0/24) are rejected at connect time
# even if listed here.
[[server]]
id        = "sandboxed-component"
transport = "wasm-component"
wasm      = "./servers/scraper.component.wasm"  # a wasm32-wasip2 component
tools     = ["fetch_url"]
wasm_allow_egress = ["10.0.0.7:8443"]           # e.g. the one internal API it may reach
# wasm_deadline_ms  = 2000                       # (optional) wall-clock cap per call