github smart-mcp-proxy/mcpproxy-go v0.68.0

5 hours ago

🔒 Colon-named tools are approved by their exact name — one-time review after upgrade

A tool's approval record, search-index entry and callability are now keyed by the exact name its server reports, colons included. Earlier releases filed a namespaced tool such as ns:erase under the text after its first colon, so it shared — and silently inherited — the approval of a sibling erase on the same server. Every dispatch path (call_tool_*, direct-name dispatch on /mcp/all, call_tool() inside code execution), preflight and describe_tool now resolve exactly the server:tool pair they dispatch.

What changes for you

  • On manual (default) and scan trust, colon-named tools on a server that already has an approved baseline become pending once, under their own names, on the first discovery after upgrade. They stay uncallable and out of retrieve_tools until you approve them: mcpproxy upstream inspect <server> to review, mcpproxy upstream approve <server> <tool>, the quarantine_security MCP tool, or the Web UI. trust_mode: auto servers and installs with quarantine_enabled: false auto-approve them; no other tool is affected.
  • Blocks carry over. A tool you had disabled under the old collapsed name stays disabled under its own name until you enable it there; the log records the carry-over at WARN with both names. A tool that was locked pending review is pending under its own name and is unlocked by approving it by that name — nothing else is needed, and nothing is deleted. A namespaced tool you had toggled in the UI keeps its old review lock (with the before/after evidence) under its own name until you approve it; if its old record approved a different definition than the server reports now, it is held as changed for review, and if it had no old record it is pending under an active gate — a toggle never approves a definition nobody reviewed.
  • Unresolved names are refused for everyone. A call to a tool that a connected server's discovered tool set does not contain is refused before any upstream call, for administrators too — while the server's discovery has not completed, retry shortly; afterwards, refresh with retrieve_tools and retry with a listed name. Quarantined, disabled and disconnected servers keep their existing answers: a call to a disconnected server still gets the not-connected / reconnect_on_use answer, and once the server reconnects and completes discovery, a name that result does not list is refused as unresolved — it is never dispatched.

Details: Security Quarantine → Namespaced tool names and Agent Tokens → Target tool tier.

Server edition: configuration keys and modes that never did anything are gone

This release removes the server-edition knobs and auth_broker modes that were accepted by the validator but had no reader in production. An old mcp_config.json still loads; what changes is how the removed keys are treated. Personal-edition users are not affected unless the file carries a server_edition or auth_broker block.

Removed keys and modes (spec 107, FR-032):

  • server_edition.max_user_servers and server_edition.workspace_idle_timeout — never enforced.
  • Per-server auth_broker.header and auth_broker.header_format — no request was ever rewritten with them.
  • auth_broker.mode: token_exchange and auth_broker.mode: entra_obo — never implemented. oauth_connect is now the only accepted mode.

What the server edition (mcpproxy-server) does with an old file

  • Loading succeeds. Each removed key is dropped with one warning naming it — server_edition.max_user_servers is no longer supported and was ignored, auth_broker.header is no longer supported and was ignored, and so on. A server whose auth_broker.mode is token_exchange or entra_obo loses its whole auth_broker block: auth_broker.mode "token_exchange" was never implemented; the auth_broker block for server "<name>" was ignored. The next write-back of the file omits the dropped keys.
  • Writing them is refused. PATCH /api/v1/config and /api/v1/config/apply reject a document that carries any removed key or mode with the same message, so a script that still sends max_user_servers now gets a validation error instead of a silent accept.
  • Remove the keys from your file, and change any token_exchange/entra_obo server to oauth_connect if you want its connect flow to keep working — otherwise its auth_broker block is dropped on the next load.

What the personal edition (mcpproxy) does with the same file

  • Nothing. The server_edition block and every server's auth_broker block now pass through the personal binary as opaque JSON — every key and value preserved, removed keys included, no warning and no validation. Earlier releases wrote both blocks back as {}, so an API-key bootstrap or a PATCH /api/v1/config from the personal binary could erase a team's SSO or broker configuration; that is fixed here (FR-040).

store_idp_tokens is now a no-op

server_edition.store_idp_tokens no longer stores anything. The identity-provider access and refresh tokens it used to persist at login existed only to feed the never-implemented token_exchange/entra_obo modes, which left a long-lived IdP refresh token at rest with nothing reading it. The writer, the reader and the offline-access scope and authorization parameters that asked the IdP for a refresh token (offline_access, access_type=offline) are removed (FR-033), so a fresh login no longer requests a refresh token from the IdP.

  • The key is still accepted so an old file loads. "store_idp_tokens": true logs one warning at boot — server_edition.store_idp_tokens is deprecated and no longer stores IdP tokens; remove it — and does nothing else.
  • Remove it from your configuration. Nothing in this release reads the IdP tokens an earlier release stored, and the first start of mcpproxy-server with server_edition.enabled: true after upgrading deletes them from config.db (the rows are removed by key before anything else in the server-edition setup runs, so this happens whether MCPPROXY_CRED_KEY is still set, unset or even invalid; the log line purged legacy IdP subject-token rows reports the count). Credentials connected through the oauth_connect flow are not touched.
  • The former IdP Token Storage page is now a tombstone.

Auth broker: a stored credential is stored, not injected

The oauth_connect connect flow, its REST routes, the mcpproxy credential commands, the encrypted credential store and MCPPROXY_CRED_KEY / credential_encryption_key all stay. What changes is the promise attached to them: a credential a user connects through the broker is kept for a future broker and is not injected into upstream tool calls. It never was — the injection, resolution and per-user connection-keying code paths that the documentation described had no production caller and are deleted in this release (FR-031, FR-034).

  • mcpproxy credential list and mcpproxy credential status now open with the line Stored credentials are kept for a future broker and are NOT injected into upstream calls in this release.
  • The auth broker and credential commands pages are rewritten accordingly; the "Credential resolution", "Header injection" and "Per-(user, server) connection keying" sections are gone.
  • Upstream calls keep using whatever the server's own configuration provides (static headers, the server's own OAuth). If you deployed the broker expecting per-user credentials on upstream calls, that expectation was never met, and this release says so rather than fixing it.
  • Historical credential_broker activity rows remain readable and labelled.

Agent tokens: a per-user quota inside the deployment cap

The server edition now enforces a 25-token quota per signed-in user on top of the existing 100-record deployment cap (#1177). Revoked tokens keep their slot until they are permanently deleted.

  • Server edition: a user at 25 tokens gets a 409 Conflict from POST /user/tokens that names their quota — permanently deleting one of their unused tokens frees a slot — so one user can no longer take the whole pool. The 100-record deployment cap remains and every stored token still counts toward it, so a deployment whose stored records add up to 100 refuses the next token for everyone until an administrator frees records; a caller who is still under their own quota then gets the 409 that says the limit is shared and points at an administrator (the quota is checked first, so a user already at 25 sees their own-quota message instead). A user who already holds more than 25 tokens keeps them; they cannot create another until they are back under the quota.
  • Personal edition: every token is ownerless, so the quota does not apply and the 100-token limit is unchanged.
  • No configuration change is needed. Details: agent tokens.

Server edition: /mcp always requires a credential

When server_edition.enabled is true, /mcp now behaves as if require_mcp_auth were true whatever the file says (spec 107, FR-029). Before this release a server-edition deployment with require_mcp_auth off (the default) handed every unauthenticated /mcp caller an anonymous administrator context.

  • No credential → 401. A session cookie or a user JWT on /mcp401 (they were never valid there; they are no longer silently promoted). Agent tokens (mcp_agt_…), the global API key and the Unix socket work exactly as before.
  • An explicit "require_mcp_auth": false is not a validation error, so no deployment fails to boot on upgrade. It logs one notice — require_mcp_auth: false is overridden to true because server_edition.enabled is true — and mcpproxy doctor reports the same line. Remove the key, or set it to true, to silence both.
  • Personal edition: unchanged; require_mcp_auth keeps its configured value.
  • If an AI client reached /mcp on a server-edition deployment without any credential, it now needs an agent token: each user mints one from the Web UI or POST /api/v1/user/tokens and sends it as Authorization: Bearer mcp_agt_….

trusted_proxies now gates every forwarded header — behind an ingress, set it or public_url

X-Forwarded-For, X-Real-IP, X-Forwarded-Proto and X-Forwarded-Host used to be believed from any peer: a direct client could choose its own session IP, force the OAuth callback to https, or move it to another host. They are now honoured only when the request's RemoteAddr is inside the new top-level trusted_proxies list (CIDRs or addresses; env MCPPROXY_TRUSTED_PROXIES, comma-separated; hot-reloadable; both editions), taking the right-most hop that is not itself a trusted proxy as the client IP. The default is empty — trust nobody (FR-027).

What changes behind a reverse proxy or ingress if you do nothing

  • The OAuth redirect_uri sent to your IdP is built from the listener's own scheme and Host — typically http://… — instead of the ingress's X-Forwarded-Proto: https. Your IdP's exact-match registration then refuses the callback and every SSO login fails.
  • The session's recorded IP, the audit client.ip, the connect-flow base URL and the swagger server URL all show the ingress's address, not the user's.

Fix (either one)

  • Set server_edition.public_url to the origin users reach (https://mcp.example.com; env MCPPROXY_PUBLIC_URL). It becomes the sole source of the callback URL (<public_url>/api/v1/auth/callback), the connect-flow base URL and the cookie Secure decision; Host and X-Forwarded-* are ignored for those (FR-025). When it is unset and the listener is not loopback — the published image listens on 0.0.0.0:8080 — boot logs a warning and mcpproxy doctor reports it; it is not a validation error.
  • Or list your ingress in trusted_proxies (["10.0.0.0/8"], ["fd00::/8"], a single address). Do both if you also want the real client IP in sessions and audit lines.
  • An invalid entry (trusted_proxies[0] "…" is not a valid CIDR or IP address) is a validation error at load and on PATCH /api/v1/config. No forwarded header ever feeds the local/remote or administrator classification. Details: reverse proxy, config file, environment variables.

Session cookieserver_edition.session_cookie_secure is new: auto (default) sets Secure when the effective scheme is https (public_url, in-process TLS, or X-Forwarded-Proto: https from a trusted proxy), true forces it, false disables it. Earlier releases never set Secure. Validation refuses false together with an https:// public_url or in-process TLS; an explicit false elsewhere is honoured with one boot warning and a mcpproxy doctor finding. HttpOnly and SameSite=Lax are unchanged (FR-026).

Post-login redirectredirect_uri on GET /api/v1/auth/login is accepted only as a same-origin path (a single leading /, no scheme, host, //, /\, backslash or control character); anything else lands on /ui/ and the login's auth_event line carries redirect_rejected. The Web UI is unaffected (FR-028).

Server edition: generic oidc identity provider

server_edition.oauth.provider accepts oidc next to google, github and microsoft, so Okta, Entra ID, Keycloak, Authentik, Auth0 and any other OpenID Connect provider work without provider-specific code (FR-020). The three existing providers behave exactly as before.

  • Configuration: issuer_url (required; https, or http only for a loopback host and allow_insecure_issuer: true), scopes (default ["openid","profile","email"]; openid is added if missing), groups_claim (default "groups"), email_verified_policy (default refuse_false), display_name (login-button label; falls back to the provider name). client_id/client_secret stay ${env:}-referenced; there is no environment variable for nested keys. authorization_endpoint, token_endpoint, jwks_uri and userinfo_endpoint come from <issuer_url>/.well-known/openid-configuration, fetched lazily on the first login and cached, so boot and readiness never wait on the IdP.
  • Every ID token is verified before any claim is read: signature against the issuer's JWKS (RS/PS/ES families only — never none, never HS*), exact iss, aud/azp, exp/nbf/iat with 60 s skew, and a per-login nonce. Discovered endpoints must be absolute https (same loopback exception), and the back-channel client never follows a redirect: the client secret and code are still sent to your configured token endpoint (and a bearer token to your configured userinfo endpoint) as normal, but a 3xx answer from any of the token, JWKS or userinfo endpoints refuses the login instead of being followed — so a compromised or misconfigured endpoint cannot redirect that credential to another host (FR-021).
  • email_verified_policyrefuse_false (default) refuses a login whose ID token says email_verified: false and admits one where the claim is absent; require_true also refuses an absent claim; ignore admits both. Pick require_true when your IdP always sets the claim; refuse_false exists so providers that omit it still work out of the box. email itself is required (email_missing otherwise).
  • Groups are captured: on every successful oidc login the user record stores the groups claim from the verified ID token (or from userinfo when the token lacks it — accepted only when the userinfo sub equals the token's sub), replacing the previous list wholesale with a groups_updated_at timestamp; a missing or malformed claim stores [] and logs groups_claim_missing. google/github/microsoft logins store []. GET /api/v1/auth/me returns your groups; GET /api/v1/admin/users shows every user's groups and groups_updated_at. Storing groups has no authorisation effect on its own; they are the input to the server-edition access grant (FR-008).
  • Subject binding: a user record now remembers (provider, provider_subject_id) and refreshes both on every login. Same provider, same email, different subject is refused (subject_mismatch), so an IdP email collision cannot take over an existing account; an administrator re-arms the binding for a genuinely re-created IdP account by disabling and re-enabling the user — the next successful login rebinds (FR-023).
  • Refusals are uniform: every denied login renders one generic 403 page ("Sign-in was not permitted") with a reference id; the reason (email_unverified, subject_mismatch, state_invalid, …) reaches only the server log and the auth_event line under that id. IdP-side failures (discovery_failed, provider_error) and proxy-side failures after verification (internal_error) render a 503 "Sign-in is temporarily unavailable" instead, so an outage is never shown as "not permitted" (FR-024).
  • Login page label and edition probe: public GET /api/v1/auth/provider returns only {"display_name": "…"} — never the issuer, client id, tenant, scopes or domains — so the Web UI labels the sign-in button and detects the edition before login; the personal build answers 404 (FR-030).
  • Guide: multi-user authentication.

Server edition: group-based server access, and credential minting is now session-cookie-only

A tenant — anyone who signs in through the team's IdP rather than through the API key or the local socket — now sees, uses, mints tokens for, connects to and diagnoses exactly the servers their IdP group grants (spec 107, FR-004/FR-009), on the REST API, the Web UI and every agent token they own. Administrators (API key, socket, and any admin_email user) are unaffected.

  • New config: server_edition.access.group_servers maps an IdP group name to a list of shared server names (or ["*"] for every shared server); access.default_servers covers a user whose groups match no key. A user's grant is the union of their groups' entries; a server must be both shared and granted to appear for them — sharing alone is not enough once the map is active. The access block itself must be present to change anything: an absent block keeps today's Shared-only behaviour (every shared server visible to every tenant, unchanged); a present block — even {}, with no group_servers/default_servers entries — is deny-all for every tenant that matches none of it. Add the block to narrow access; there is no config that widens it beyond Shared.
  • Non-disclosing: a server outside a tenant's grant does not exist for them on any surface — not its name, tools, prompts, counts, status, logs or activity. This applies to core REST (/api/v1/*, /events), the Web UI and agent tokens a tenant owns.
  • Agent tokens narrow on every use, not just at mint: an owned token's effective scope is recomputed from the user's current groups and the current access map on every authentication (one GetUser per call) — so enabling, editing or removing an access entry takes effect on the token's very next call, without rotating or re-minting it. The same is true for a tenant's session on core REST and its SSE stream (the per-frame refresher re-resolves the principal, so un-sharing narrows the next frame). Widening a user's access (the IdP adds them to a group, or the map is edited to include them) only takes effect once they sign in again — groups themselves refresh only at login.
  • Credential-minting doors are session-cookie-only: POST /api/v1/auth/token (JWT renewal), POST /api/v1/user/tokens and POST /api/v1/user/tokens/{name}/regenerate now accept only the browser session cookie — a bearer JWT or an agent token gets 401. A derived credential can no longer mint another credential, closing the chain where a JWT could renew itself indefinitely and then mint a 30-day agent token in its last second. POST /api/v1/user/tokens also now caps expires_in at 365 days (previously unbounded), matching core /api/v1/tokens. The practical freshness bound is now session TTL + the longer of JWT TTL and the longest-lived owned agent token (≤ 365 days); an administrator disable still takes effect immediately.
  • Subject-rebind procedure for a re-created IdP account: if your IdP re-creates a user's account (new subject, same email), the login is refused (subject_mismatch) rather than silently taking over the existing record. An administrator re-arms the binding by disabling the user and then re-enabling them — this arms a single-use, persisted rebind window — and the user's next successful login accepts the new subject and rebinds automatically. No other action is needed and no record is deleted.
  • Tenant Web UI: a signed-in tenant now gets a working dashboard, server list and activity view built entirely from the session cookie and the tenant-allowed routes — no ?apikey=, no calls to administrator-only or global-state endpoints (/info, /routing, /docker/status, /connect, /stats/tokens, /security/overview, /onboarding/state, core /activity*, core /config); those cards, chips and pages are hidden rather than issued-and-403'd — Settings stays an administrator-only page (its own personal-server and token management live under /my/servers, /my/tokens). Diagnostics and history use the tenant-scoped /user/diagnostics and /user/activity endpoints. The access map is edited by an administrator through Settings' Raw JSON tab and shown read-only as group chips on the admin server page and on AdminUsers.
  • No action needed if you do not set server_edition.access at all: every tenant keeps seeing every Shared server exactly as before this release, on group grants alone. To start restricting tenants by IdP group, add the access block — from that point on, only a matching group entry (or default_servers) grants a shared server; a present-but-empty block denies every tenant until you populate it. Administrators are unaffected either way.

Server edition: every authorization decision and tool call now writes an audit line

A new audit_log writes one JSON line per pre-dispatch authorization decision and one per completed tool call (authz/tool_call events, spec 107 FR-012..FR-019), plus one per login/logout attempt (auth_event, already covered above). Arguments are never logged in the clear: each line carries args_sha256, a SHA-256 over the RFC 8785 (JCS) canonical form of the call's arguments, and args_bytes, never the arguments themselves. A quarantined or otherwise hidden server name is written for the operator's own record but is never echoed back to the caller — the audit line and the caller-facing refusal stay separately governed. Nested code_execution sub-calls get their own authz/tool_call pair carrying parent_id, so a script that fans out into several upstream tools is fully attributable, not collapsed into one line.

  • Personal edition default: off (audit_log.enabled: false); nothing changes unless you turn it on. Server edition default: on, writing to stdout, with one line logged at startup announcing the sink. Set audit_log.path to a file instead (rotated: max_size_mb/max_backups/max_age_days/compress, defaults 50 MB / 10 / 90 days / compressed) if you want the audit stream off your process's own stdout.
  • Under the native stdio transport, stdout is JSON-RPC and can never double as the audit sink. With audit_log absent, the server edition silently falls back to {enabled:false} and logs one WARN (audit_log.stdout is ignored under the stdio transport; set audit_log.path) instead of writing audit JSON into the protocol stream. If you explicitly set audit_log.enabled: true, stdout: true with no path under stdio, that is refused, not silently downgraded: startup fails with exit code 4, audit_log.stdout cannot be used under the stdio transport (stdout carries JSON-RPC); set audit_log.path.
  • An unwritable audit path is a boot failure, not a warning. If audit_log.path cannot be opened for append (missing parent directory, permissions), mcpproxy-server exits with code 4 and logs audit_log.path %q cannot be opened for append: %v. Point the path at a writable location before starting, or use the stdout sink where the transport allows it.
  • The sink is a single mutex-guarded synchronous writer (plan.md Complexity Tracking) — a write failure after startup increments an always-on counter (visible in mcpproxy doctor) rather than blocking or dropping the request; audit lines are best-effort after boot, guaranteed-writable at boot.
  • audit_log is bound at sink construction, so every key under it (enabled, path, stdout, max_size_mb, max_backups, max_age_days, compress) requires a restart to take effect; a hot PATCH/apply is accepted but only applies on the next start.
  • Details: audit log.

This release focuses on enterprise security and multi-tenancy capabilities for Teams edition, with major improvements to scope-based authorization and audit logging.

New Features

Teams Edition:

  • Generic OIDC Authentication: Added support for any OpenID Connect identity provider, enabling flexible SSO integration
  • Identity Provider Group Grants: Server access can now be granted based on IdP group membership with tenant session support across REST API and Web UI
  • Comprehensive Audit Logging: Every authorization decision and tool call now generates an attributable JSONL audit line for compliance tracking

Bug Fixes

Teams Edition:

  • Scope Authorization Overhaul: Complete rework of permission system ensuring OAuth users are scope-restricted like agent tokens, with proper filtering before limits and scope-first refusal precedence
  • Direct Surface Authorization: Fixed authorization checks for direct-surface definitions at every system boundary
  • Profile Selection: Enforced scope-based restrictions on profile selection endpoints with non-disclosing error messages
  • Stored Script Security: Script enumeration endpoints now administrator-only
  • Log Attribution: Fixed per-record logging with proper subject-bound OAuth callback tracking

Both Editions:

  • Configuration Persistence: Fixed bug where CLI flags and environment variables were incorrectly saved to config file
  • Docker Security: Added container ownership verification before scanning
  • Process Management: Prevented stdio process termination on ambiguous health checks and fixed dropped call_tool_write arguments
  • Docker Instance ID: Changed scope from host-wide to data directory for better isolation

Improvements

Teams Edition:

  • OAuth callback path now configurable via redirect_uri
  • Scanner isolation mode separated from process isolation controls
  • Web UI shares single in-flight authentication probe for better performance

Download Installers

Platform Download Notes
macOS (Apple Silicon) Download DMG Signed & Notarized - Recommended for M1/M2/M3/M4
macOS (Intel) Download DMG Signed & Notarized
Windows (64-bit) Download Setup Setup wizard
Windows (ARM64) Download Setup For ARM Windows devices
Linux Debian/Ubuntu (AMD64) Download .deb sudo apt install ./mcpproxy_*.deb
Linux Debian/Ubuntu (ARM64) Download .deb For ARM64 (Raspberry Pi etc.)
Linux Fedora/RHEL (AMD64) Download .rpm sudo dnf install ./mcpproxy-*.rpm
Linux Fedora/RHEL (ARM64) Download .rpm For ARM64
Linux (AMD64) — tarball Download tar.gz Binary only
Linux (ARM64) — tarball Download tar.gz Binary only

Homebrew (macOS/Linux):

brew install smart-mcp-proxy/mcpproxy/mcpproxy
Other download options (auto-update URLs, archives)

Auto-update URLs (always points to latest):

Binary archives (this version):


Installation Instructions

Windows

  1. Download the installer for your architecture
  2. Run mcpproxy-setup-*.exe
  3. Follow the installation wizard (requires Administrator privileges)
  4. Launch "MCPProxy" from Start Menu

macOS

  1. Download the signed DMG for your Mac
  2. Double-click the DMG to mount it
  3. Double-click the PKG installer inside
  4. Follow the installation wizard
  5. Launch mcpproxy.app from Applications folder

Linux / Manual Installation

  1. Download the appropriate archive
  2. Extract: tar -xzf mcpproxy-*.tar.gz
  3. Make executable: chmod +x mcpproxy
  4. Run: ./mcpproxy serve
Platform Support & Usage

Platform Support

  • macOS: Full system tray support with menu and icons
  • Windows: Full system tray support with menu and icons
  • Linux: Headless mode only (CLI)

Usage

GUI (Recommended):

  • Launch mcpproxy.app from Applications (auto-starts core server)
  • Manages server via system tray menu

CLI:

mcpproxy serve                           # Start server
mcpproxy serve --listen 127.0.0.1:8081  # Custom port
export MCPPROXY_API_KEY=your-secret-key  # Set API key

Don't miss a new mcpproxy-go release

NewReleases is sending notifications on new releases.