MCPProxy v0.70.0: Profiles, Per-Client Credentials, and Agents That Find Their Tools
TL;DR
v0.70.0 is out. It contains 89 commits since v0.69.0, the biggest release in a while. Four things worth your attention:
- Profiles now enforce policy. A profile is no longer just a list of servers. It can cap the tool tier (read, write, destructive) and allow or deny individual tools, and it decides what an agent can discover and what it can call.
- Every client gets its own credential. Connect no longer copies the admin API key into Claude Desktop, Cursor or Codex configs. Each client gets a scoped
mcp_cli_credential bound to a profile. - Agents are told where their tools are. Claude Code and Codex used to conclude that GitHub “isn’t available” and fall back to the
ghCLI. The proxy now tells each agent which servers it can reach and what it may do there. - Upgrades keep quarantined servers quarantined. We found and fixed a path where a server that v0.69 held for review came out the other side of the upgrade fully approved.
Upgrade notes are at the end. Two of them matter if you script against the CLI or plan to downgrade.
Profiles With Teeth
Profiles existed before, as a way to scope tool discovery to a subset of servers. In v0.70.0 they became policy (Spec 108):
max_tiercaps what a profile can call:read,writeordestructive. Tiers come from tool annotations.tools.allow/tools.denyrules matchserver:toolglobs. Deny wins on overlap. An explicit allow can let a single tool past the tier cap.unannotateddecides what happens to tools that declare no hints: deny them, or treat them as read or as write.code_executionandmanagement_toolscan be switched off per profile.
The policy is enforced on every execution surface: call_tool_*, the direct /mcp/all surface, nested calls from code_execution, and REST. Discovery follows the same rules (#1390, #1411). retrieve_tools filters by policy before it cuts results to the limit, so a hidden tool can’t push an allowed one out of the window, and it reports how many matches the profile hid as hidden_by_profile.
When a call is refused, the error says why: github:create_issue is a write tool; profile "Work Read-only" allows read tools only. An unauthenticated caller confined by anonymous_profile still gets the old non-disclosing wording, so the operator’s profile name is never handed out.
Every surface can now answer “why can this agent see or not see that tool?”. The Web UI, the macOS app, mcpproxy access explain, and a new profiles admin MCP tool all show the same access explainer.
A Credential Per Client
Until now, connecting a client meant writing the instance admin API key into its config file. Any client, and anything that could read that file, then had the keys to the whole proxy, REST API included.
v0.70.0 changes that (#1389, #1430). Connect writes a per-client credential (mcp_cli_…) that:
- is bound to a profile, either locked or switchable within an allowlist;
- works on MCP endpoints only (REST answers it with
403); - can be rotated or forgotten from the new Clients hub without touching other clients.
Existing configs that still hold the admin key are not rewritten behind your back. The Clients page and mcpproxy doctor list them, and Upgrade clients holding the admin key converts them in one step. After that, rotate the admin key.
Docs: Connect Clients, Profiles.
Agents That Find Their Tools
People kept reporting the same thing. They connected Claude Code or Codex to MCPProxy, asked it to open a GitHub issue, and watched it run gh issue create instead of using the GitHub server sitting right behind the proxy.
That’s partly by design. In the default retrieve_tools mode, an agent sees a handful of built-in tools, not hundreds of upstream ones. That’s where the token savings come from. But it means the agent has to know that GitHub is behind retrieve_tools, and two things were stopping it.
Instructions were not being sent. MCP lets a server send instructions on initialize, and MCPProxy had good text for it. But the default /mcp endpoint is served by a different internal server object than the one the instructions were attached to, so a client on /mcp, /mcp/call or /mcp/code received nothing. That has been the case for a while. It’s fixed.
Nothing named the servers. Claude Code defers MCP tools behind its own tool search, so the model sees only mcp__mcpproxy__retrieve_tools until it searches. When it searches for “github”, nothing matched.
v0.70.0 builds both texts per caller (#1486):
YOUR ACCESS (this connection): Connected upstream servers you can use: github.
Find their tools with 'retrieve_tools' (query: the server name plus the task).
Allowed operations: read, write — destructive tool calls will be refused.
The retrieve_tools description ends with CONNECTED SERVERS searchable here: github, linear, …, so a tool search for “github” now lands on it. When the set of connected servers changes, clients get tools/list_changed.
Everything in that block comes from the same checks the proxy applies at call time: the caller’s profile, its agent token’s allowed_servers and permission set, and the profile’s max_tier. A scoped token never sees the name of a server outside its scope. Names that could carry prompt text, such as anything with spaces or newlines, are counted but never spelled out. If you’d rather keep server names out of your LLM provider’s context entirely, set advertise_upstream_servers: false.
For the clients that don’t weigh server instructions much, there’s a one-liner for their memory file:
mcpproxy agent-instructions >> AGENTS.md
It follows your routing_mode and can include the currently connected servers with --with-servers. Docs: Agent Instructions.
The Upgrade Bug We Almost Shipped
This one came out of release-candidate testing, and it’s worth telling straight.
v0.69 has an admission gate: a server added by hand-editing mcp_config.json is quarantined until you review it. v0.69 also re-reads the config file when it restarts a server, and that path wrote the file’s entry to the database without the gate. A server with no quarantined key reads as false. Restarts are not rare: the baseline security scan triggers one shortly after startup. So the database ended up saying “not quarantined” for a server that v0.69 was still holding in memory.
On upgrade, the new version trusted the database, treated the server as already vetted, and auto-approved all of its tools.
Upgrading to v0.70.0 closes it (#1485, #1488):
- The upgrade check. Being known to the database is no longer enough. A server must also have an approval baseline, meaning at least one tool that was ever approved. A server that actually ran always has one, because its tools are approved automatically when it first connects. A server that never got past quarantine has none, and it stays held.
- Restarts. A server’s recorded quarantine now survives restarts and config writes (#1463).
- Explicit settings. A
quarantinedvalue you state when adding or importing a server is written to the config file as your decision.
The cost is that a few vetted servers with no tool records are held once on upgrade: servers that expose only prompts or resources, and servers that never connected since v0.21. Approve them, or set "quarantined": false before upgrading.
Navigation, Rebuilt Around One List
The second large piece of work is Spec 109, a pass over every surface so the Web UI, the macOS app and the CLI tell the same story:
- One needs-attention list. Home, the tray,
mcpproxy statusandmcpproxy doctorall lead with the same items: sign-in required, needs review, failed to start, and the rest. - A real Review queue. It shows what changed and what the scan found, starts fail-closed, and its default approval admits read tools and holds write tools until you say otherwise.
- Catalog-first Add Server, with a real popularity signal (Spec 110) so the live registry search ranks the server you meant first.
- Clients hub, grouped sidebar, command palette (Cmd/Ctrl+K), and Activity filters you can deep-link.
- One status vocabulary everywhere:
Online,Sign-in required,Needs review, and so on.
Smaller Fixes
- Dead stdio servers come back. If a stdio upstream’s process died, every call failed with “transport closed” while the proxy kept reporting the server healthy, and it never respawned. Now it’s marked unhealthy right away and respawned through the normal backoff (#1489). In our QA run, it was unhealthy within 0.1 s of the kill and serving calls again after about 22 s.
- A bad import no longer bricks startup. An import entry that the next startup would refuse, such as a stdio server with no command, is now rejected at import time instead of being saved (#1490).
- Open-object schemas survive.
/mcp/alland thecall_tool_*arguments keepadditionalProperties. Grammar-constrained local models (llama.cpp) were reading{"properties":{}}as “no arguments allowed” (#1434, #1438). - Client header forwarding. An allowlist of headers from the MCP client can be passed through to upstreams (Spec 112).
- The search index migrates itself. It’s versioned now, and a mapping change triggers an automatic rebuild (#1386).
How It Was Tested
v0.70.0 went through five release candidates, and the misses are part of the story:
- rc.2: an independent RC test found the upgrade bug above.
- rc.3: our own release gate refused to build it. The first version of the upgrade fix also quarantined servers added while the proxy was running, which broke the OAuth matrix cell.
- rc.4: a deeper test pass found the dead-stdio and bad-import bugs. Both turned out to exist in earlier releases too.
- rc.5: 42 checks passed, 0 failed, each fix shown against the previous RC as a control. Code signing and notarization were verified on the shipped artifacts.
Upgrade Notes
- Downgrading. Before you go back to v0.69 or earlier, set
require_mcp_auth: true. Older versions don’t knowmcp_cli_credentials, and with MCP auth off they treat them as no credential at all, which means unconfined access. - Clients holding the admin key. They keep working. Convert them from the Clients page, then rotate the admin key.
- CLI table output changed.
upstream list,statusanddoctorchanged their human-readable tables. If you parse them, switch to-o json, which is unchanged. describe_tool. It no longer returnsinvisible. A tool outside your scope reportsnot_found.- Servers with no approved tools. As described above, they’re held for review once after upgrading.
Get it from the release page, with brew upgrade mcpproxy, or let the macOS app update itself.