Keep API keys out of the repo when building MCP clients
Store LLM API keys such as ANTHROPIC_API_KEY in a `.env` file (or environment variables / secret managers) and add `.env` to `.gitignore`. Other security practices for MCP clients: validate server responses, be cautious with tool permissions, review the tools a server exposes before allowing model-driven calls, and connect only to servers and executable commands you trust.
Security rules for sandboxed programmatic tool calling
Six security requirements for code mode: per-call authorization — the broker is still the MCP host for spec purposes, so apply the same human-in-the-loop confirmation policy to sandbox-originated calls as to direct calls; approving a script does not grant blanket approval, though hosts may grant categorical approval (e.g. allow `ticketing_createIssue` for this script run) while still evaluating each call against that grant. Cross-server data flow — tool results from one server are untrusted input to another and output truncation alone does not prevent exfiltration. Network isolation — the sandbox should have no direct network access. No credential exposure — API keys and tokens are held by the host, which adds authentication when forwarding. Resource limits — set timeouts and memory limits. Output filtering — validate and truncate sandbox console output before returning it to the model.
Per-action user approval for tool calls
Claude Desktop requests explicit user approval before executing each MCP tool call such as a file system operation; the user can deny the request. Server authors should not assume tools execute without a human-in-the-loop confirmation step.
Filesystem server directory allow-list is passed as CLI args
@modelcontextprotocol/server-filesystem takes the directories it is permitted to access as trailing command-line arguments after the package name; adding or changing paths in the `args` array changes the server's access scope. The server runs with the invoking user's account permissions, so it can do anything the user can do manually — only grant directories you are comfortable exposing.
Multiple remote MCP servers can be connected simultaneously
A client can be connected to several remote MCP servers at the same time. Recommended practice is to organize connectors by purpose or project and to periodically review and remove unused connectors, both for clarity and for security. Users are also advised to verify server authenticity and review requested permissions before connecting.
Remote MCP servers normally require authentication (OAuth, API keys, or username/password)
Most remote MCP servers require authentication before the client can access resources. Implementations commonly use OAuth, API keys, or username/password combinations. During connection the client may redirect the user to a third-party authentication provider or render a form in-app; only after auth completes is the secure connection to the remote server established. A remote server that offers no auth flow will still connect but exposes its capabilities publicly.
Sampling security expectations for clients
Sampling is designed for human-in-the-loop control: clients can require explicit user consent, display the exact prompt, model selection and token limits, let users approve/deny/modify requests and responses, support auto-approval configuration for trusted operations, and offer redaction of sensitive information. Clients should implement rate limiting and validate all message content.
Never request secrets via elicitation form mode
Servers must not use elicitation form mode to request sensitive information such as passwords, API keys, access tokens, or payment credentials. Those interactions belong in URL mode, which keeps the data out of band so it never passes through the client or the LLM context. Clients warn about suspicious requests and let users review form data before sending.
Roots are advisory, not a security boundary
Roots communicate intended filesystem boundaries but do not enforce security restrictions; actual security must be enforced at the operating system level via file permissions and/or sandboxing. The specification says servers "SHOULD respect root boundaries" rather than "MUST enforce" them, because servers run code the client cannot control.
URL-mode elicitation requires explicit consent and no auto-fetch
For URL-mode elicitation, clients must show the full URL and gather explicit user consent before opening it, and must never fetch the URL automatically. The client only learns whether the user consented; the interaction itself stays between the user and the target site.
Human oversight mechanisms for model-controlled tools
Because tools are model-controlled and can be discovered and invoked automatically, applications are expected to implement human oversight: displaying available tools in the UI so users can enable/disable them per interaction, approval dialogs for individual tool executions, permission settings that pre-approve certain safe operations, and activity logs showing all tool executions with their results.
Sanitize logs when debugging
When debugging MCP integrations, sanitize logs by protecting credentials and masking personal information, and verify permissions, authentication and access patterns rather than dumping raw payloads containing sensitive data.
OAuth callback URL must bind a loopback host
An MCP client's OAuth callback URL must bind a loopback host: `localhost`, `127.0.0.0/8`, or `[::1]`. The local listener receives the authorization code over plaintext http, so a non-loopback host is rejected with an error and (in the MCP Inspector) there is no flag to override this. If the browser runs on a different machine, forward the callback port to it.
Non-interactive OAuth flags for CI: --stored-auth-only and --use-stored-auth
By default the Inspector CLI runs the same loopback OAuth flow as the TUI, opening a browser and waiting on a localhost callback that a CI job cannot complete. `--stored-auth-only` never starts interactive OAuth or step-up and never auto-opens a browser: it uses tokens from the shared store if present, otherwise fails immediately with `auth_required` — this is the flag CI wants. `--use-stored-auth` reuses a token the web Inspector already obtained on the machine, refreshing it first when a refresh token is stored.
CLI fails fast with auth_required when there is no TTY
If neither `--stored-auth-only` nor `--use-stored-auth` is given and there is no TTY on stdin or stderr, the Inspector CLI fails fast with `auth_required` rather than hanging for fifteen minutes on an OAuth callback nobody will complete.
servers/show does not scrub credentials in url or stdio args
`servers/show` redacts secret-bearing fields (`env` values, sensitive headers, OAuth client secrets), but it does not scrub credentials embedded in a server `url` (userinfo or query tokens) or in stdio `args`. Treat raw `url` and `detail` fields as sensitive before pasting them into an issue.
OAuth callback URL must be a loopback host
The Inspector's OAuth redirect URI (`--callback-url`, default `http://127.0.0.1:6276/oauth/callback`) must use a loopback host, either `127.0.0.1` or `localhost`. The local callback listener receives the authorization code over plaintext `http`, so any other host is rejected and there is no flag to override this restriction.
Never combine DANGEROUSLY_OMIT_AUTH with DANGEROUSLY_BIND_ALL_INTERFACES
Combining `DANGEROUSLY_OMIT_AUTH` and `DANGEROUSLY_BIND_ALL_INTERFACES` for the MCP Inspector web backend is explicitly warned against: the backend spawns processes and holds OAuth tokens, so anyone who can reach it over the network can drive it.
ALLOWED_ORIGINS replaces the default list and cannot be disabled
`ALLOWED_ORIGINS` replaces the default origin list rather than merging with it, so list every origin you'll browse from including loopback forms, e.g. `ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274`. Each entry must include the scheme; a scheme-less value is dropped with a warning. A blank value does not disable the check, it falls back to the default, and there is no knob to turn origin validation off.
Origin allow-list follows the bind host; TLS/proxy needs ALLOWED_ORIGINS
The Inspector's default origin allow-list follows the bind host, so with `HOST=192.168.1.50` the origin `http://192.168.1.50:6274` is accepted with no further config. Behind TLS or a reverse proxy the browser's `Origin` becomes the public origin and won't match the bind host, so set e.g. `ALLOWED_ORIGINS=https://inspector.example.com`.
Inspector refuses wildcard binds without DANGEROUSLY_BIND_ALL_INTERFACES
The Inspector binds `localhost` by default and its backend spawns processes. It refuses to bind wildcard all-interfaces addresses (`0.0.0.0`, `::`, and every equivalent spelling) unless `DANGEROUSLY_BIND_ALL_INTERFACES=true` is set. Binding a specific address (e.g. `HOST=192.168.1.50`) needs no opt-in, because that is one deliberate exposure rather than every interface, which is the shape DNS-rebinding attacks target.
Never set DANGEROUSLY_OMIT_AUTH on a reachable Inspector
Keep authentication on whatever the hosting shape: do not set `DANGEROUSLY_OMIT_AUTH` on an Inspector instance reachable by anyone but you.
Only one TUI OAuth flow can hold port 6276 (EADDRINUSE)
Because the Inspector TUI's OAuth callback port is fixed, only one TUI OAuth flow can hold it at a time; a second concurrent flow fails with `EADDRINUSE`. Override it with the `--callback-url` flag or the `MCP_OAUTH_CALLBACK_URL` environment variable, using a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when the authorization server registers redirect URIs dynamically.
TUI OAuth callback URL is fixed at http://127.0.0.1:6276/oauth/callback
The Inspector TUI's OAuth callback listener defaults to `http://127.0.0.1:6276/oauth/callback`. The port is fixed deliberately because a pre-registered (static) OAuth client, a Client ID Metadata Document (CIMD), or an enterprise-managed IdP all need a redirect URI known in advance; register that URI once and it works across sessions. On a remote host where the browser runs elsewhere, forward the callback port so the redirect reaches the listener.
Where the TUI reads OAuth client settings from
Per-server OAuth fields in the Inspector catalog (static client id/secret, scopes, the enterprise-managed flag) are applied automatically by the TUI. Install-wide settings such as CIMD and enterprise IdP come from `~/.mcp-inspector/storage/client.json`, the same file the web client's Client Settings dialog writes; point at a different file with `--client-config` or `MCP_CLIENT_CONFIG_PATH`.
localhost and 127.0.0.1 are different redirect URIs
Redirect URIs must match exactly what was registered with the authorization server: `localhost` and `127.0.0.1` are treated as different URIs, so a mismatch breaks the OAuth flow.
Six-step MCP OAuth authorization flow
The MCP authorization flow runs in this order: (1) initial handshake returns 401 with WWW-Authenticate + resource_metadata; (2) client fetches the Protected Resource Metadata document; (3) client discovers authorization server metadata via OIDC Discovery or OAuth 2.0 Authorization Server Metadata (RFC 8414), learning issuer, authorization_endpoint, token_endpoint and registration_endpoint; (4) client registers, either pre-registered or via Dynamic Client Registration to the registration_endpoint; (5) user authorizes in a browser at /authorize and the code is exchanged for access_token/refresh_token using OAuth 2.1 authorization code with PKCE; (6) client calls the MCP endpoint with `Authorization: Bearer <token>` and the server validates it.
Protected Resource Metadata document shape (RFC 9728)
The PRM document is hosted by the MCP server at a predictable path (`/.well-known/oauth-protected-resource`) and is JSON like: {"resource": "https://your-server.com/mcp", "authorization_servers": ["https://auth.your-server.com"], "scopes_supported": ["mcp:tools", "mcp:resources"]}. It is defined by RFC 9728, section 3.2 has a fuller example.
MCP 401 challenge must include WWW-Authenticate with resource_metadata
When an unauthenticated MCP client connects to a protected HTTP MCP server, the server responds with `401 Unauthorized` and a `WWW-Authenticate` header that carries `Bearer realm="mcp"` plus a `resource_metadata` parameter pointing at the Protected Resource Metadata (PRM) document, e.g. `WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"`. Omitting the challenge means clients cannot discover how to authenticate.
Keycloak test authorization server via Docker
A local Keycloak authorization server for MCP testing can be started with: `docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak start-dev`. It listens on port 8080, its OIDC configuration is at `http://localhost:8080/realms/master/.well-known/openid-configuration`, and it supports Dynamic Client Registration by default. This configuration is for testing only, never production.
Standards MCP authorization builds on
MCP authorization is built on OAuth 2.1 (core framework), RFC 8414 (Authorization Server Metadata discovery), RFC 7591 (Dynamic Client Registration), RFC 9728 (Protected Resource Metadata), RFC 8707 (Resource Indicators), RFC 7662 (Token Introspection) and OpenID Connect Discovery.
Audience claim prevents token passthrough
Configure the authorization server to embed an audience (`aud`) claim naming the MCP server's URI in issued access tokens so the server can verify the token was meant for it and not another API; this defends against token passthrough. In production the audience must be derived from the `resource` parameter passed by the client, not hardcoded to a fixed value.
Keycloak OAuth endpoint paths for MCP servers
For a Keycloak realm base URL of `http://<host>:<port>/realms/<realm>/`, the endpoints an MCP server configures are: introspection `protocol/openid-connect/token/introspect`, authorization `protocol/openid-connect/auth`, and token `protocol/openid-connect/token`, with the realm base URL itself as the issuer.
Keycloak setup needed for MCP: scope, audience mapper, trusted hosts, confidential client
To make Keycloak work with an MCP server you must: create a client scope named `mcp:tools`, set its type to Default and enable 'Include in token scope' (required for token validation); add an Audience mapper on that scope (Mappers > Configure a new mapper > Audience) named e.g. `audience-config` with Included Custom Audience set to the MCP server URI such as `http://localhost:3000`; under Clients > Client registration > Trusted Hosts disable 'Client URIs Must Match' and add your host IP (visible in Keycloak logs as `Failed to verify remote host : 192.168.215.1`); and create a separate client with Client authentication enabled whose Client ID/Client Secret the MCP server uses for token introspection.
MCP authorization pitfalls checklist
Key mistakes to avoid: writing your own token validation instead of using vetted libraries; issuing long-lived access tokens; accepting a token without validating it; storing cached tokens without encryption or cache eviction; allowing plain HTTP outside of localhost development; using catch-all scopes instead of least-privilege per-tool scopes and verifying required scopes per route/tool; logging Authorization headers, tokens, codes or secrets; reusing the MCP server's client secret for end-user flows; omitting the WWW-Authenticate challenge on 401; leaving Dynamic Client Registration unauthenticated so anyone can register a client; accepting tokens from other realms/tenants instead of pinning one issuer; accepting generic audiences such as `api`; and returning detailed error internals to clients instead of generic messages with internally logged correlation IDs.
Token introspection verifier (RFC 7662) implementation rules
A resource-server token verifier POSTs `application/x-www-form-urlencoded` body with `token` and `client_id` (plus `client_secret` only when configured, since public clients authenticate with client_id alone) to the introspection endpoint. It must reject the token if the HTTP status is not 200, if `active` is false, if `aud` is missing, or if no audience entry matches the server's own resource URL. Only allow introspection endpoints starting with `https://`, `http://localhost` or `http://127.0.0.1`.
Never tie authorization to Mcp-Session-Id
Treat the `Mcp-Session-Id` header as untrusted input: never tie authorization decisions to it, regenerate it when authentication state changes, and validate its lifecycle server-side.
CIMD trust policies for authorization servers
Authorization servers accepting Client ID Metadata Documents can apply domain-based trust policies: allowlists of trusted domains for protected servers, accepting any HTTPS client_id for open servers, reputation checks for unknown domains, restrictions based on domain age or certificate validation, and prominently displaying the CIMD and associated client hostnames to prevent phishing. Servers retain full control over access policies.
Validate redirects and use egress proxies against SSRF
MCP clients SHOULD apply the same HTTPS and IP-range validation to redirect targets, not blindly follow redirects to internal resources, and consider disabling automatic redirect following so each hop can be validated. Server-side MCP client deployments SHOULD route OAuth discovery through an egress proxy that blocks internal destinations (for example Stripe's Smokescreen). Beware TOCTOU with DNS: an attacker domain can resolve to a safe IP at validation time and an internal IP at request time, so consider pinning DNS results between check and use.
How many scopes to include in a WWW-Authenticate challenge
Servers can choose among three approaches for the scopes listed in a step-up challenge: the minimum approach includes only the scopes required for the specific operation that triggered the error; the recommended approach includes those plus related scopes that commonly work together, reducing step-up rounds; the extended approach adds any other scopes the server anticipates the client may need soon. The choice depends on the server's assessment of user experience and authorization friction.
Least-privilege scope model for MCP servers
Implement a progressive scope model: a minimal initial scope set (e.g. mcp:tools-basic) covering only low-risk discovery/read operations, incremental elevation via targeted WWW-Authenticate scope="..." challenges when a privileged operation is first attempted, and down-scoping tolerance where the server accepts reduced-scope tokens and the authorization server MAY issue a subset of requested scopes. Servers should emit precise scope challenges rather than the full catalog and log elevation events (scope requested vs granted subset) with correlation IDs.
Localhost redirect URI impersonation with Client ID Metadata Documents
Native and local MCP clients commonly use localhost redirect URIs. A Client ID Metadata Document proves control of a domain but cannot prove which local process listens on a localhost redirect URI, so an attacker can present the legitimate client's metadata URL as its client_id, bind to any localhost port, supply that as redirect_uri, and receive the authorization code while the user sees the legitimate client's name. Countermeasures belong to authorization servers, including extra warnings for localhost-only redirect URIs and clearly displaying the redirect URI hostname.
Mix-up attacks and authorization response validation
A client that talks to many authorization servers can be tricked by a malicious authorization server into sending it a code or token issued by an honest one (a mix-up attack, RFC 9207 Section 1). Authorization Response Validation mitigates this by binding the response to the authorization server the client recorded before redirecting, so the code cannot be redeemed at an unintended token endpoint. PKCE alone does not prevent it because the client transmits code_verifier to the attacker's token endpoint, and resource indicators do not help; the mitigation depends on honest authorization servers emitting iss.
Local MCP server compromise via malicious startup commands
Local MCP servers are binaries executed on the same machine as the client, so a one-click configuration can carry a malicious startup command (e.g. a command that posts ~/.ssh/id_rsa to a remote host, or runs sudo rm -rf). Risks include arbitrary code execution with client privileges, no visibility into executed commands, command obfuscation, data exfiltration via compromised JavaScript reaching a legitimate local server, and irrecoverable data loss.
State handle hijacking and how to bind handles to users
State handle hijacking occurs when an attacker obtains or guesses a server-minted state handle and passes it as a tool argument to read or modify another user's state. MCP servers implementing authorization MUST verify all inbound requests and MUST NOT treat possession of a state handle as authentication. Servers SHOULD generate handles with a secure random number generator (never predictable or sequential IDs), expire them, and bind them server-side to the authenticated user — for example keying stored state as <user_id>:<handle> where user_id is derived from the verified token rather than supplied by the client — rejecting a handle presented by any other principal.
SSRF mitigations: HTTPS enforcement and blocked IP ranges
MCP clients SHOULD require HTTPS for all OAuth-related URLs in production, rejecting http:// except for loopback (localhost, 127.0.0.1, ::1) during development with an explicit opt-out, matching OAuth 2.1 Section 1.5. Clients SHOULD block requests to private/reserved ranges per RFC 9728 Section 7.7: IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16; loopback 127.0.0.0/8 and ::1; link-local 169.254.0.0/16 (cloud metadata); IPv6 fc00::/7 and fe80::/10. Do not hand-roll IP validation, since octal, hex, and IPv4-mapped IPv6 encoding tricks defeat custom parsers.
SSRF risk when an MCP client follows OAuth discovery URLs
MCP clients fetch URLs supplied by the server during OAuth metadata discovery: the resource_metadata URL from the WWW-Authenticate header, the authorization_servers URLs from the Protected Resource Metadata document, and the token_endpoint / authorization_endpoint and other URLs from Authorization Server Metadata. A malicious MCP server can point these at internal IPs (http://192.168.1.1/admin), cloud metadata endpoints (http://169.254.169.254/), localhost services (http://localhost:6379/), or use DNS rebinding and redirect chains, turning the client into an SSRF proxy that leaks cloud credentials.
OAuth state parameter handling in MCP proxy servers
MCP proxy servers implementing OAuth MUST generate a cryptographically secure random state value per authorization request; store it server-side (secure session store or encrypted cookie) ONLY AFTER consent is explicitly approved; set the state cookie/session immediately before redirecting to the third-party IdP; validate at the callback that the state query parameter exactly matches the stored value; reject callbacks with missing or mismatched state; and make state values single-use (deleted after validation) with a short expiry such as 10 minutes. Setting the state cookie before consent approval renders the consent screen ineffective.
Exact-match redirect_uri validation on MCP proxy servers
An MCP proxy server MUST validate that the redirect_uri in an authorization request exactly matches the registered URI using exact string matching (no pattern matching or wildcards), and MUST reject requests where the redirect_uri has changed without re-registration.
Consent cookie hardening rules for MCP servers
If cookies track consent decisions they MUST use the __Host- cookie name prefix, set Secure, HttpOnly and SameSite=Lax attributes, be cryptographically signed or backed by server-side sessions, and be bound to the specific client_id rather than merely recording that 'the user has consented'.
Requirements for an MCP server's consent page UI
The MCP-level consent page MUST: clearly identify the requesting MCP client by name; display the specific third-party API scopes being requested; show the registered redirect_uri where tokens will be sent; implement CSRF protection (state parameter or CSRF tokens); and prevent iframing via a frame-ancestors CSP directive or X-Frame-Options: DENY to block clickjacking.
Per-client consent required before forwarding to third-party authorization
MCP proxy servers MUST maintain a registry of approved client_id values per user, check that registry BEFORE initiating the third-party authorization flow, and store consent decisions securely (server-side database or server-specific cookies). The correct order is: dynamic client registration -> client opens MCP server /authorize -> MCP server checks stored consent for that client_id -> MCP-server-owned consent page -> POST /consent approve -> store decision -> only then redirect to the third-party /authorize using the static client_id.
Confused deputy attack on MCP proxy servers with static client IDs
An MCP proxy server that fronts a third-party API is vulnerable to a confused deputy attack when ALL of these hold: (1) it uses a single static OAuth client_id with the third-party authorization server, (2) it lets MCP clients dynamically register their own client_ids, (3) the third-party authorization server sets a consent cookie after first authorization, and (4) the proxy does not implement per-client consent before forwarding to the third party. The attacker dynamically registers a client with redirect_uri=attacker.com, sends the user a crafted authorization link; the third-party sees the existing consent cookie for the static client_id, skips the consent screen, and the MCP authorization code is redirected to the attacker, who exchanges it for an MCP token.
Common scope design mistakes in MCP servers
Common scope mistakes are: publishing all possible scopes in scopes_supported; using wildcard or omnibus scopes such as *, all, or full-access; bundling unrelated privileges to preempt future prompts; returning the entire scope catalog in every challenge; changing scope semantics silently without versioning; and treating claimed scopes in a token as sufficient without server-side authorization logic. Broad scopes expand the blast radius of a stolen token, make revocation disruptive, add audit noise, enable privilege chaining, and cause users to abandon consent dialogs.
MCP servers MUST NOT accept tokens not issued for them (no token passthrough)
Token passthrough is an explicitly forbidden anti-pattern: an MCP server MUST NOT accept any access token that was not explicitly issued for the MCP server itself. Servers must validate the token audience claim (see RFC 9068) and must not forward client-supplied tokens unmodified to downstream APIs. Failing this breaks rate limiting/request validation controls, destroys audit trails (downstream logs show the wrong identity), and creates confused-deputy conditions.
MCP App UI runs in a sandboxed iframe with deny-by-default CSP
The UI resource is rendered by the host in a secure iframe with a deny-by-default Content Security Policy. If the app loads separate CSS and JS assets you must explicitly configure CSP and CORS; otherwise bundle everything into a single HTML file (e.g. with `vite-plugin-singlefile`) so no external assets are fetched.
MCP Apps sandbox restrictions
MCP Apps run in a sandboxed iframe that prevents the app from accessing the parent window's DOM, reading the host's cookies or local storage, navigating the parent page, or executing scripts in the parent context. All app-host communication goes through postMessage, and the host controls which capabilities the app can access — for example restricting which tools an app may call or disabling the `sendOpenLink` capability.
Do not redirect user to MCP Authorization Server under enterprise-managed auth
When a server indicates enterprise-managed authorization is required, the client must request an ID-JAG from the enterprise IdP's authorization endpoint using the previously saved Identity Assertion (OpenID ID Token or SAML assertion) and exchange it for an access token. The client must NOT redirect the user to the MCP Authorization Server's authorization endpoint.
ID-JAG token exchange flow for enterprise MCP auth
In Enterprise-Managed Authorization the MCP client first logs the user in at the enterprise IdP (browser redirect -> IdP authorization code -> token request -> ID Token stored by client). The client then exchanges the ID Token with the IdP for an Identity Assertion JWT Authorization Grant (ID-JAG) after the IdP evaluates policy, then presents the ID-JAG to the MCP Authorization Server in a token request to obtain an MCP access token, and finally calls the MCP Resource Server API with that access token in a loop.
Enterprise-Managed Authorization extension identifier
The Enterprise-Managed Authorization extension is identified by the string `io.modelcontextprotocol/enterprise-managed-authorization`. It lets an organization control MCP server access centrally through its existing identity provider (IdP) such as Okta, Azure AD, or corporate SSO, instead of each employee authorizing each MCP server individually.