MCP servers
.mcp.json at the repo root declares the MCP servers Claude Code connects at session start. It is committed, so every developer gets the same set; each still approves a server on first encounter. There is no install step — uvx and npx fetch what they need on first use — but that first use has a cost worth knowing about before it surprises you (see First use on a machine).
The one server not declared there is aws-mcp, which lives at user scope instead. Why AWS is user-scoped covers the reasoning; the short version is that a project-scoped server follows you into a cloud session, and this one cannot work there.
.mcp.json changes take effect only on a session restart. Pulling or merging the change does not fix a session that is already running.
aws-mcp is remote, unlike the rest
Every other server here runs locally. aws-mcp is AWS's managed remote server; what runs locally is only mcp-proxy-for-aws, a thin client that signs requests with SigV4 from your ambient AWS credentials and forwards them to https://aws-mcp.us-east-1.api.aws/mcp.
Two consequences. Requests leave the machine, so what you ask it is visible to AWS in the same way any AWS API call is — and every call lands in CloudTrail, which is an audit gain rather than a loss. And the endpoint is us-east-1 regardless of where we operate: the region we care about travels as --metadata AWS_REGION=us-west-2, not in the URL.
It replaced aws-api-mcp-server, which AWS has put into end-of-development, and it absorbed aws-knowledge-mcp-server — the documentation tools are the same aws___search_documentation / aws___read_documentation this server already exposes, so running both only duplicated them.
Why AWS is user-scoped
aws-mcp is registered at user scope by bun run aws.setup, not declared in .mcp.json. Nothing to do by hand — the same command that writes your AWS profiles registers it, because the two are the same setup: this server signs with the credentials that command provisions.
The scope is the point. Project-scoped servers are loaded automatically in cloud sessions, without the approval prompt a local session shows. A cloud session has no ~/.aws, so the proxy would load there with nothing to sign with — dead on arrival, and exposing a duplicate set of aws___* tool names alongside the claude.ai AWS connector, which is the only AWS client that works in that environment. User scope stays on the machine where the credentials actually live.
That leaves one rule worth stating plainly, because nothing enforces it:
Expect to hold both clients, and know which one you are calling. A cloud session can only use the claude.ai connector — the sandbox has no ~/.aws for this server to sign with. A local session should prefer this one: it signs with your own credentials, and it is the name the committed allow list covers.
They do not de-duplicate. Claude Code matches a connector against a local server by endpoint, and it cannot see through this one — the URL is an argument to uvx, not a url field — so in a local session both load and expose identical aws___* tool sets under different prefixes. Prefer the aws-mcp one there; /aws-login is what keeps the connector alive for the cloud sessions that have no alternative.
To re-register after a version bump, or if you ever remove it:
bun run aws.setupPass --no-mcp to devtools/aws-config-setup.sh to write the AWS profiles without touching your MCP config.
When AWS calls prompt
.claude/settings.json covers only the read-only lookups — documentation, regions, tasks — for the server names it pins, so those run promptless there. Both general-purpose tools, aws___run_script and the deprecated aws___call_aws, prompt on every install: they reach every AWS API through one name apiece, so pre-approving either would pre-approve writes. A hosted connector's server segment is a per-install id that matches no checked-in rule, so even the lookups prompt there.
Nothing in the harness holds a write back, and no permission rule can: MCP rules match on server and tool name, and the two general-purpose tools reach every AWS API through one name apiece. So a call reaching AWS unprompted says nothing about whether it was a read. IAM is the usual backstop — Agent-ReadOnly is what an agent gets when nothing intervened — but it is not a boundary, and it is not reliably even the identity: an admin profile is selectable per call and static credentials in the environment override both (aws-access.md, "What the split does not stop"), while through the connector the principal is whichever AWS session was chosen at consent time. sts get-caller-identity, run through the path you are about to use, is the only thing that answers it. What governs mutations is the aws-operations skill, Environment > Identity.
Because that rule lives in a skill that loads on description match, a PreToolUse hook — .claude/hooks/aws-mcp-skill-reminder.sh, the same shape as the OpenSearch and prod-DB reminders — restates it into context ahead of every call, so a session that never loaded the skill still has it. The hook reads nothing and decides nothing; it cannot tell a write from a read and does not try.
Every package spec is pinned to an exact version
Two reasons, and the second is the non-obvious one:
- Reproducibility — every developer and every session runs the same server build.
- Session start stays off the network. An exact spec is a stable cache key, so the launcher reuses the environment it already built. An unpinned or
@latestspec makes it revalidate against the registry on every launch, and a revalidate slower than the MCP handshake timeout leaves the server absent from the session with no error naming a version. That failure is invisible and intermittent, which is what makes it get misread as an outage in whatever the server talks to.
Never write @latest in a package spec. It forces the per-launch revalidate by design — it is not a safety net. devtools/__tests__/mcp-config.test.ts fails CI if one reappears.
A pin is pkg==1.2.3 or pkg@1.2.3 — both are exact, and uvx accepts either. The test classifies by position, because that is how uvx reads its own command line: the values it resolves are the --with / --from constraints and the first positional, and everything from that positional onward is an argument to the tool. Each resolved package must carry an exact version — directly, or through a constraint naming the same distribution, which is how uvx --with mlflow[mcp]==3.15.1 mlflow … pins its entry package. A --with written after the package is a tool argument, not a constraint, and uvx never sees it.
Prefer uv cache prune over uv cache clean. clean wipes the cache entirely, so every uv-backed server drops back to a cold first use (below) on its next launch. prune drops only unused entries and keeps the environments the servers reuse.
First use on a machine
The first launch of a given spec builds its environment rather than reusing one. That build can outrun the MCP handshake timeout, and the consequence is not a slow server — it is a server absent from that session, with nothing in the failure naming a package build. On a fresh clone all six resolve cold and concurrently, so the heaviest ones are the likely casualties: opensearch and mlflow-mcp are the two that take longest even warm.
It is self-correcting — the build finishes in the background, so restarting the session once picks up everything. To avoid the bad first session entirely, warm a spec outside a session first:
uvx opensearch-mcp-server-py==0.11.0 --help
uvx --with 'mlflow[mcp]==3.15.1' mlflow --helpThe same applies right after a version bump or a uv cache prune, for the specs affected.
An interrupted npx install leaves a package with no bin link
npx caches one tree per spec under ~/.npm/_npx. If a first run is interrupted mid-install — Ctrl-C, a session restart, a laptop asleep on a slow download — that tree can be left holding the installed package but no node_modules/.bin symlink for the server's binary. The next launch finds a cache entry, concludes there is nothing to install, and fails with:
sh: sf-mcp-server: command not foundNothing in that names npx, a cache, or a version, so it reads as the tool being absent from the machine rather than a half-written cache. The fix is to drop the tree and let the next launch rebuild it:
rm -rf ~/.npm/_npxThat is the blunt form and it costs every npx-launched server a cold first use, the same tradeoff as uv cache clean above; there is no prune equivalent, so removing only the offending hash means finding it by hand.
uvx does not have this failure mode. Neither does bunx, which recovers cleanly from a cold cache plus a hard-interrupted first install — a real argument for it, and the reason it is still not used here is below.
Why primevue stays on npx
Every other tool in this repo is bun, and bunx handles the cache better, so this looks like it should be a one-word change. It is not.
@primevue/mcp does not pin its own MCP SDK. It depends on @primeuix/mcp, which declares @modelcontextprotocol/sdk as ^1.25.2, and that caret currently floats to 1.30.0 — a release that rejects the server's own tool registrations:
Failed to start PrimeVue MCP Server: Error: Tool get_composable expected a Zod
schema or ToolAnnotations, but received an unrecognized objectThe second -p in the spec is what prevents that: passing -p twice makes npx install both packages into one tree and run the named binary from it, so the exact SDK sits where @primeuix/mcp resolves it. bunx takes only one -p — a second replaces the first rather than adding to it, in either order — so the constraint has no bunx spelling. Plain bunx @primevue/mcp@4.5.4 starts, then dies on the error above.
Expressing the pin the bun-native way does not work either. It would mean a @primevue/mcp devDependency plus an overrides entry, and this repo already resolves @modelcontextprotocol/sdk at 1.29.0 as a transitive of @anthropic-ai/claude-agent-sdk, whose peer range requires ^1.29.0. Scoping the override to one dependent is the obvious way out, and Bun warns and ignores it — nested overrides are unsupported — leaving only the flat form, which would pin both consumers to one version. So npx here is a deliberate exception rather than something nobody got around to.
Upgrading a server
Edit the version in the spec in .mcp.json and restart the session. Warm the new spec first, per above, or expect to restart a second time.
primevue carries one extra constraint: its @primevue/mcp version must match the primevue in package.json, because @primevue/mcp releases track PrimeVue's and a mismatched pin serves documentation for a version the app doesn't run. The same test asserts the two agree, so bumping one without the other fails rather than drifting quietly.
Its SDK pin is held to a value, not just an exact shape — the test names the version verified to start the server — so raising it reds CI until the test is edited too. That is the intent: the failure it guards against is a startup error naming no version, and the only thing that clears a new SDK is starting the server against it and watching it answer an initialize.
When a server is missing from a session
Missing tools are not evidence that the service behind them is down — an MCP server and its backing service fail independently (AGENTS.md, "A missing MCP tool is not an outage"). Probe the service directly before concluding anything. On a machine that hasn't run this spec before, First use is the likeliest cause and a restart is the fix. If restarting does not fix it and the server is npx-launched, the cache tree is the next thing to suspect — that failure survives any number of restarts.
For OpenSearch, the opensearch-investigation skill carries the reachability probe and a curl degrade path that keeps an investigation moving while the MCP is unavailable. aws-operations carries the AWS case, where there is no CLI degrade path — a missing server means restarting the session — alongside the account facts a tool description can't.