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).
| Key | Type | Default | What it does |
|---|---|---|---|
listen | string 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. |
audit | path | "./mcpdef-audit/audit.log" | The append-only, hash-linked audit ledger (JSONL, one record per governed call). Parent dirs are created on first run. |
policy | path | unset | Reserved for a policy directory. Parsed but unused today — declare rules inline with [[policy]]. |
pins | path | unset (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. |
profile | string | unset | The 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_ms | int (ms) | unset / 0 = no timeout | Per-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_origins | array 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_inflight | int | unset = unlimited | Max concurrent in-flight HTTP requests; excess is shed with 503 + Retry-After: 1 (fail-fast load-shedding). |
wire | legacy | dual | modern | legacy | Which 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.
| Key | Type | Default | What it does |
|---|---|---|---|
per_tool_per_sec | float > 0 | unset = per-tool scope off | Refill rate (tokens/sec) for each tool's own bucket. |
per_tool_burst | float ≥ 1 | = per_tool_per_sec | Per-tool bucket capacity (max burst). |
global_per_sec | float > 0 | unset = global scope off | Refill rate for the gateway-wide bucket (checked before the per-tool one). |
global_burst | float ≥ 1 | = global_per_sec | Gateway-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.
| Key | Type | Default | What it does |
|---|---|---|---|
allow_private | bool | true | Allow private / loopback / unique-local upstream addresses (MCPdef commonly fronts internal or localhost MCP servers). Set false to reach only public upstreams. |
require_https | bool | true | Require 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.
| Key | Type | Default | What it does |
|---|---|---|---|
enabled | bool | false | Turn bearer validation on. When true, issuer, resource, and one of jwks/jwks_uri are required (blank strings count as unset). |
issuer | string (URL) | unset | The authorization server's iss claim value. |
resource | string (URL) | unset | This 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). |
jwks | inline JSON or path | unset | Pinned keys: inline JWKS JSON (a value starting with {) or a path to a JWKS file. Preferred for hardened deploys — no startup network dependency. |
jwks_uri | string (URL) | unset | Fetch 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.
| Key | Type | Default | What it does |
|---|---|---|---|
enabled | bool | false | Start the admin server alongside the gateway. |
listen | string 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.
| Key | Type | Default | What 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_phrases | array of strings | [] | Extra prompt-injection phrases on top of the built-in pack, matched case-insensitively (e.g. "exfiltrate the"). |
secret_substrings | array 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.
| Key | Type | What it does |
|---|---|---|
name | string (non-empty) | The role name matched against token scopes/roles. |
grants | array of strings | Grants 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.
| Key | Type | Default | What it does |
|---|---|---|---|
name | string | required | Identifies the rule; it is the rule written to the audit record on a deny. |
effect | "allow" | "deny" | required | Case-insensitive. Anything else fails mcpdef validate. |
servers | array of globs | omitted = any server | Match on the governed server id. |
tools | array of globs | omitted = any tool | Match on the tool name. |
agents | array of globs | omitted = any caller | Match on the audit identity (e.g. agent:ci-*). |
args | array 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:
| Operator | Type | Matches when |
|---|---|---|
equals | string | the value at path equals the string. |
glob | string | the value at path matches the glob. |
contains | string | the value at path contains the substring. |
exists | bool | the 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).
| Key | Type | What it does |
|---|---|---|
tools | array of globs | Allowlist (deny-by-default). Unset = allow all, subject to deny. |
deny | array of globs | Denies; 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.
| Key | Applies to | Type | Default | What it does |
|---|---|---|---|---|
id | all | string (unique, non-empty) | — | The server's name in policy, audit records, and servers list. |
transport | all | "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. |
spec | all | 2025-11-25 | 2026-07-28 | auto (stdio only) | 2025-11-25 | Which 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. |
command | stdio | argv array | — (required) | The child process to spawn. |
env | stdio | table 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. |
url | streamable-http, sse | string (URL) | — (required) | The upstream endpoint. Goes through the egress/SSRF guard. |
tools | all | array of globs | unset = all (subject to deny) | Allowlist: only these tools are exposed/callable (deny-by-default once set). |
deny | all | array of globs | [] | Glob denies (e.g. delete_*); deny wins over allow. |
profile | all | string | unset | Inherit allow/deny from [profile.<name>] (see resolution rule above). |
resources | — | array | unset | Reserved: resource-URI allowlist. Parsed but not enforced — Phase 1 governs tools. |
wasm | wasm, wasm-component | path | — (required) | The .wasm core module / wasm32-wasip2 component to run in-path under Wasmtime. |
wasm_fuel | wasm, wasm-component | int | 200_000_000 (SandboxLimits::default, mcpdef-sandbox) | Fuel per call (≈ one unit per instruction); exceeding it traps out of fuel. |
wasm_max_memory_mb | wasm, wasm-component | int (MiB) | 64 | Linear-memory ceiling for the module. |
wasm_deadline_ms | wasm, wasm-component | int (ms) | unset / 0 = no bound | Per-call wall-clock deadline via epoch interruption (20 ms tick granularity), on top of the fuel (CPU) bound. |
wasm_allow_egress | wasm-component | array of "ip:port" | [] = deny all | Outbound 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).
| Command | Flags beyond --config | What it does |
|---|---|---|
mcpdef run | --profile <name>, --http | Run 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, --profile | One-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|syslog | Print 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.
- Audit ledger (
[gateway] audit): JSON Lines, oneRecordper line (mcpdef-audit::Record):seq,ts_unix_ms,agent,server,method?,tool?,decision("allow"/"deny"),rule?,latency_ms,prev_hash,hash.hash = SHA-256over a unit-separator-delimited encoding of the fields +prev_hash; the first record chains to 64 hex zeros (GENESIS). Append-only; re-opening resumes the chain from the current head. The hash is computed over the explicit field encoding, not the JSON text, so field order in a line does not matter. - Pin store (
[gateway] pins): TOML,[<server-id>]tables mappingtool = "<hex sha-256>"(mcpdef-pin::PinStore, serde-transparentBTreeMap<String, BTreeMap<String, String>>). Sorted deterministically so it diffs cleanly in version control; written atomically (temp file + rename). The hash covers the tool's governed fields only:name,description,inputSchema,outputSchema,annotations, canonicalized with recursively sorted keys. - JWKS (
[gateway.auth] jwks): a standard JWKS JSON document; keys must carrykidand be RSA or EC (RS256/384/512, PS256, ES256/384).
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