π 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) andscantrust, 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 ofretrieve_toolsuntil you approve them:mcpproxy upstream inspect <server>to review,mcpproxy upstream approve <server> <tool>, thequarantine_securityMCP tool, or the Web UI.trust_mode: autoservers and installs withquarantine_enabled: falseauto-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
WARNwith 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_toolsand 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_useanswer, 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_serversandserver_edition.workspace_idle_timeoutβ never enforced.- Per-server
auth_broker.headerandauth_broker.header_formatβ no request was ever rewritten with them. auth_broker.mode: token_exchangeandauth_broker.mode: entra_oboβ never implemented.oauth_connectis 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 whoseauth_broker.modeistoken_exchangeorentra_oboloses its wholeauth_brokerblock: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/configand/api/v1/config/applyreject a document that carries any removed key or mode with the same message, so a script that still sendsmax_user_serversnow gets a validation error instead of a silent accept. - Remove the keys from your file, and change any
token_exchange/entra_oboserver tooauth_connectif you want its connect flow to keep working β otherwise itsauth_brokerblock is dropped on the next load.
What the personal edition (mcpproxy) does with the same file
- Nothing. The
server_editionblock and every server'sauth_brokerblock 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 aPATCH /api/v1/configfrom 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": truelogs 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-serverwithserver_edition.enabled: trueafter upgrading deletes them fromconfig.db(the rows are removed by key before anything else in the server-edition setup runs, so this happens whetherMCPPROXY_CRED_KEYis still set, unset or even invalid; the log linepurged legacy IdP subject-token rowsreports the count). Credentials connected through theoauth_connectflow 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 listandmcpproxy credential statusnow open with the lineStored 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_brokeractivity 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 ConflictfromPOST /user/tokensthat 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 the409that 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/mcpβ401(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": falseis 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β andmcpproxy doctorreports the same line. Remove the key, or set it totrue, to silence both. - Personal edition: unchanged;
require_mcp_authkeeps its configured value. - If an AI client reached
/mcpon a server-edition deployment without any credential, it now needs an agent token: each user mints one from the Web UI orPOST /api/v1/user/tokensand sends it asAuthorization: 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_urisent to your IdP is built from the listener's own scheme andHostβ typicallyhttp://β¦β instead of the ingress'sX-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_urlto the origin users reach (https://mcp.example.com; envMCPPROXY_PUBLIC_URL). It becomes the sole source of the callback URL (<public_url>/api/v1/auth/callback), the connect-flow base URL and the cookieSecuredecision;HostandX-Forwarded-*are ignored for those (FR-025). When it is unset and the listener is not loopback β the published image listens on0.0.0.0:8080β boot logs a warning andmcpproxy doctorreports 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 onPATCH /api/v1/config. No forwarded header ever feeds the local/remote or administrator classification. Details: reverse proxy, config file, environment variables.
Session cookie β server_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 redirect β redirect_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, orhttponly for a loopback host andallow_insecure_issuer: true),scopes(default["openid","profile","email"];openidis added if missing),groups_claim(default"groups"),email_verified_policy(defaultrefuse_false),display_name(login-button label; falls back to the provider name).client_id/client_secretstay${env:}-referenced; there is no environment variable for nested keys.authorization_endpoint,token_endpoint,jwks_urianduserinfo_endpointcome 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*), exactiss,aud/azp,exp/nbf/iatwith 60 s skew, and a per-loginnonce. Discovered endpoints must be absolutehttps(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_policyβrefuse_false(default) refuses a login whose ID token saysemail_verified: falseand admits one where the claim is absent;require_truealso refuses an absent claim;ignoreadmits both. Pickrequire_truewhen your IdP always sets the claim;refuse_falseexists so providers that omit it still work out of the box.emailitself is required (email_missingotherwise).- Groups are captured: on every successful
oidclogin the user record stores the groups claim from the verified ID token (or fromuserinfowhen the token lacks it β accepted only when the userinfosubequals the token'ssub), replacing the previous list wholesale with agroups_updated_attimestamp; a missing or malformed claim stores[]and logsgroups_claim_missing.google/github/microsoftlogins store[].GET /api/v1/auth/mereturns your groups;GET /api/v1/admin/usersshows every user's groups andgroups_updated_at. Storing groups has no authorisation effect on its own; they are the input to the server-editionaccessgrant (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
403page ("Sign-in was not permitted") with a reference id; the reason (email_unverified,subject_mismatch,state_invalid, β¦) reaches only the server log and theauth_eventline under that id. IdP-side failures (discovery_failed,provider_error) and proxy-side failures after verification (internal_error) render a503"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/providerreturns 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 answers404(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_serversmaps an IdP group name to a list of shared server names (or["*"]for every shared server);access.default_serverscovers 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. Theaccessblock itself must be present to change anything: an absent block keeps today'sShared-only behaviour (every shared server visible to every tenant, unchanged); a present block β even{}, with nogroup_servers/default_serversentries β is deny-all for every tenant that matches none of it. Add the block to narrow access; there is no config that widens it beyondShared. - 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
accessmap on every authentication (oneGetUserper call) β so enabling, editing or removing anaccessentry 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/tokensandPOST /api/v1/user/tokens/{name}/regeneratenow accept only the browser session cookie β a bearer JWT or an agent token gets401. 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/tokensalso now capsexpires_inat 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 administratordisablestill 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/diagnosticsand/user/activityendpoints. Theaccessmap is edited by an administrator through Settings' Raw JSON tab and shown read-only as group chips on the admin server page and onAdminUsers. - No action needed if you do not set
server_edition.accessat all: every tenant keeps seeing everySharedserver exactly as before this release, on group grants alone. To start restricting tenants by IdP group, add theaccessblock β from that point on, only a matching group entry (ordefault_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. Setaudit_log.pathto 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_logabsent, the server edition silently falls back to{enabled:false}and logs oneWARN(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 setaudit_log.enabled: true, stdout: truewith nopathunder 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.pathcannot be opened for append (missing parent directory, permissions),mcpproxy-serverexits with code 4 and logsaudit_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.mdComplexity Tracking) β a write failure after startup increments an always-on counter (visible inmcpproxy doctor) rather than blocking or dropping the request; audit lines are best-effort after boot, guaranteed-writable at boot. audit_logis 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 hotPATCH/applyis 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_writearguments - 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/mcpproxyOther download options (auto-update URLs, archives)
Auto-update URLs (always points to latest):
Binary archives (this version):
Installation Instructions
Windows
- Download the installer for your architecture
- Run
mcpproxy-setup-*.exe - Follow the installation wizard (requires Administrator privileges)
- Launch "MCPProxy" from Start Menu
macOS
- Download the signed DMG for your Mac
- Double-click the DMG to mount it
- Double-click the PKG installer inside
- Follow the installation wizard
- Launch mcpproxy.app from Applications folder
Linux / Manual Installation
- Download the appropriate archive
- Extract:
tar -xzf mcpproxy-*.tar.gz - Make executable:
chmod +x mcpproxy - 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