Skip to content

Hosting the MCP Server

MCP Server Integration covers local mode: one process per user, launched by an AI assistant over stdio, with no authentication. This page covers hosted mode (--mode hosted / IAM_VALIDATOR_MCP_MODE=hosted): a shared server over HTTP, with authentication, an operator-owned immutable config, audit logging, and a production ASGI entry point. For running that same hosted server on AWS Lambda instead of a container, see MCP Lambda.

Local vs. hosted, at a glance

Local Hosted
Transport stdio HTTP (Streamable HTTP)
Auth none required (--auth must be set explicitly)
Config can be overridden per session via set_config resolved once at startup, immutable for the process lifetime
Filesystem access validate_policies accepts path/glob not available — inline policies only
analyze_policy credentials caller's own AWS profile (profile param) the server's own credentials; no profile param
Audit logging none one JSON record per tool call

Enabling hosted mode

iam-validator-mcp --mode hosted --transport http --auth token --config ./config.yaml

mode='hosted' refuses to start with auth='none' unless --auth none was passed explicitly — there's no way to silently end up with an unauthenticated IAM tool exposed over HTTP.

Auth providers

Set with --auth/IAM_VALIDATOR_MCP_AUTH: none (hosted mode requires this be explicit), token (static bearer tokens), jwt, or an identity provider name — azure, google, github, keycloak, auth0, workos.

All provider material (token files, JWT keys, IdP client secrets) comes from IAM_VALIDATOR_MCP_AUTH_* environment variables or a file path they name — never a CLI flag, which would land in shell history, ps output, and container-inspect output. For example, --auth token requires IAM_VALIDATOR_MCP_AUTH_TOKEN_FILE (a path) or IAM_VALIDATOR_MCP_AUTH_TOKENS (inline JSON mapping token → {client_id, scopes?}); --auth jwt requires IAM_VALIDATOR_MCP_AUTH_JWT_ISSUER, _JWT_AUDIENCE, and exactly one of _JWT_JWKS_URI / _JWT_PUBLIC_KEY.

Scope-based tool gating

A token's scopes determine which tools/resources/prompts it can see, via a scope → tag mapping:

Scope Tag Gates
iam:validate validate validate_policies, describe_checks
iam:query query query
iam:analyze analyze analyze_policy
iam:config orgconfig get_config (set_config never registers in hosted mode)

A caller missing a scope doesn't see an authorization error — the tool (and its resource twin, e.g. iam://checks for iam:validate) is simply absent from list_tools()/list_resources(), so an unauthorized caller can't even discover the scope taxonomy.

Immutable config and config_digest

In hosted mode, the config named by --config/IAM_VALIDATOR_MCP_CONFIG is resolved once at process startup and never reread — set_config doesn't register at all in hosted mode, so there is no way to mutate it short of redeploying. A missing, unreadable, schema-invalid config file, or a declared custom check that fails to import, fails startup outright rather than falling back to defaults.

Every startup computes a config_digest: a stable SHA-256 over the resolved config plus each check's (id, source, enabled, severity) — the registry is included, not just the YAML, because a check can be added via an installed package's entry point without appearing in the config file at all. get_config returns it, and every validate_policies response carries it as config_digest, so a finding can always be traced back to the exact config version that produced it.

Request limits

Five ServerSettings limits bound what a hosted validate_policies/analyze_policy call can cost, each raising a client-visible error when exceeded (max_response_bytes degrades the response instead — dropping summary, then issues, then whole entries — before falling back to truncation):

Setting Env var Default
--max-policies IAM_VALIDATOR_MCP_MAX_POLICIES 50
--max-policy-bytes IAM_VALIDATOR_MCP_MAX_POLICY_BYTES 1 MiB
--max-request-bytes IAM_VALIDATOR_MCP_MAX_REQUEST_BYTES 8 MiB
--request-timeout-s IAM_VALIDATOR_MCP_REQUEST_TIMEOUT_S 60s
--max-response-bytes IAM_VALIDATOR_MCP_MAX_RESPONSE_BYTES 4 MiB

analyze_policy is separately bounded by --allowed-regions (default: the server's own AWS region only) and --analyze-rate-limit (default 10/min — a best-effort in-process guard, not an AWS quota), since it spends the server's own AWS credentials and access-analyzer:ValidatePolicy quota on every call.

Audit logging

Every hosted tool call emits exactly one JSON record on the iam_validator.mcp.audit logger — on success, a tool error, a cancellation, or any other exception. Fields: timestamp, tool, subject (the authenticated caller, or "anonymous" under --auth none), scopes, config_digest, policy_count, duration_s, outcome, and severity_counts. The record is built only from these primitives — it never includes policy content, a finding message, or any other request/response field, so policy content cannot reach the audit log at any level. Local/stdio mode never emits audit records.

Production ASGI serving

iam-validator-mcp --transport http (or iam-validator mcp --transport http) is a local, single-process server suitable for development. For production, run the ASGI app factory under a real server:

uvicorn iam_validator.mcp.asgi:create_app --factory --host 0.0.0.0 --port 8000

create_app() builds one ServerContext and mounts the FastMCP app (with stateless_http=True and host_origin_protection="auto", the MCP-required DNS-rebinding guard) under /mcp, alongside two outer, unauthenticated routes:

  • GET /health — liveness only: {status, uptime_s, version, config_digest, config_source}.
  • GET /ready — readiness: config resolved, registry built, AWS service data warm; 503 until all three are true, 200 after.

config_source in both payloads reports only the kind of config source ("file" or "none") — never the filesystem path — since these routes are deliberately unauthenticated for load-balancer/orchestrator probes. They're declared as routes on the outer Starlette app rather than @mcp.custom_routes specifically so they never pass through the mounted FastMCP app's Origin guard or auth provider; /mcp itself still enforces both.

The server binds 127.0.0.1 by default; 0.0.0.0 is an explicit opt-in via --host/IAM_VALIDATOR_MCP_HOST (the Docker image below sets it).

Docker image

The repository root Dockerfile builds a hosted-mode image: it bakes AWS service reference data at build time (iam-validator sync-services) so the running container makes no outbound calls, installs the package as a built wheel rather than an editable source tree, runs as a non-root user, and never sets PYTHONOPTIMIZE/-O (would strip the docstrings describe_checks reads as each check's description). It deliberately leaves IAM_VALIDATOR_MCP_AUTH and any config file unset — you must supply --auth/IAM_VALIDATOR_MCP_AUTH and, if needed, --config/IAM_VALIDATOR_MCP_CONFIG yourself. IAM_VALIDATOR_MCP_CACHE_DIRECTORY points at /tmp/iam-validator-cache; under a read-only container root filesystem, mount a writable volume there or run with --tmpfs /tmp, or startup fails.

docker build -t iam-validator-mcp .
docker run -p 8000:8000 \
  -e IAM_VALIDATOR_MCP_AUTH=token \
  -e IAM_VALIDATOR_MCP_AUTH_TOKENS='{"secret-token": {"client_id": "example"}}' \
  iam-validator-mcp

Reverse proxy requirements

Behind a reverse proxy, disable response buffering for the /mcp Streamable HTTP stream (proxy_buffering off; on nginx, plus X-Accel-Buffering: no) and raise proxy_read_timeout — a buffered or prematurely closed connection breaks the MCP protocol's server-sent-event stream.

Offline operation

iam-validator sync-services --output-dir ./aws_services downloads AWS service reference data ahead of time; point --aws-services-dir/ IAM_VALIDATOR_MCP_AWS_SERVICES_DIR at that directory so a hosted deployment (or an air-gapped one) makes no outbound calls to servicereference.us-east-1.amazonaws.com at runtime. The Docker image does this at build time.