Configuration schema

Full schema for mcpgw.yaml. The default search path is /etc/mcpgw/mcpgw.yaml; override with --config=/path/to/file.yaml on mcpgw server.

mcpgw validates the entire config at startup and on every SIGHUP. Validation errors are fatal at startup and rejected on reload (the previous config remains in effect).


Top-level keys

KeyTypeDefaultRequiredHot-reload?
listenstring host:portyesno
tlsobjectnilnopartial
input_rate_limitobjectdisablednoyes
licenseobjectyesno
default_upstreamstring""recommendedno
upstreamslist of upstream[]yesno
routeslist of route[]nono
authobjectdisablednopartial
policyobjectyesyes
telemetryobjectnopartial
auditobjectyesyes
proxy_protocolobjectnilnono
tool_searchobjectdisablednono

Hot-reload column: “yes” means SIGHUP re-applies; “no” means restart required; “partial” means some sub-keys reload and others do not (called out per-section).


listen

listen: 0.0.0.0:7332

host:port the gateway binds to. Use 0.0.0.0 to accept from any interface; use 127.0.0.1 to restrict to localhost. The gateway exits non-zero if the port is in use.


tls

tls:
  cert_file: /etc/mcpgw/tls/fullchain.pem
  key_file:  /etc/mcpgw/tls/privkey.pem
  client_ca: /etc/mcpgw/tls/client-ca.pem    # optional — enables mTLS

When cert_file and key_file are set, listen serves HTTPS. When tls is absent, listen serves plaintext HTTP. cert_file and key_file must be configured together.

client_ca enables mutual TLS: every client connection must present a certificate signed by the listed CA file. client_ca requires cert_file and key_file.

On SIGHUP, mcpgw re-reads cert_file, key_file, and client_ca from the current config. If the reload fails, the previous certificate remains active. Changing TLS from disabled to enabled, or enabled to disabled, requires a restart.


input_rate_limit

input_rate_limit:
  enabled: true
  requests_per_second: 50
  burst: 100

Protects the gateway itself from request floods before body read, auth, JSON-RPC parse, routing, policy, or upstream forwarding. Buckets are keyed by the TCP peer address from RemoteAddr; X-Forwarded-For is intentionally ignored.

This limiter is separate from policy.rules[].action: rate_limit. Use input_rate_limit for gateway resource protection and policy rate limits for per-tool quotas.

FieldTypeDefaultNotes
enabledboolfalseWhen false, no input throttle is applied.
requests_per_secondfloatToken refill rate. Required and must be > 0 when enabled.
burstintBucket capacity. Required and must be > 0 when enabled.

On SIGHUP, mcpgw replaces the input limiter with the new config. Existing buckets are not preserved across reload.


license

license:
  path: /etc/mcpgw/license.jwt
  grace_days: 30        # optional, defaults to 30
FieldTypeDefaultNotes
pathstringAbsolute path to the JWT file. Must be 0600 and readable by the mcpgw process owner.
grace_daysint30Days after exp during which the gateway fails open. Override per-deployment for compliance.

See License JWT claims for the JWT structure and Explanation: licensing for fail-open / fail-closed behavior.


default_upstream

default_upstream: filesystem

Logical upstream name (must match an upstreams[].name) used for MCP methods that have no tool_name: initialize, tools/list, resources/list, prompts/list, ping. Without it, spec-compliant clients fail at handshake with 404 no_route.

If unset, methods without a tool name return 404. There is no “broadcast to all upstreams” mode today; multi-upstream tools/list aggregation is on the roadmap.


upstreams

upstreams:
  - name: filesystem
    url: http://mcp-fs:9000
    timeout: 30s             # optional, default 30s
    max_idle_conns: 32       # optional, default 32
FieldTypeDefaultNotes
namestringUnique logical name. Appears in mcp.upstream span attribute. Lowercase recommended.
urlstringBase URL. Scheme must be http or https; host required. Path is the upstream’s MCP endpoint (most servers use /).
timeoutduration30sPer-request deadline. Includes connection, request, and response read.
max_idle_connsint32HTTP connection pool size for this upstream.

URLs are validated at startup. default_upstream must reference a known name.


routes

routes:
  - match: { tool_prefix: "fs_" }
    upstream: filesystem
  - match: { tool_prefix: "git_" }
    upstream: git
  - match: { tool_name_in: ["db_query", "db_schema"] }
    upstream: database

Evaluated top-down. First match wins. Routes support the same tool-name matcher fields as policy rules: tool_name, tool_prefix, tool_glob, tool_regex, and tool_name_in. Set at most one matcher per route. Tool calls that match no route and fall outside default_upstream’s remit return 404 no_route.


auth

auth:
  enabled: true
  header: Authorization
  scheme: Bearer
  keys:
    - id: claude-desktop-prod
      hash: "argon2id$v=19$m=65536,t=3,p=2$..."
      scopes: ["*"]
      created_at: "2026-05-01T14:23:01Z"
      expires_at: "2026-08-01T14:23:01Z"
FieldTypeDefaultHot-reload?Notes
enabledboolfalseyesWhen false or absent, /mcp preserves unauthenticated v1 behavior.
headerstringAuthorizationnoHeader to read.
schemestringBearernoPrefix before the key. Set to "" to read the raw header value.
keys[].idstringyesHuman-readable key id. Appears in audit and spans. Must be unique.
keys[].hashstringyesArgon2id hash from mcpgw key generate.
keys[].scopeslist[]yesReserved for future per-key authorization. Parsed and preserved, but not enforced in this release.
keys[].created_attimestampzeroyesInformational.
keys[].expires_attimestampzeroyesOptional. Zero means never expires.

If auth.enabled: true, keys must be non-empty. Missing, invalid, and expired keys return HTTP 401 with JSON-RPC -32005 unauthorized.

Use How-to: enable API-key authentication for the full recipe.

auth.oauth

auth:
  enabled: true
  oauth:
    enabled: true
    issuer: https://idp.example.com
    audience: mcpgw
    jwks_url: https://idp.example.com/.well-known/jwks.json
    required_scopes: ["mcp:read"]
    jwks_cache_ttl: 5m
    leeway: 30s
    metadata_path: /.well-known/oauth-protected-resource
    public_url: https://gateway.example.com

When auth.oauth.enabled: true, the gateway accepts RFC 6750 Bearer tokens in addition to (or instead of) API keys. The gateway fetches JWKS from jwks_url, caches keys for jwks_cache_ttl, and validates each token for issuer, audience, expiry, and (optionally) required scopes.

auth.enabled: true is required when auth.oauth.enabled: true.

FieldTypeDefaultHot-reload?Notes
auth.oauth.enabledboolfalseyesEnables OAuth 2.1 Bearer-token verification. Requires auth.enabled: true.
auth.oauth.issuerstringnoRequired when enabled. Must be an HTTP or HTTPS URL with a non-empty host. Matched against the JWT iss claim.
auth.oauth.audiencestringnoRequired when enabled. Matched against the JWT aud claim.
auth.oauth.jwks_urlstringnoRequired when enabled. Must be an HTTP or HTTPS URL with a non-empty host. Used to fetch the signing key set.
auth.oauth.required_scopeslist[]yesOptional. Every listed scope must appear in the token’s scope (or scp) claim. An empty list disables scope enforcement.
auth.oauth.jwks_cache_ttlduration5myesHow long fetched JWKS are cached before re-fetching.
auth.oauth.leewayduration30syesClock-skew tolerance applied to exp and nbf checks.
auth.oauth.metadata_pathstring/.well-known/oauth-protected-resourcenoHTTP path where the gateway serves RFC 9728 protected-resource metadata.
auth.oauth.public_urlstringderived from listennoBase URL advertised in WWW-Authenticate headers and the metadata document. Required in production when listen binds 0.0.0.0; the gateway logs a warning if unset in that case. Must be an HTTP or HTTPS URL with a non-empty host.
auth.oauth.cimd.enabledboolfalseyesMaster switch. When false (default), no CIMD fetches occur.
auth.oauth.cimd.timeoutduration5syesPer-fetch HTTP timeout. First-request latency: the CIMD fetch is synchronous in the auth path. The first request from a previously-unseen URL-format client_id can block for up to this duration if the endpoint is unresponsive (the request still completes — errors are silenced — but latency is affected). Subsequent requests are served from cache.
auth.oauth.cimd.max_body_bytesint6465536 (64 KiB)yesMaximum response body size. Larger responses are rejected (silently — CIMD failures never block authenticated requests).
auth.oauth.cimd.cache_ttlduration1hyesHow long fetched metadata is cached. The cache is keyed by URL and bounded by a 1024-entry LRU (least-recently-used eviction on insert; reads promote). The cap is not currently exposed in YAML — change it at the Go API level if a different ceiling is needed. Hot reload (SIGHUP) creates a fresh fetcher with a fresh cache.
auth.oauth.cimd — Client ID Metadata Documents

When the verified JWT’s client_id (or azp fallback) is an https:// URL, mcpgw fetches that URL and surfaces the resulting client_name field as audit.oauth_client_name and the mcp.auth.oauth_client_name span attribute. CIMD failures are silent: a misbehaving CIMD endpoint cannot block authenticated requests, only degrade audit richness.

auth:
  enabled: true
  oauth:
    enabled: true
    # ... other oauth fields ...
    cimd:
      enabled: true
      timeout: 5s
      max_body_bytes: 65536
      cache_ttl: 1h

Hot reload (SIGHUP) creates a fresh fetcher with a fresh cache. Toggling enabled: false and sending SIGHUP atomically disables CIMD fetching.


tool_search:
  enabled: true
  mode: synthesize
  refresh_interval: 60s
  max_results: 20

Tool-search synthesis. When enabled in synthesize mode, the gateway intercepts tools/list and tools/call mcp_search, serving them from a refreshed index of upstream tool definitions. In passthrough mode (the default), tools/list is forwarded to the upstream unchanged.

See Enable tool-search synthesis for the operator workflow and Tool-search tradeoffs for design rationale.

FieldTypeDefaultHot-reload?Description
tool_search.enabledboolfalsenoMaster switch. When false, tool_search.mode is ignored and the upstream receives all requests unchanged.
tool_search.modeenumpassthroughnopassthrough (no interception, default — the gateway forwards tools/list to the upstream as-is. Even with enabled: true, the index is NOT built in this mode; this combination is a no-op preserved for forward compatibility with future per-tenant mode toggling.) or synthesize (the gateway answers tools/list from a synthesized stub and tools/call mcp_search from the index). Mode change requires restartSIGHUP only refreshes the index.
tool_search.refresh_intervalduration60snoHow often the gateway re-fetches tools/list from each upstream to rebuild the index.
tool_search.max_resultsint20noHard cap on results returned by mcp_search. Caller-provided limit arguments above this value are clamped silently.

tool_search is not hot-reloadable. Any change to tool_search.mode, tool_search.enabled, refresh_interval, or max_results requires a full restart.

Failing upstreams: negative-cache backoff. A tools/list fetch that errors puts that upstream into exponential backoff (initial 30s, doubling per consecutive failure, capped at 30m). Subsequent refresh cycles skip backed-off upstreams silently — their previous error is still surfaced in lastErrors so metrics scrapers continue to observe the issue, but no HTTP call is made. A successful fetch resets the backoff. Partial-failure semantics are preserved: a single healthy upstream still populates the merged index, and a stuck upstream does not block the merge. The schedule (30s initial / 30m ceiling) is not exposed in YAML.


policy

policy:
  default_action: allow
  rules:
    - id: deny-shell
      action: deny
      when: { tool_name: "shell_exec" }
    - id: rl-fs-write
      action: rate_limit
      when: { tool_name: "fs_write" }
      tokens_per_second: 10
      burst: 20
    - id: redact-secrets
      action: redact
      when: { tool_name: "*" }
      redact:
        - regex: 'Bearer [A-Za-z0-9._-]+'
          replacement: "[REDACTED]"

See Policy rule schema for full grammar. Hot-reloadable: SIGHUP re-applies the entire policy block atomically.

default_action controls unmatched tool calls:

ValueDefaultNotes
allowyesPreserves v1 behavior: unmatched tool calls pass through.
denynoExplicit allowlist mode. Unmatched tool calls return -32001 policy_denied and audit as rule_id: "default_deny".

Policy rule action values:

ValueDescription
allowForward the request unchanged.
denyReject the request. HTTP 403, JSON-RPC -32001 policy_denied.
redactRewrite matching byte sequences in the request body before forwarding. Requires a redact[] list on the rule.
rate_limitToken-bucket rate limit per (rule, session). HTTP 429, JSON-RPC -32003 rate_limited when the bucket is empty. Requires tokens_per_second and burst.
strip_appParses the tool-call response and removes content blocks where type == "ui" or mimeType starts with "application/vnd.mcp-ui". Applies to both JSON and SSE responses. The stripped audit field records how many blocks were removed.

Policy when fields:

FieldTypeNotes
methodstringJSON-RPC method exact match (e.g. elicitation/create, sampling/createMessage). Empty or absent matches any method.
directionenum"" or absent (any direction, default), "client_to_server" (inbound requests only), "server_to_client" (SSE response frames only). Setting "server_to_client" enables SSE-frame inspection on responses from the upstream.
tool_namestringExact tool name. "*" matches any tool.
tool_prefixstringTool name prefix match.
tool_globstringGlob pattern over tool name.
tool_regexstringGo RE2 regex over tool name.
tool_name_inlistTool name allowlist; matches if the tool name is in the list.

Set at most one tool matcher (tool_name, tool_prefix, tool_glob, tool_regex, or tool_name_in) per rule. method and direction may be combined with any tool matcher or used alone.


telemetry

telemetry:
  customer:
    enabled: true
    endpoint: http://datadog-agent:4318
    service_name: mcpgw
    resource_attrs:
      env: production
      version: v1.0.0
  operator:
    enabled: false
    endpoint: ""
    service_name: ""

customer is the OTLP/HTTP exporter for your observability backend. operator is reserved for a future release — vendor-side anonymous usage telemetry, off by default, opt-in only. Today the operator block is parsed but inert.

FieldTypeDefaultHot-reload?Description
customer.enabledboolfalseno
customer.endpointstringno
customer.service_namestringmcpgwno
customer.resource_attrsmap[string]string{}no

endpoint accepts a bare host:port; mcpgw appends /v1/traces. To override the path (OTLP-compatible proxies mounted under non-default paths), pass the full URL.


audit

audit:
  path: /var/log/mcpgw/audit.jsonl
  max_size_mb: 100
  compress_rotated: true
  sinks:
    - type: gcs
      bucket: acme-mcpgw-audit
      prefix: prod/
      flush_interval: 60s
      flush_batch_lines: 10000
      flush_batch_bytes: 5000000
      compress: true
    - type: s3
      bucket: acme-mcpgw-audit
      region: us-east-1
      prefix: prod/
      flush_interval: 60s
      flush_batch_lines: 10000
      flush_batch_bytes: 5000000
      compress: true
      object_lock: true
      retention_days: 2555
    - type: kafka
      brokers: ["kafka-1:9092", "kafka-2:9092"]
      topic: mcpgw-audit
      partition_strategy: hash
      partition_key: session_id
      acks: all
      compression: zstd
      flush_interval: 5s
      flush_batch_lines: 5000
      flush_batch_bytes: 1000000
      tls:
        enabled: true
      sasl:
        mechanism: scram-sha-512
        username_env: KAFKA_USERNAME
        password_env: KAFKA_PASSWORD
    - type: webhook
      url: https://siem.acme.com/ingest
      method: POST
      headers:
        Authorization: "Bearer ${SIEM_TOKEN}"
      retry:
        max_attempts: 5
        backoff: exponential
        initial_interval: 1s
        max_interval: 60s
FieldTypeDefaultHot-reload?
pathstringyes
max_size_mbint100yes
compress_rotatedbooltrueyes
sinks[].typestringyes
sinks[].bucketstringyes
sinks[].regionstringyes
sinks[].prefixstring""yes
sinks[].object_lockboolfalseyes
sinks[].retention_daysint2555yes
sinks[].brokerslistyes
sinks[].topicstringyes
sinks[].partition_strategystringhashyes
sinks[].partition_keystringsession_idyes
sinks[].acksstringallyes
sinks[].compressionstringzstdyes
sinks[].tls.enabledboolfalseyes
sinks[].tls.ca_filestringsystem rootsyes
sinks[].tls.cert_filestringyes
sinks[].tls.key_filestringyes
sinks[].tls.skip_verifyboolfalseyes
sinks[].sasl.mechanismstring""yes
sinks[].sasl.username_envstring""yes
sinks[].sasl.password_envstring""yes
sinks[].urlstringyes
sinks[].headersmap{}yes
sinks[].flush_intervaldurationsink-specificyes
sinks[].flush_batch_linesintsink-specificyes
sinks[].flush_batch_bytesintsink-specificyes
sinks[].retry.max_attemptsint3yes
sinks[].retry.backoffstringexponentialyes
sinks[].retry.initial_intervalduration1syes
sinks[].retry.max_intervalduration30syes

mcpgw rotates the file when its size exceeds max_size_mb. Rotated files are renamed to <path>.<unix-millis> and asynchronously gzipped if compress_rotated: true. mcpgw never deletes rotated files.

Audit sinks are additive best-effort copies. The local JSONL file remains canonical; sink failures are logged and do not block /mcp requests. GCS uses Google Application Default Credentials. S3 uses the AWS SDK default credential chain with the configured region. Kafka writes one audit JSONL line per record and uses Kafka-native batching/compression.


proxy_protocol (reserved)

proxy_protocol:
  enabled: true
  trusted_cidrs:
    - 10.0.0.0/24

Reserved for a future release. Today this block is parsed but rejected at startup with a clear error. See How-to: behind a load balancer for the supported alternatives.


Validation rules

The startup validator enforces these invariants. Any failure is fatal; on SIGHUP reload, the previous config remains in effect.

  • upstreams[].name is unique
  • upstreams[].url parses and has scheme http/https and a non-empty host
  • default_upstream, if set, references a known upstream name
  • routes[].upstream references a known upstream name
  • input_rate_limit.requests_per_second is > 0 when input_rate_limit.enabled is true
  • input_rate_limit.burst is > 0 when input_rate_limit.enabled is true
  • auth.keys is non-empty when auth.enabled is true
  • auth.keys[].id is unique
  • auth.keys[].hash parses as Argon2id
  • auth.enabled: true is required when auth.oauth.enabled: true
  • auth.oauth.issuer, auth.oauth.jwks_url, and auth.oauth.public_url (when set) must use http or https scheme with a non-empty host
  • audit.sinks[].type is gcs, s3, kafka, or webhook
  • audit.sinks[type=gcs].bucket is non-empty
  • audit.sinks[type=s3].bucket and region are non-empty
  • audit.sinks[type=kafka].brokers is non-empty and every broker is host:port
  • audit.sinks[type=kafka].topic is non-empty and has no whitespace
  • audit.sinks[type=kafka].partition_strategy is round_robin, hash, sticky, or absent
  • audit.sinks[type=kafka].partition_key is session_id, auth_key_id, tool_name, method, request_id, or absent
  • audit.sinks[type=kafka].acks is 0, 1, all, or absent
  • audit.sinks[type=kafka].compression is none, gzip, snappy, lz4, zstd, or absent
  • audit.sinks[type=kafka].tls.cert_file and key_file are set together and parse as an X.509 key pair
  • audit.sinks[type=kafka].tls.ca_file, if set, contains at least one PEM certificate
  • audit.sinks[type=kafka].sasl.mechanism is plain, scram-sha-256, scram-sha-512, or absent
  • audit.sinks[type=webhook].url is HTTPS unless allow_insecure: true
  • policy.rules[].id is unique
  • policy.default_action is allow, deny, or absent
  • policy.rules[].action is one of allow, deny, redact, rate_limit, strip_app
  • policy.rules[].when sets at most one tool matcher
  • policy.rules[].when.direction is "", "client_to_server", "server_to_client", or absent
  • policy.rules[].when.tool_glob parses as a valid glob
  • policy.rules[].when.tool_regex compiles as Go regexp
  • policy.rules[].when.tool_name_in is non-empty when set
  • policy.rules[].redact[].regex compiles as Go regexp (RE2 syntax)
  • policy.rules[].redact[].jsonpath is not set (rejected; reserved for a future release)
  • policy.rules[].tokens_per_second is > 0 for action: rate_limit
  • audit.path is writable by the gateway user
  • license.path exists, is regular, and has mode 0600 or stricter