A local router for every coding agent you use
My Claude Code is a local server that speaks four protocols at once — Anthropic's Messages API, OpenAI's Chat Completions and Responses APIs, and Google's Gemini API. Whichever one your agent already speaks, it believes it is talking to that vendor. This proxy receives the request, forwards it to whichever provider you configured, and translates the reply back into the wire format the agent asked in. Streaming, tool use, reasoning and image input all keep working.
The 16 mcc-* launchers — Claude Code, Codex, OpenCode,
Gemini CLI, Crush, Cline, Goose, Aider, Kimi Code, Qwen Code, Command
Code, Droid, Pi, Kilo, Roo Code, Antigravity — and the desktop apps
beside them all point at one address and share this one control panel.
Claude Code is one first-class client here, not the frame around
everything else; it leads the walkthroughs below only because it is the
one most readers arrive with.
Three things follow from that:
- The server must stay running. It is a daemon, not a library.
- Your agent's model picker can list this catalog, not the vendor's. Choosing Sonnet routes to whatever you mapped Sonnet to. Codex and Pi's pickers always do this; Claude Code's needs model discovery on (see the Claude Code section below).
- Provider keys never leave the machine. Your agent only holds a token that authenticates it to this proxy.
Three steps to working: add a provider key, map the model tiers, point
your agent at http://127.0.0.1:8082.
First run, end to end
Eight steps, in order, from nothing installed to a request you can
read in the log. Everything after this section is a single topic in
depth; this is the path through all of them. It is written with Claude
Code as the client because that is the most common arrival, and every
step except the one that writes its settings.json is the
same for Codex, Gemini CLI, Aider or any other agent — the
“Point another CLI here” section swaps that one step out.
-
Install. In the Windows Command Prompt,
curl -fsSL -o "%TEMP%\install-mcc.cmd" https://raw.githubusercontent.com/FiredMosquito831/my-claude-code/main/scripts/install.cmd && "%TEMP%\install-mcc.cmd"— it fetches the same installer and runs it with the execution policy bypassed, so PowerShell can never refuse it. In PowerShell,& ([scriptblock]::Create((irm "https://raw.githubusercontent.com/FiredMosquito831/my-claude-code/main/scripts/install.ps1"))); in WSL, Linux or macOS,curl -fsSL "https://raw.githubusercontent.com/FiredMosquito831/my-claude-code/main/scripts/install.sh" | sh. On any platform with Node,npm install -g @firedmosquito831/my-claude-codeis the same thing through a third door: the package is a wrapper that runs the digest-verified installer above with the desktop flag, so it leaves you the server, everymcc-*command and the desktop app, plusmccas an alias (npx @firedmosquito831/my-claude-coderuns it without installing globally, and installs nothing until you ask for a server). Pick one environment and stay in it — PowerShell and WSL keep separate config directories (C:\Users\<you>\.mccand~/.mccinside WSL), and installing in both is the usual way to end up editing the config your server is not reading. There is a second way in on every platform: download the desktop app from the latest release — MyClaudeCode-Setup-windows-x86_64.exe, MyClaudeCode-linux-x86_64.deb (or the.tar.gz), or MyClaudeCode-macos-universal.dmg — and run it. That installs the desktop app only — no Python, no server, no admin rights — and the app runs the command above for you, in its own window, the first time you launch it. The macOS image is unsigned, so after dragging the app into Applications runxattr -d com.apple.quarantine "/Applications/My Claude Code.app"once; installing with the command above and runningmcc-desktopavoids that step entirely. -
Reopen the terminal, then start the server:
mcc-server. It is a daemon and must stay running; close it and every agent pointed at it stops working. The startup log prints the admin URL. -
Open the dashboard at
http://127.0.0.1:8082/admin. The Get Started page is a live checklist — each step is ticked because the thing is really configured, not because you clicked something. Work down it and you have done this walkthrough. -
Add a provider. Two ways, and both end on the same
card:
-
A provider MCC already knows: on
Providers, type its name into
Search providers — the box matches the variable
name too, so
GROQ_API_KEYfinds Groq. Press Configure, paste the key into Add key, press it. - Anything else with an OpenAI-compatible endpoint: press Add custom provider, give it a base URL and a key, and press Add provider. MCC probes the host once to learn which dialect it speaks and records what came back.
-
A provider MCC already knows: on
Providers, type its name into
Search providers — the box matches the variable
name too, so
-
Look at what you actually got on the
Models page. Every model the provider offered is
here, each with where its facts came from and which tiers it is
eligible for. If a model you expected is missing, it was missing
from the provider's own
/modelsanswer — press Refresh models on the card rather than editing anything here. - Set the tiers on Model Config. Default Model is the only required one: it answers anything you have not mapped. Map the small tier to something cheap and fast before you map anything else — agents hit it constantly for bookkeeping, and it governs how quick the whole session feels. Press Apply.
-
Launch an agent from Coding agents.
The page lists every CLI this proxy can launch and says whether each
binary is on your PATH. Copy a command from a card you have installed
—
mcc-codex,mcc-opencode,mcc-crush,mcc-geminiand the rest — and run it. The launcher hands that CLI a config file this server already wrote; nothing of yours is edited. - Read the request on Analytics. Ask the agent anything, then refresh. The row names the model that answered, the agent that asked, and what it cost. If nothing appears at all, the agent never reached MCC — the request went straight to a vendor instead, and the fix is in that agent's configuration, not here.
A tick on Get Started is a measurement, not a memory. Every required step is recomputed from live configuration each time the page loads, so a step un-ticks itself if you later remove the key it was reporting. The two optional steps that cannot be measured — reading this Guide, and opening the Coding agents page — are done because you went there.
Add a provider
On the Providers tab, type the provider's name into
Search providers — it also matches the variable name, so
GROQ_API_KEY finds Groq. Press Configure to open
that provider's key pool, paste your key into Add key and press
it. Keys save immediately; Apply is not needed for them.
A provider holds a pool of keys rather than one value. Paste several separated by commas to add them together, give each its own Remove, and pick a Rotation policy when you have more than one (that one does need Apply, and restarts the server). Then press Refresh models: it makes a real API call, so a model count means the key works and My Claude Code can read that provider's catalog. Choose the model itself on Model Config.
OpenCode Zen and OpenCode Go read who is calling. Both
gateways take a set of identity headers off every request — which
program, which version, which project, which conversation, which call.
Before 6.69.0 My Claude Code sent none of them, and on 2026-09-10 the Zen
free tier was measured answering such a request
400 MissingSessionID, “OpenCode's free tier can
only be used in OpenCode”. The same request carrying the five
headers was answered 200. So 6.69.0 sends them, and by
default it sends the values the official OpenCode client sends. It is not
the OpenCode client, so that is a claim made on your account's behalf: the
OpenCode Client Identity field on the OpenCode Zen card switches
it to mcc, which sends my-claude-code and this
version instead. The conversation header — the one the host was
measured acting on — goes either way, and the header naming your
coding agent never leaves this machine. If the host later benches you on
its daily free quota, the 429 that resets at midnight UTC is recorded
against the provider and shown on Models.
New in 6.74.0 — OpenCode Zen is four APIs, not one.
Which one a model is served on is a property of that model, published in
OpenCode's own registry, and two of the seven free models Zen lists today
(both Muse Spark contributor models) live on the Responses API. Sending
them to Chat Completions came back as a bare HTTP 500 with
nothing in it — the vendor has an open issue calling that a missing
“wrong endpoint” error. MCC now resolves the endpoint per
model and shows it on Models as a
wire surface row: responses (registry) when the
vendor published it, (learned) when MCC probed this
deployment and found out, (override) when you pinned it. If
the endpoint MCC picked refuses in a way that is about the endpoint, MCC
spends one tiny probe on the other one, remembers the answer and retries
your request there. Nothing is ever hidden: a model Zen serves through an
API this provider does not speak keeps its row and reads
unservable with the reason.
| Result | Almost always means |
|---|---|
| 401 / 403 | The key is wrong, expired or revoked. |
| 404 | The key is fine — the model id is not on your account. |
| 402 | Billing: no credit, or plan quota exhausted. |
| Timeout | Network, or a local endpoint that is not running. |
That 404 case catches people constantly — check the exact model id against the provider's own list before assuming the key is bad.
An OpenAI-compatible endpoint with no card of its own goes in under
Custom providers — a display name, a base URL and a
key. Two things differ from a built-in card. The base URL is used
verbatim, so it must already end in /v1 (or
whatever the gateway's own curl example puts before
/chat/completions); My Claude Code does not append it. And
its keys live in ~/.mcc/custom_providers.json, not
~/.mcc/.env. Creating, enabling or re-keying one reads its
model list once and publishes it everywhere immediately, with no restart;
if that read fails the card says so, and Refresh models retries
on demand.
Local backends take a base URL instead of a key, and offer Test connection where remote providers offer Refresh models:
LM_STUDIO_BASE_URL="http://127.0.0.1:1234/v1"
OLLAMA_BASE_URL="http://127.0.0.1:11434"
Everything is stored in ~/.mcc/.env. Edit that file directly
if you prefer, then restart the server — config is read at startup.
If you installed before 6.40.0, your configuration
is in the legacy ~/.fcc instead — the same file,
the same everything, just the older directory name. It stays fully
supported and MCC never moves it for you. The startup log names the
directory actually in use, and so does the banner at the top of
Get Started.
To move it, stop the server and quit the tray, run
mcc-migrate in a terminal, then start the server again.
That renames the directory in one atomic step — nothing is
copied, deleted or merged — and leaves a
~/.fcc-old/RESTORE.txt note saying how to undo it. There
is deliberately no button here for it: relocating your keys and your
request history should not be one click away in a browser tab, and on
Windows the rename cannot succeed while this server is running
anyway. To pin a directory instead, set the MCC_CONFIG_DIR
environment variable to an absolute path before starting the server.
The file records what you chose, not what the defaults are. A
setting you never touched is written as a commented placeholder
naming its default — # FALLBACK_BENCH_ENABLED= (default: false)
— while a plain KEY=value line means the dashboard
set it, and the field shows set here. Every field prints its
default underneath, and one that was set gets a Use default
button that removes the line again. That is also why a switch is a
three-way select — Default, On, Off
— and why every list carries a Default option: a
checkbox has nowhere to say "I have not chosen". Leaving a setting
alone is what lets a later release improve its default for you;
choosing the default explicitly pins it against that.
Use Claude models
There are two Anthropic providers and the difference matters.
Anthropic (Claude API) is the ordinary path: a
Claude Console API key, billed per token like every other provider
here. Create one at
platform.claude.com,
paste it into the card, press Refresh models, and set a
model ref such as anthropic/claude-sonnet-4-6. This is
the authentication method Anthropic documents for software that
calls Claude on your behalf.
Anthropic Claude subscription (OAuth, Caution) is not permitted by Anthropic. That is the card's own label, and the reason for it: their terms state that Free, Pro and Max OAuth credentials are for Claude Code and Claude.ai only, and that third-party products may not route requests through them. There is no “inside Claude Code” exemption — once My Claude Code is in the path, it is My Claude Code that presents your credential upstream. Anthropic may enforce without prior notice, and the risk is to your Claude account.
“Caution” is three standing restrictions, not
“experimental”. The policy above is the first.
The second: the credential serves only Anthropic's own
entrypoints — cli, cli-bg,
sdk-cli, sdk-py and sdk-ts,
which is the Claude Code CLI and the Claude Agent SDK. MCC reads the
cc_entrypoint marker Claude Code writes into the request
body, and every other harness pointed at your proxy — OpenCode,
Cline, Crush, a bare API call — is refused rather than quietly
billed to your plan. (Until 6.36.0 the gate admitted the CLI alone
and wrongly refused the Agent SDK.) The third: it is one
credential and never a rotation pool — a comma-separated
ANTHROPIC_OAUTH_ACCESS_TOKEN is rejected outright,
because rotating subscription credentials is the “unusual
traffic pattern” Anthropic's own policy names.
If you do enable it, four buttons matter. Sign in with Anthropic opens a loopback callback where one can work and falls back on its own to a paste prompt under WSL or over SSH — paste either the code Anthropic shows you or the whole callback URL from the address bar, both are accepted. Use Claude Code credentials imports the login you already have (on macOS that usually lives in the login keychain, which MCC cannot read — sign in instead). Refresh now and Disconnect light up once MCC holds a credential of its own. None of these needs a server restart; they take effect on the next request.
Two failures that look alike and are not. If MCC's
own store is dead — access token expired and the refresh token
past its own expiry — MCC falls through to Claude Code's
~/.claude/.credentials.json and logs which source it
chose and which it skipped. A refresh that fails with
429 or 5xx is transient: the credential is kept, it
is reported as an ordinary rate limit or overload like any other
provider's, and it does not mean sign in again — doing
so would rotate a working credential away. Only a 400/401/403 with a
real OAuth error body is definitive, and then the store is
renamed aside to
anthropic_oauth.json.dead-<epoch> rather than
deleted. Disconnect does the same. Claude Code's own file is
never written to in any of these paths.
Read docs/ANTHROPIC-SUBSCRIPTION.md — it is on the
Docs tab — before switching it on. The
supported alternative is the Claude API card above.
Claude models also arrive through bedrock,
vertex, and gateways such as kilo,
nous_portal and cline — all
pay-per-token, none with a policy question attached. And the two-door
pattern still works: keep native claude for your
subscription, and use mcc-claude for everything else.
Map model tiers
Routing is by tier: Mythos, Fable, Opus, Sonnet, Haiku
and a
fallback each map to a real model on your provider. That is what lets an
unmodified agent run on any backend. Each rail heading also carries the
id every other coding agent asks for to reach it —
Mythos (mcc/cyber),
Fable (mcc/best), Opus (mcc/good), Sonnet
(mcc/medium), Haiku (mcc/cheap) and the Vision
adapter (mcc/vision). The Default rail carries none, because
it is not a tier: it is the route every unset rail falls back
to, including Fable's. Before 6.51.0 mcc/best named the
Default rail; if you never filled Fable in, it still lands there.
Mythos is new in 7.2.0 and ships unset, so
claude-mythos-5.1 and mcc/cyber follow
MODEL until you fill the rail in — which is exactly
where they went before 7.2.0, silently and with no rail to say so.
Mythos leads the list but is never the default: apps that are told a
default model are still told mcc/best.
Map Haiku to something cheap and fast. Agents use the small tier constantly for internal bookkeeping, and a slow model there makes the whole session feel sluggish even when your main model is quick. This affects perceived speed more than any other setting here.
Reserve the big tier for real work, and point the fallback at something cheap so unmapped requests cannot surprise you.
Every other coding agent gets these tiers too. Claude
Code never names a model — it asks for
claude-sonnet-5 and gets whatever you mapped Sonnet to.
Every other agent had to name a real provider/model ref, so
a model id typed into its config a month ago is still that id today
however many times you moved the route it should have followed. Six
names now sit at the top of every generated picker —
mcc/cyber (Mythos),
mcc/best (Fable), mcc/good
(Opus), mcc/medium (Sonnet), mcc/cheap
(Haiku) and mcc/vision — each carrying that route's
own fallback chain and pause list. An unset tier follows
MODEL, exactly as claude-opus-5 does, and the
card says so rather than inventing a model for it. One agent can be
given its own chain per tier under Tiers on its
Coding agents card; nothing else is affected.
Managing many models at once. Each provider header
carries Show all, Hide all and Invert, and
they act on whatever the filter and the state chips have narrowed the
provider to. Tick the boxes in the left gutter to pick models by hand
— click one and Shift-click another for a range,
Shift+arrow to extend or shrink one from the keyboard, or press
and drag down the gutter — then use the bar at the bottom. That
bar is the only way this page changes what the catalogue
shows, for one row as much as for three hundred; the word beside each
checkbox is a readout — Shown, Hidden, or
Hidden by nous_portal/* — and not a second control.
Hide all for a whole provider writes one pattern like
nous_portal/* as a standing policy, so models that provider
adds next week are hidden on arrival too. It shadows the exact
patterns underneath rather than deleting them, so the first
Show all afterwards lifts only the glob and hands back exactly
the per-model state you had before; press it again to clear those too.
A glob you wrote yourself is never deleted on your behalf — if one
of yours still hides a model, that row says Hidden by it, and
clicking the pattern offers to remove it. None of this changes routing:
a hidden model still resolves and still serves.
994 patterns and no globs? Ticking models one at a
time writes one exact pattern each, and a real install reached 994 of
them — a 30 KB line rewritten on every write.
Migrate exact patterns to globs, beside Save patterns,
folds every provider whose models are all individually hidden into one
provider/*. It previews first: how many patterns become how
many, and how many models are hidden before and after. It is only
offered when those two numbers are equal, and the write is undoable.
Filter first, then act on what is left: / jumps to the
search box, Select all N on the result count takes exactly the
filtered set, and Escape clears the selection again.
Provider headers stick to the top of the list with their
visible/hidden/configured counts, and Hide all works while the
provider is collapsed. Above 200 models the button asks once, in place.
What happened is written into the status panel and stays there —
no toast to miss — with an Undo that restores both lists
exactly.
Give each tier a fallback chain. Press Add fallback under a tier's model to name a stand-in, and another for a stand-in after that. If the model a request routes to cannot serve it, the next entry takes over — a free model that rate-limits at the wrong moment stops being the end of the request.
Reorder a chain by dragging it. Every entry has a grip on its left. Drag it to move the row; hold Ctrl (or Cmd) while clicking to pick several, or Shift-click for a run — Shift+Space and Shift+↑/↓ on a focused grip do the same from the keyboard. Drag onto another tier’s card to copy the model there, hold Shift as you drop to move it instead, and drop onto a card’s top slot to make that model the route’s own — whatever it replaces becomes fallback 1. A group lands in the order it has on screen. The up/down arrows still do the same job one step at a time, Ctrl+Z undoes the last drag, and nothing is written until you press Apply.
Pause one entry without deleting it. Each row has a Pause button. A paused model keeps its place and stays on screen with its whole name, but the router never tries it: no attempt is spent on it and no deadline is consumed, and it still appears in the request log under not tried with the reason paused — so a paused route is still readable. A pause takes effect immediately, with no Apply, and the panel at the top of this page offers an Undo. If it fails, that same panel says so and the row returns to the state it was really in. Pausing an entry on one route does not pause the same model on other routes. A ref that appears in two chains is two entries; pause it on each route you want it off. Pausing is not hiding. Hiding a model on the Models page only changes what appears in listings and never changes routing; pausing only changes routing and never changes listings.
Failover stops once you have seen output. A model that fails while connecting, authenticating or rate-limiting is replaced silently. One that dies halfway through streaming an answer is not, because switching then would splice two different replies together — so a chain rescues the failures that happen before the first word, not the ones that happen at word five hundred.
A context-length rejection no longer ends the chain.
A model that answers “this prompt is too long” with an HTTP
400 used to stop the route dead, because a 400 normally means you
sent something invalid and retrying it elsewhere is pointless. A
prompt that overflows one model’s window is the exception: the very
next model on the chain may hold twice as much. Those rejections are now
classified as context_length and the chain continues, so a
long session falls through to a roomier model instead of failing. The
reason is spelled out when it happens — Request exceeds this
model’s context window. Needed about N tokens; this model holds
M. To get the old fail-fast behaviour back, add
context_length to FALLBACK_SKIP_KINDS, which
defaults to invalid_request.
Neither does a model refusing the request. Since
6.46.0 a 400 only ends the route when the provider’s
own message says the request is malformed. Every other
400 — a model that host does not serve on that
endpoint, a sampling value one model pins, a tool name longer than
that dialect allows — is the new model_rejected
kind, which is not in FALLBACK_SKIP_KINDS by default, so
the next model gets its turn. Measured over 274,375 requests, every
400 this install ever received was one of the latter,
and each one ended a chain with healthy models still in it. Your
client sees no change: both kinds still go out as
invalid_request_error. To restore the old behaviour, set
FALLBACK_SKIP_KINDS=invalid_request,model_rejected.
Neither does running out of credits. Several
gateways answer an account with no balance left with an HTTP
400 that calls itself an invalid request
— one measured on 2026-09-02 said “You have
insufficient credits to make this request. Please purchase
more credits to continue using the service.” and
six configured fallback models were never tried. That is now
its own kind, quota, matched on the
provider’s own words (a 402, or a 400/403 naming an
explicit billing phrase) and never on a bare mention of
“credit”. MCC rotates to the next key first
— another key may have credits — then falls
through to the next model, and benches the empty key for
RATE_LIMIT_COOLDOWN_SECONDS. The Models page
shows it as COOLDOWN — credits exhausted. If
every model ends this way you are told plainly:
all keys reported exhausted credits, pointing at
Providers.
Images are routed separately. Plenty of fast text models cannot read a screenshot at all. Set a Vision adapter and any request carrying an image goes there instead, but only when the tier's own model is known not to accept images — a model nobody publishes capabilities for is left alone rather than diverted. When that applies to one of your tiers, its card says so directly, and the adapter gets its own Add fallback chain like any other route.
Or the image can be described instead of rerouted. Vision adapter mode, in the same card, chooses. Route — the default, and what every release before 6.51.0 did — sends the whole request to the vision model, so the vision model answers it. Describe sends each image to the vision model on its own, puts its answer in the picture’s place, and lets the model your tier actually picked answer the question. Pick describe when your coding model is fast, cheap and blind and the screenshot is context rather than the question — in route mode the vision model has to answer a coding question it has no context for. Pick route when the picture is the question. Descriptions are cached against the image itself, so a screenshot re-sent every turn is described once; Clear image descriptions on Requests forgets them. If a describe call fails, the request falls back to route mode and then to a plain sentence — it is never the reason you lose an answer.
An image a tool returned now arrives as an image. A
browser screenshot, or a Read of a PNG, comes back nested
inside the tool result — and no OpenAI-format chat message can carry
an image there. Before 6.49.0 MCC turned it into base64 text, and
every OpenAI-dialect provider billed roughly one token per byte for a
picture the model never saw: one 213 KB screenshot measured 324,000 prompt
tokens, four of them failed the request outright. It is now moved into a
short message right after the tool output, marked as tool output rather
than as something you typed. TOOL_RESULT_IMAGE_DELIVERY on
Model Config picks the rule, and each request records
which one applied.
Afterwards, Analytics shows the whole path a request took: which models were tried, which one answered, and whether the vision adapter stepped in.
What MCC knows and learned
The Models page is where every model MCC has discovered lives, and it answers two different questions about each one: what its provider publishes, and what this deployment has actually taught MCC by refusing something.
Catalogues re-read themselves. Every usable
provider's model list is fetched again in the background once an hour,
so a gateway that added a model this morning is listed this afternoon
without a restart. The line under Providers and models says
when that last happened and when it happens next; the server log says
catalogue changed: +N -M when the list actually moved.
MODEL_DISCOVERY_REFRESH_SECONDS is the interval, in
seconds, and 0 turns the sweep off — in which case
the same line says so rather than going quiet.
This is not Claude Code's model discovery.
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY is a variable
MCC writes for Claude Code and never reads; it decides whether
Claude Code's own picker lists this catalog. The setting above decides
how often MCC re-reads its providers. Changing one does nothing to the
other.
The Learned chip filters to models this host has corrected. Every negative MCC holds was learned from something the upstream itself said — a 400 naming an output ceiling, a retry that only succeeded once a reasoning field was dropped, a probe that came back without vision. Open a model and the What this host taught MCC block lists them with the evidence, the age, and a Forget link.
Facts survive a restart — they are written to
~/.mcc/learned_facts.json — so an upgrade does not
re-pay every 400. They expire because they have to: MCC stops sending
the thing that would produce a positive, so a host that raises its own
cap has no way to tell us. Three evidence classes, three clocks:
| Evidence | Example | Good for |
|---|---|---|
| Stated | a number or an enum the host named in its own words — an output cap, the effort words it accepts | 30 days |
| Inferred | “it worked once the field was gone” — a rejected reasoning field, no streamed usage, no vision, no tool calls | 7 days |
| Withheld | a model id the backend refused by name | 72 hours |
Expiry never deletes. A fact past its clock is still loaded and still shown — marked stale — and simply stops being applied. The next real request re-pays one rejection and the row is fresh again. That is the self-healing a restart used to give for free, put back on a timer.
Probe capabilities asks instead of waiting. The button on a provider card sends a handful of deliberately tiny requests — a one-pixel image, a trivial tool definition — and writes down what came back, so a model's real vision and tool support are known before a real request pays for the discovery. A probe is narrow: it only ever records an absence the host demonstrated, never a capability the catalogue already claims. No response text is stored for any fact — the evidence is a bounded, redacted excerpt, and for a probe it is a status word and at most an HTTP code.
Forgetting works at three scopes. Forget on a row drops that one fact; the button on a provider card drops everything learned about that provider; Forget everything learned under Requests clears the store. Use it when you have changed something on the host's side — a raised quota, a new deployment behind the same name — and do not want to wait out the clock.
Tiers for every agent
Claude Code never names a model: it asks for
claude-sonnet-5 and gets whatever you mapped Sonnet to.
Every other coding agent had to name a concrete
provider/model ref, so a model id typed into its config a
month ago is still that id today, whatever you have since changed here.
Five aliases close that gap. They are pointers to the same routes the
Claude aliases use:
| Alias | Points at |
|---|---|
mcc/cyber | MODEL_MYTHOS — the Mythos route, listed first, never the default |
mcc/best | MODEL_FABLE — the Fable route, and the alias apps are given as their default model |
mcc/good | MODEL_OPUS |
mcc/medium | MODEL_SONNET |
mcc/cheap | MODEL_HAIKU |
mcc/vision | MODEL_VISION |
Inside a CLI these appear under the provider mcc, which is
why OpenCode's own flag reads -m mcc/mcc/best — one
mcc for the provider, one for the alias namespace. Two
segments, not three: mcc/tier/best would buy nothing and
would make Kimi's generated key mcc/mcc/tier/best.
An unset tier collapses onto MODEL. If you
have not mapped Opus, then mcc/good resolves to the same
place an unmapped mcc/best does — exactly as
claude-opus-5 already does today. Four agents asking for
four different tiers can therefore all land on one model, and nothing is
wrong. The dashboard says so rather than hiding it: MCC choosing a
distinct model for a tier you left unset would be MCC picking a model
for you.
Per-agent overrides. The Coding agents card has a Tiers section for saying "best means this in Crush, and the global thing everywhere else". Each (agent, tier) pair has three states, and the middle one is the reason the feature exists:
- No entry — inherit the global tier chain. The default for every agent and every tier.
- An entry with no model — keep the global primary, but use this agent's own fallbacks and pauses.
- An entry with a model — this agent's own chain leads.
A tier may never point at a tier. An override resolving
to something in the mcc/ namespace is dropped with a log
line, because it would either loop or resolve through a provider id that
does not exist — and neither failure is visible from the file that
caused it. Overrides live in ~/.mcc/harness_tiers.json; the
card is the supported way to edit them.
Reasoning
Whether a request carries a reasoning instruction is decided by two facts, not one: what the model has a knob for, and what the host has a field for. A control is only sent when both are true. When they disagree the nearest thing both can express is sent instead, and when nothing can be, nothing is sent and the request detail says so in those words — no reasoning instruction sent (model default applies). Each model on the Models page states which dialect its host speaks and where that came from: default OpenAI dialect, declared by this provider, or learned from the host's own rejection.
Hosts that never mention reasoning still get it. Any
OpenAI-compatible host with no dialect of its own is assumed to parse
reasoning_effort over the classic four rungs
minimal | low | medium | high — sent only when the
model's own metadata says it has effort control. If such a host answers
400 naming that field, the request is retried once without it, that
provider and model is remembered for the life of the process,
and the Models page relabels the dialect as learned. A 400 about
anything else still fails visibly.
Every provider learns this way, not just the
OpenAI-compatible ones. An Anthropic-protocol host that
refuses a thinking object, and a Responses host that
refuses a reasoning block, are read by the same matcher
and relabelled the same way. The same 400 also teaches the
output cap a host states for itself —
max_tokens: 100000 > 64000, which is the maximum allowed
number of output tokens for … — and later requests
clamp to it without paying the rejection again. Both memories are lost
on a restart or a config reload, on purpose: a host that was briefly
broken heals by itself.
A rung you ask for is a rung you get. An effort you
name that the host cannot spell is clamped to the nearest rung it can,
never quietly replaced by that host's own default — so
max becomes high on a four-rung host rather
than medium. The same rule covers a model that only has
on/off sitting behind a host that only speaks effort: it leaves as your
rung, clamped. Hosts with an honest on/off field keep using it. Where a
catalogue disagrees with itself about a reasoning control, the more
capable of the two records wins — controls only; numeric limits
still take the tightest.
Reasoning needs room, and takes it from the same budget as the
answer. Thinking tokens and reply tokens come out of one
max_tokens, so an agent asking for 8,000 can spend all of
it thinking and return one sentence. When the resolved policy asks for
reasoning, the budget now starts from
the larger of the client's ask and the model's own published
output limit, then narrows as before — model limit,
ceiling, context headroom. A model whose limit nobody publishes is
never widened, and max_tokens: 0 is left alone. The
request detail names it when it happens:
max_tokens raised from 64,000 to 131,072 for reasoning.
Turning reasoning off changes nothing about the ask.
What comes back is measured separately from what was
asked. The Models page counts reasoning requested and
reasoning returned as two independent facts, and Analytics
stores the second as thinking_chars: 0 means
a stream completed and carried no reasoning, while
Not measured means nobody counted. From 6.73.2
the ChatGPT (Codex) provider returns thinking blocks
whenever the endpoint sends a reasoning summary — its Responses
protocol names that channel as an event type rather than a delta field,
and the converter had no branch for it, so 2,671 of 2,671 requests over
seven days read returned 0 while the summary was arriving on
57% of them. Rows written before that release stay at 0:
they record what was delivered, and nothing is backfilled. A turn where
the endpoint sends only encrypted_content and no summary
still reads 0, and correctly — the model thought and
released none of it, so no empty block is invented.
MAX_OUTPUT_TOKENS_CEILING on
Limits & Resilience caps that widening
and ships set to 131,072. It is set on purpose:
widening asks for the model's maximum, and OpenAI- and Azure-style
limiters reserve max_tokens against your per-minute budget
before generating a word, so an unbounded thinking turn on a
262,144-output model can be rate-limited for tokens it was never going
to use. Blank means the default; 0 is the sentinel for no
ceiling at all.
Point Claude Code here
Claude Code reads its settings file, and that file
wins over exported environment variables — so
export ANTHROPIC_BASE_URL=... will appear to do nothing if
the file says otherwise. The fastest way to point it at this proxy is the
button on this dashboard, not hand-editing JSON.
1. Open the Claude Code page from
the left nav. The path field is pre-filled with
~/.claude/settings.json, and the card lists every settings
file it can see on this machine — if this server runs under WSL
while Claude Code runs on Windows, that includes the Windows-side file
too, since those are two different files.
2. Press Configure. It writes
ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN
into that file's env block, merging with anything else
already there. The previous file is backed up next to it with a
.fcc-backup suffix the first time it writes. If the file
already points somewhere else, the status line shows what is there before
you overwrite it; Remove deletes just those two keys. If
a higher-precedence file (like an enterprise managed settings file) also
sets these variables, the card warns you which one wins.
3. Restart Claude Code and run /status. It
should report Anthropic base URL: http://127.0.0.1:8082. If
it still shows Anthropic, the file was not picked up — check the
path and that the JSON is valid.
4. Pick a model. No overrides needed to select a tier by name — but the picker below only lists the MCC catalog once model discovery is on:
mcc-claude sets the proxy variables for just the current
shell session, without touching any file. Add --discover-models
to also enable the picker above (an extra request to the proxy on every
launch, so it is opt-in): mcc-claude --discover-models.
The legacy fcc-claude alias was retired in 7.0.0.
Editing the file by hand instead
Open ~/.claude/settings.json
(%USERPROFILE%\.claude\settings.json on Windows), creating
it if missing, and merge in these entries, keeping anything else:
{
"env": {
"ANTHROPIC_AUTH_TOKEN": "<your token>",
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8082"
}
}
The token must match this server's ANTHROPIC_AUTH_TOKEN,
generated for this machine on the first start and shown on the
Providers page under Runtime. It
authenticates your agent to the
proxy and is not a provider key. To also populate the native model
picker, add "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1"
to the same env block.
Point Claude Desktop here
The desktop app has a native gateway setting, so no file editing is required. Menu labels shift slightly between versions; this is the current route.
1. Enable developer mode: Help → Troubleshooting → Enable Developer Mode. The app restarts with a Developer menu. On older builds it is Settings → enable Developer mode.
2. Open Developer → Configure Third-Party Inference…
3. Fill in the Connection section, then Apply Changes:
| Field | Value |
|---|---|
| Connection | Gateway |
| Gateway base URL | http://127.0.0.1:8082 |
| Gateway API key | your ANTHROPIC_AUTH_TOKEN (Providers → Runtime) |
| Gateway auth scheme | bearer |
| Credential kind | Static API key |
| Model discovery | off |
Then fill in the model list with the five entries Configure writes
for you. Claude Desktop accepts a model name only if it looks like a
Claude model, so these are display names MCC resolves itself —
mcc/best and its siblings are rejected by the app’s
own filter. Tick Offer 1M-context variant and Default to
1M context on each, and never use a bare tier name such as
sonnet: with discovery off the app flags it.
| Model ID | Display label | Tier alias | Routes to |
|---|---|---|---|
claude-mythos-5.1 | Mythos 5.1 | mythos | MODEL_MYTHOS (mcc/cyber) |
claude-fable-5.1 | Fable 5.1 | fable | MODEL_FABLE (mcc/best) |
claude-opus-5 | Opus 5 | opus | MODEL_OPUS (mcc/good) |
claude-sonnet-5 | Sonnet 5 | sonnet | MODEL_SONNET (mcc/medium) |
claude-haiku-4.5 | Haiku 4.5 | haiku | MODEL_HAIKU (mcc/cheap) |
4. Use Test connection and Test model discovery before restarting — both call this server, so it has to be running.
5. Restart the app. With model discovery off and the
five models named above, the picker is filled from your own list at
launch rather than from a /v1/models call, so it cannot
come up empty because a request was slow. The initial warning dialog
is safe to ignore. With a gateway active the app runs
local sessions only.
Point a desktop app here
Some applications MCC cannot launch. They are already running, or they are not, and the only way in is the one configuration file each reads at startup. The Desktop apps group at the bottom of the Coding agents page is one card per app, and a card writes that file for you.
Do it in four steps.
- Read the card. Every one names the exact file it would edit, which keys MCC would own, which keys it would replace, and the base URL it would write. The badge beside the title is the app's current state: not installed, installed, not configured, configured by MCC, configured but drifted, or config file will not parse.
- Press What will this write? It renders a real unified diff of the real document and touches no disk. Nothing has been written at this point and nothing will be until you press the other button.
-
Press Configure. Your original file is
copied to a
.mcc-backupbeside it before the first edit, once. Every byte MCC does not own is parsed, carried through and written back unchanged — comments and key order included, which is why each app declares its own document format rather than being round-tripped through JSON. - Export the token if the card says to. MCC never sets an environment variable on your behalf, so a card whose app wants one prints the export and the next status poll reports whether it took.
A card that says “managed by your organisation” has no button on purpose. Some of these apps read a higher-precedence source than the file MCC writes — a Windows policy key, a macOS managed profile. Where one is present, MCC writing its file anyway would succeed, report success, and change nothing, so the card names the source and the keys it enforces instead.
Two ways to undo, and what separates them is what they refuse. Both remove the keys MCC created and both put back a value MCC replaced. Remove MCC's keys never refuses: no record, no hash check, always available, and the default. Restore the original values exactly guarantees the pre-MCC state instead — it requires the record MCC wrote at Configure time and declines when the file has been rewritten since, rather than reverting an edit you made on purpose. Before 6.56.0 the first mode deleted a replaced value and then dropped the record, so a Codex user's own default model was lost and the second mode answered “no record” afterwards. It no longer does either.
The drift badge is not an error. It means an owned key no longer holds what MCC wrote — you hand-edited the file, or the app rewrote it. Nothing is corrected behind your back. Press Configure again to take MCC's values back, or Undo to hand the file over entirely.
| App | What the card does |
|---|---|
| Codex desktop | Writes ~/.codex/config.toml. The desktop app and the CLI share one provider table, so this points both. Codex has no model picker once a custom provider is set, so Configure also writes model — the one value it overwrites by necessity, and the reason the restore mode exists. |
| Goose desktop | Owns a whole custom-providers file and names it from config.yaml. Goose reads API keys from its keyring, so no token is written into the file. |
| OpenCode desktop | Same provider map as the CLI, with {env:…} references expanded by OpenCode itself, so no token reaches the file. |
| VS Code (Copilot) | Owns exactly one element of the custom-endpoints array and reorders nothing. Reload the window for the import to run. |
| Crush | Charm's Crush — also the official client for Hyper/HyperCharm, which ships no client of its own. Its provider type for an OpenAI-compatible endpoint is openai-compat. |
| Roo Code | VS Code extension; its provider profile is written the same way. |
Antigravity (agy) | Routable since 6.55.0 through the Gemini surface: with modelProvider set to gemini, agy speaks the public Gemini API MCC already serves at /v1beta. Two limits: it must be given GEMINI_API_KEY and GOOGLE_GEMINI_BASE_URL as exports before it starts, and it rejects mcc/* names before sending anything, so models are named with agy's own ids. The Antigravity IDE lists custom endpoints as unsupported; this card is the CLI only. |
| Command Code | A status card. It has been configured by mcc-commandcode since 6.27.0 and this adds no new mechanism — it reports the state and how to open it. Command Code ships no desktop application. |
| Claude Desktop | A button since 6.56.0. Anthropic’s MDM documentation names the local configuration source — %LOCALAPPDATA%\Claude-3p\configLibrary\, and ~/Library/Application Support/Claude-3p/configLibrary/ on macOS — so MCC owns one document there, points the library’s appliedId at it, and leaves every configuration you authored alone. That library is the lowest-precedence source the app reads: a managed profile under HKLM/HKCU\SOFTWARE\Policies\Claude (macOS: Managed Preferences) replaces it wholesale, so MCC probes for one first and refuses to write rather than leave you a file the app ignores. Since 7.3.0 the document MCC owns is the full working shape: model discovery off, five claude-* models with their tier aliases and 1M-context flags, and this app’s own preference keys at the values a proven-working configuration uses — never your organisation banner. Relaunch the app to load it. See Point Claude Desktop here for the manual route. |
An app that cannot be pointed here says so, with a date. Kimi's and Qwen's desktop builds, LM Studio and Warp each carry a dated reason rather than silence, so “can I use this through MCC?” has an answer you can re-check rather than an absence you have to test yourself.
Without a browser. mcc-apps list,
mcc-apps status <app>,
mcc-apps configure <app> [--preview] and
mcc-apps undo <app> [--restore] are the same four
actions against the same routes the page uses, so a card and a command
cannot disagree about what would be written. The server has to be
running: resolving what to write means walking the model ladder, which
only the server can do.
Point another CLI here
mcc-codex
mcc-pi
mcc-opencode
mcc-opencode2
mcc-kilo
mcc-commandcode
mcc-kimi
mcc-qwen
mcc-crush
mcc-cline
mcc-goose
mcc-aider
mcc-droid
mcc-gemini
The Coding agents page lists every CLI this proxy can
launch: whether its binary is on your PATH,
every command and flag it answers to with a copy
button each, the protocol it will speak, and the catalogue this proxy
generates for it. Nothing here installs a coding agent — a
missing one is reported with that agent's own install command.
OpenCode, OpenCode 2 and Kilo read a config file
rather than command-line flags, and this proxy does not edit yours.
Each of them documents a variable naming an extra config file
— OPENCODE_CONFIG and KILO_CONFIG
— so the launcher writes a file of its own under
~/.mcc and hands over its path. Your proxy token is not in
that file either: it is written as OpenCode's own
{env:...} substitution and supplied to the launched
process only. Inside those CLIs the models appear under the provider
mcc, as mcc/<provider>/<model>.
This server owns those files; the launcher only reads
them. Every agent's document is written under
~/.mcc when this server starts and rewritten whenever
the model list or any model's resolved capabilities change, so
mcc-<agent> opens the file it needs and launches
— no HTTP, no wait. A launcher asks this server to build a
document only when the file is not there at all, and that one build
gets Coding agent catalogue build budget on
Limits & Resilience → Deadlines (20 s by
default). Each agent's card above names its file and when it was last
written. Before 6.36.1 only the launcher could create a document and
its fetch was given the 1.5 s health-check budget for a route that
takes 1.8–4.0 s, so the first launch failed, the file was never
created, and every launch after it failed identically.
Command Code is the one exception, and it says so on its
card. It reads exactly one document,
~/.commandcode/providers.json, and publishes no flag
or variable naming another — so this proxy merges a single
provider.mcc key into your file. Every other key is
written back unchanged, your document is copied to
providers.json.mcc-backup before the first edit, a
refresh that resolves the same numbers writes nothing, and
mcc-commandcode --disconnect takes the key back
again. The token still stays off disk: Command Code refuses a
literal key there and expands
$MCC_COMMANDCODE_API_KEY from the launched
process's environment instead.
Kimi Code reads a config file too, and takes a flag for
it. It is a Python tool —
uv tool install kimi-cli, not npm — and it
publishes --config-file, so this proxy writes
~/.mcc/kimi-code-config.toml and passes that path.
Your ~/.kimi/config.toml is never read, written or
backed up, and your sessions and skills are untouched. Two things
are different from the CLIs above and both are on its card: the
flag replaces the config rather than overlaying it, so an
mcc-kimi session takes Kimi's own defaults for
theme and hooks; and Kimi's
api_key accepts no environment reference of any kind,
so this is the one generated file that holds the proxy token
— mode 0600, under ~/.mcc, beside
the .env that already holds the same value.
Qwen Code and Crush each publish a variable naming a
whole config document, so neither file of yours is
touched. Qwen Code reads
QWEN_CODE_SYSTEM_SETTINGS_PATH and this proxy
writes ~/.mcc/qwen-code-settings.json: a
modelProviders.anthropic array carrying every
routable model with its real context window, and an
envKey naming MCC_QWEN_API_KEY
rather than the token itself. The auth type comes from
--auth-type anthropic on the command line,
which outranks anything saved in your settings, so nothing
under ~/.qwen is read for it or written to.
Crush reads CRUSH_GLOBAL_CONFIG, which names a
config directory, so this proxy owns
~/.mcc/crush/ and writes one
crush.json into it with
"api_key": "$MCC_CRUSH_API_KEY" — Crush's
own documented reference form. Your
~/.config/crush is never read for a provider,
written or backed up.
Both trades are on their cards. A Qwen session takes this
proxy's modelProviders list as the whole list,
because Qwen declares that key replace rather than
merge; every other setting you have still applies. A Crush
session takes Crush's own defaults for the LSP servers, MCP
servers and permissions you set globally, because
the variable moves the whole global layer — your
project-local crush.json still applies and your
sessions, logs and statistics are untouched, since the data
directory is not moved. Crush is also the one catalogue
where an unknown cannot be omitted: ten of its per-model
fields are required, so anything the ladder did not resolve
becomes Crush's own number and is listed under
_mcc_defaulted and on the card.
Cline, Goose and Aider reach this proxy over
POST /v1/chat/completions, which is why
they could not be served before. Each takes a different lever
and none of them is your own config file. Cline
publishes --config, which moves its whole
configuration directory, so this proxy owns
~/.mcc/cline/ and writes one
providers.json into it declaring the
openai-compatible provider — the one entry
that takes an arbitrary base URL and makes no discovery call.
-P openai-compatible is passed on every launch,
because a provider block that is written but not selected
leaves Cline falling back to its own hosted account.
Goose is configured entirely from the
environment: OPENAI_HOST plus
OPENAI_BASE_PATH=v1/chat/completions compose the
endpoint, GOOSE_PROVIDER and
GOOSE_MODEL pick the session, and
GOOSE_CONTEXT_LIMIT carries that model's real
context window. Nothing is written to disk at all —
Goose's only file mechanism would put this proxy's document
inside Goose's own config directory, beside your settings,
which is the one thing none of these launchers do.
Aider reads two documents and publishes a flag
for each: --model-metadata-file takes a LiteLLM
model_cost map with every model's context window,
output ceiling, per-token prices and vision support, and
--model-settings-file takes a list saying what each
model accepts — whether
--reasoning-effort will be honoured, whether
temperature may be sent. The proxy token is in
neither: OPENAI_BASE_URL and
OPENAI_API_KEY are set in the launched process
only.
Droid is the odd one out, and in your favour.
It was expected to need the chat-completions door too, but
Factory's customModels[].provider: "anthropic"
accepts an arbitrary baseUrl — so
mcc-droid talks to this proxy in its own native
Anthropic Messages protocol, with no translation in between.
Its --settings flag is a runtime overlay merged
for that process only, so this proxy owns
~/.mcc/droid-settings.json and
~/.factory is never edited. The key is written as
Droid's own ${MCC_DROID_API_KEY} reference and
supplied to the launched process, so it never lands on disk.
No Factory account is needed for a model this proxy routes.
Cline is the second generated file that holds the proxy token
literally, for a measured reason: its environment fallback for
a missing apiKey does not authenticate a headless
run. Like Kimi Code's, that file is mode 0600
under ~/.mcc, beside the .env that
already holds the same value. Cline also carries limits for
one model at a time — its schema has no
per-model array — so mcc-cline -m <model>
is what moves the context window and output ceiling onto the
model you actually asked for.
What the picker is told. Each model in that catalogue
carries its own context window, output ceiling, vision and tool support
and reasoning-effort vocabulary, as this proxy's metadata ladder resolved
them — not a fixed placeholder. A model with a 32k window is
advertised as 32k, and a model that only supports low and
high offers exactly those two rungs.
Where a CLI's own format demands a number nothing published, the proxy
uses that CLI's documented default and says so — on the
agent's card, in a one-line summary on the launcher's output
(MCC_CATALOGUE_VERBOSE=1 for the per-model list), and in a
_mcc_defaulted block inside the generated file. Kilo CLI is
the exception on that last one: its config validator rejects unknown
top-level keys, so its file carries no such block and its card says so.
These catalogues are pickers rather than protocol surfaces, so they list
fewer entries than /v1/models deliberately:
:batch refs are an asynchronous pricing tier of a model you
already have and are left out, and every model appears once under its
plain id. Hiding a model on the Models page removes it
from every agent's catalogue too.
Cline, Crush and Goose have to be given one model up front to open a
session at all. They start on the route named by MODEL, or
on the first entry that is not a free tier when that route is hidden
— not simply on whatever happens to be listed first.
A coding agent is not a provider. The CLIs here sit downstream of this proxy and send requests to it; the names on the Providers page are the upstream gateways it buys tokens from. Some names appear in both and mean different things.
Editor integrations work the same way — Claude Code and Codex in VS Code, or Claude Code through JetBrains ACP.
Housekeeping commands do not need the proxy.
Each launcher keeps a list of subcommands and flags that reach the
CLI untouched — no MCC configuration, no generated file, and no
running server. mcc-kimi login,
mcc-crush dirs, mcc-droid doctor and
mcc-<agent> --version all answer with the proxy
stopped, because logging in or printing a version has nothing to do
with which provider serves a request. Anything that actually runs the
agent is configured normally.
Connect any OpenAI-compatible client
Anything that speaks the OpenAI API can point at this proxy without a plugin or an adapter. Give it these three, and nothing else:
| Base URL | http://127.0.0.1:8082/v1 |
| API key |
your ANTHROPIC_AUTH_TOKEN, sent as
Authorization: Bearer <token>
|
| Model |
any id from GET /v1/models, for example
anthropic/claude-sonnet-4-5
|
Two OpenAI request shapes are served.
POST /v1/chat/completions is the one
almost every client sends — the OpenAI SDKs, the IDE plugins,
the agent frameworks — with streaming, tool calls, images and
reasoning_effort. POST
/v1/responses is the newer shape Codex speaks. Both
run through the same routing, fallback chain, reasoning gating and
request log as /v1/messages, so a request costs and
behaves the same whichever door it arrives through.
curl http://127.0.0.1:8082/v1/chat/completions \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "anthropic/claude-sonnet-4-5", "stream": true,
"messages": [{"role": "user", "content": "hello"}]}'
GET /v1/models answers in the OpenAI list shape, so a
client with a model dropdown fills it from this proxy. Thinking is
streamed as delta.reasoning_content, which is the field
the clients that display reasoning actually read. The
Requests page labels each of these rows
openai_chat in its Protocol row, so you can tell at a
glance which door a request came through.
Four coding agents reach this proxy through that door,
and each has a launcher of its own.
mcc-cline writes an
openai-compatible provider into a configuration
directory this proxy owns.
mcc-goose sets OPENAI_HOST and
OPENAI_BASE_PATH=v1/chat/completions and writes no
file at all.
mcc-aider sets OPENAI_BASE_URL to the
/v1 form above and hands Aider two generated model
documents.
mcc-droid is the exception: Factory's Droid takes
an arbitrary base URL on its anthropic provider, so
it uses POST /v1/messages instead. See
Coding agents for every command each one
answers to.
n above 1 is refused with a 400 rather than served
quietly as one choice. seed, logprobs and
the frequency and presence penalties have no equivalent upstream and
are dropped — the request still runs, and the drop is recorded
in the server log rather than being silent.
Connect a Gemini client
Gemini CLI and everything built on Google’s
google-genai SDKs speak one protocol and no other:
they cannot be pointed at an OpenAI-shaped endpoint at any
price. So this proxy serves Google’s own request shape as
a third door. Give a Gemini client these three:
| Base URL |
http://127.0.0.1:8082 — the root, with
no /v1 and no /v1beta: the
Google SDKs append
/v1beta/models/… themselves
|
| API key |
your ANTHROPIC_AUTH_TOKEN, sent as
x-goog-api-key (what the SDKs send) or
?key= (what Google’s own curl examples
use). A bearer Authorization header is
accepted too and still outranks both.
|
| Model |
any id from GET /v1beta/models, for example
anthropic/openrouter/qwen/qwen3-coder.
Slashes are legal in the path and survive the wire.
|
POST
/v1beta/models/{model}:generateContent answers
with one GenerateContentResponse;
:streamGenerateContent answers
with Google’s alt=sse stream — bare
data: frames and no [DONE] sentinel,
which is OpenAI’s convention and would break a Gemini
client. :countTokens is served too.
systemInstruction, generationConfig
(including thinkingConfig and
responseSchema), tools and
toolConfig are all translated, and inline images
and function-call round trips work. All of it runs through the
same routing, fallback chain, reasoning gating and request log
as /v1/messages.
curl "http://127.0.0.1:8082/v1beta/models/anthropic/claude-sonnet-4-5:generateContent" \
-H "x-goog-api-key: $ANTHROPIC_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"contents": [{"role": "user", "parts": [{"text": "hello"}]}]}'
The Requests page labels these rows
gemini in their Protocol row, and their Endpoint
row carries the model and the method — the path
is the model on this surface, so collapsing it to a
constant would hide which model a row was for.
One coding agent reaches this proxy through that
door. mcc-gemini sets
GOOGLE_GEMINI_BASE_URL and
GEMINI_API_KEY in the launched process and hands
Gemini CLI a settings document this proxy owns, through the
CLI’s own
GEMINI_CLI_SYSTEM_SETTINGS_PATH variable. Your
~/.gemini/settings.json is never written, and the
OAuth tokens beside it are never read. See Coding
agents for every command it answers to —
including why a headless run needs
--skip-trust.
candidateCount above 1 is refused with a 400
rather than served quietly as one candidate.
safetySettings and cachedContent have
no equivalent upstream and are dropped, as are Google-hosted
tools such as googleSearch — the request
still runs, and every drop is recorded in the server log rather
than being silent. Thought parts are returned only when the
request asked for them with
thinkingConfig.includeThoughts.
Search the web
Claude Code's web_search is normally executed and billed by
Anthropic. This proxy fulfils it locally, so no Anthropic search credits
are used and it works with any model provider.
WEB_SEARCH_PROVIDER=auto needs no
configuration — with no keys it falls back to keyless
DuckDuckGo. Add any provider key and auto prefers it. A
missing key on an explicitly chosen provider fails visibly rather than
degrading silently.
The setting most worth changing. By default most providers return a one or two sentence snippet. Several can return the extracted text of the page — the difference between the model guessing from a summary and actually reading the source.
EXA_CONTENTS=text
TAVILY_INCLUDE_RAW_CONTENT=markdown
FIRECRAWL_SCRAPE_FORMAT=markdown
WEBSEARCH_DIGEST_CONTENT_CHARS=4000
Jina, Parallel and Linkup return extracted text by default. It has its own
cap, separate from the snippet cap, so opting in is not trimmed back to
snippet length — set it to 0 for snippets only. It
costs more on most providers and adds input tokens to every search; each
option's drawer states its cost.
Domain filters from the client are forwarded and applied
server-side on Exa, Tavily, Firecrawl, Linkup, Perplexity
and Parallel, so you pay for relevant results instead of filtering
afterwards. Providers without native support search normally; each
recorded attempt shows supports_domain_filters so you can
tell which happened.
Two options that matter for coding work:
FIRECRAWL_CATEGORIES=github,research
TAVILY_CHUNKS_PER_SOURCE=3
Locale is worth setting if you are not in the US —
Firecrawl returns US results unless told otherwise
(FIRECRAWL_COUNTRY=DE). Safe search is available on Brave,
SearXNG, SerpAPI, SearchAPI and DDGS. All 66 advanced options live in the
Advanced options drawers, which are open when the page loads, and each
states what leaving it blank does.
Where the advanced settings went
Nowhere — they are all on the page. Since 7.29.1 nothing on this dashboard is hidden behind a toggle. A setting marked advanced simply sorts after the common ones in its card and carries that tag; every one of them renders the moment the page loads. Before 7.29.1 the mark meant the field was not drawn at all until you found a Show advanced button, and on the Providers page that button was never added, so a provider's proxy and base-URL overrides could not be reached from the dashboard at all. If you would rather see the short form, each card has a Collapse advanced control; it starts expanded and remembers your choice in this browser.
Images, and what they cost
A screenshot is the most expensive thing an agent routinely sends, and three separate mechanisms decide what actually leaves this machine. They are all on Model Config.
1. An image a tool returned arrives as an image
A browser screenshot, or a Read of a PNG, comes back
nested inside a tool result — and no OpenAI-format chat message
can carry a picture there. Before 6.49.0 that picture was sent as
base64 text, which every OpenAI-dialect provider tokenised at
roughly a token per byte: one 213 KB screenshot measured
324,000 prompt tokens, and the model never saw a
picture at any price. Now it is hoisted into a short user
message straight after the tool output, marked as tool output rather
than as something you typed, with a one-line note left behind pointing
at it.
TOOL_RESULT_IMAGE_DELIVERY chooses the rule.
auto (the default) attaches the picture unless the model
that answers is published as unable to read one, in which case a plain
sentence naming the tool takes its place; attach always
sends it; strip never does. There is deliberately no
option to go back to base64 text. The request detail marks what
happened with an image_delivery line, so a request that
lost a picture says so.
2. Oversized pictures are shrunk before they are sent
IMAGE_MAX_LONG_EDGE ships at 1568 —
Anthropic's own resize budget — and applies to the deep copy the
router already takes, so your client's request is never mutated and
every dialect converter is handed pre-shrunk data. On three of the four
billing families this is token-neutral because they resize server-side
anyway; on the fourth, OpenAI's patch-billed models, it is a real 41%
saving and the one case where the model genuinely sees less.
0 sends exactly what the client sent.
Two knobs beside it, both opt-in.
IMAGE_JPEG_QUALITY re-encodes a resized image as JPEG;
0 — the default — never changes the format,
because these are screenshots of text and code and that is the worst
case there is for JPEG ringing. An image with an alpha channel is
skipped whatever it says. IMAGE_DETAIL is OpenAI's
per-image fidelity field; auto emits nothing at all, and
it is meaningless outside the OpenAI dialects.
Was the estimate right? A provider row on the
Models page carries a
billed/est chip once there is enough traffic to compare:
the input tokens that host actually billed against what MCC's
per-family estimator predicted. A ratio near 1.0×
means the estimator has this family right; a large one means it does
not, and the number is the only thing that can audit that.
3. A blind model can be given the words instead
Plenty of fast, cheap models cannot read a screenshot. Set a Vision adapter and MCC diverts — but only when the tier's own model is known not to accept images. A model whose provider publishes nothing is left alone, because rerouting on silence moves traffic away from models that handle images perfectly well.
Vision Adapter Mode —
VISION_ADAPTER_MODE in the env file — picks what
“divert” means, and the default is unchanged from every
release before 6.51.0:
| Mode | What happens | Pick it when |
|---|---|---|
route (default) | the whole request is handed to the vision chain, and the vision model answers it | the picture is the question |
describe | each image is sent to the vision chain on its own — say what is there, verbatim where legible, do not interpret — and its answer replaces the image in the transcript. The model your route picked then answers, with the screenshot rendered as words | the picture is context: a fast text-only model doing the coding and a screenshot arriving mid-conversation |
What describe mode actually costs. One extra sub-request per image, not per request — and descriptions are cached on the image itself, so a screenshot Claude Code re-sends every turn is described once. Each describe call is an ordinary routed request: the vision chain's own fallbacks, pause list, health registry and retries apply, and it gets its own row in the request log against the request that carried the picture, priced separately so it cannot make the answering model look more expensive than it was. Clear the cache from Requests → Clear image descriptions when a better vision model arrives.
Nothing here can cost you an answer. A describe call
that fails falls back to route; a route with nowhere to
divert to falls back to the placeholder sentence. And both modes fire
only when the answering model is published as unable to read
images — a model that can see is simply sent the picture, which
is cheaper than two calls and always better.
See what a request actually did
Two local SQLite stores under ~/.mcc/logs/, written by a
background thread so they never block a request.
Not every row has a provider. Requests MCC answered
itself show answered locally · <rule> in the
provider column rather than crediting a provider that was never called.
They are real rows with real token counts, and they filter like any
other value — see
the Token Optimizer page.
Local answers is set to Hide. On a busy install those rows outnumber the upstream ones, so the table, the cards, the charts and the breakdowns default to real upstream traffic. Switch the Local answers filter to Show for the old totals, or Only to look at nothing else; the choice is remembered across refreshes. Rows whose provider is genuinely unknown stay visible under Hide, and the All time rollup ignores this filter as it ignores every other one. Every filter applies itself — selects on change, typing after a short pause — so Apply is now only there when you want it.
Input tokens are reported in parts, because they bill differently:
| Column | Meaning |
|---|---|
| Input (uncached) | prompt tokens the provider actually processed |
| Cached input | prompt tokens served from the provider's cache |
| Cache hit rate | cached divided by total input |
| Cache writes | tokens written into the cache |
p50 and p95 latency are approximate. They are read from a 64-bucket histogram rather than by sorting every stored duration, which is what makes an all-time view load in a fraction of a second instead of half a minute. On a real log the error measured at or under 2.3%; on a filter matching only a handful of requests it can be larger, because there is nothing inside the bucket to interpolate against. The average, and every count, stay exact.
A hit rate of — means that provider never reported caching, which is different from a measured 0.0%. OpenRouter, OpenAI (prefixes of 1,024+ tokens), DeepSeek and — since 6.68.2 — ChatGPT OAuth report it. NVIDIA NIM's hosted endpoint does not do real prefix caching, so a near-zero rate there is accurate rather than a fault.
ChatGPT OAuth rows logged before 6.68.2 read —, and stay that way. The endpoint was reporting the figure all along and MCC was not reading it, so those requests were never measured; nothing is backfilled, because a guess in that column would be worse than a dash. Their input counts were also cache-inclusive and so ran high — from 6.68.2 the cached part is reported separately, the way every other provider's already was.
Every row has a View dialog with the full exchange, the resolved configuration and timing — usually the fastest way to see what actually happened. Web search analytics separate logical searches from provider attempts, so one search that fell back shows as one search and several attempts.
Export opens the export window for Model requests, Route attempts or Web Search — JSON, CSV, XLSX or TXT, over a period of your choice, with selectable fields and optional grouping by provider, period, model or key. It streams the entire matching row set, not just the visible page.
Export attempts, beside it, opens the same window on the Route attempts scope: one row per model the chain tried, rather than one per request. A request that fell back twice is one row in the Model requests export — one provider, one model, one verdict — and the two models that spent the time are not in it at all. Each attempt row carries its own provider, model, outcome, key, TTFT, first-reasoning time, duration and what ended it, beside the parent request's time, harness, endpoint, models and status.
The filters are the request filters, and empty means never measured. Period, provider, model, key, status, harness, search and Local answers all select requests, so every attempt of a selected request is in the file — including the failures, which is the point. The latency columns did not exist before 7.4.0 and are not backfilled, attempt tokens are filled only on the winning attempt, and a skipped attempt has no latency at all — only a reason. Those cells are blank rather than zero. The scope is detail-only: there is no Group by, because the per-model p50 TTFT cards already answer that question properly.
Finding a request again
Search text looks at everything a request contains — the prompt, the reply, the model's reasoning, and its tool calls including commands and paths. That matters more than it sounds: on a typical machine reasoning appears in about half of all requests and tool calls in three quarters, so searching only the prompt and reply missed most of the transcript.
Every word has to appear, in any order and anywhere in the request, so
proxy 8082 finds one that says "restart the proxy" in the
prompt and "port 8082 is busy" in the reasoning. One word behaves as it
always did. Matching ignores case and matches inside words.
What the request body pane says
The wire is the headline. Each attempt in the request detail opens
with every parameter that actually left — every
top-level key of the body plus each provider-specific
extra_body.<name> — above the message
structure, and that list is never truncated. Only
messages and tools degrade, to counts and
names, once the stored body passes
REQUEST_LOG_WIRE_BODY_MAX_CHARS on the request-log card;
what is kept always parses as JSON.
Reasoning reads as one of three honest states: reasoning sent with the value carried, no reasoning instruction sent (model default applies), or not measured where that provider has no instrumented commit boundary — the pane is never hidden, because a hidden pane reads as "no body was sent". A badge marks the one real contradiction: reasoning was resolved to something the wire then clamped or substituted. Sending nothing at all is not a contradiction and does not badge, and rows written before this was recorded badge nothing either way.
A dash is not a zero anywhere on this page. A dash
means nobody measured it; 0 means it was measured and was
zero — a completed stream that returned no reasoning stores a
real 0. The same rule covers keys: each attempt names the credential
it used, and (no key available) is what you see when
the whole pool was benched, rather than a key label invented after the
fact. The Models page adds a measured chip per model — how often
reasoning was requested over the last 7 days, and how often it came
back — two independent facts.
The retry ladder. An attempt is not one request upstream: it is up to five tries per credential, across every credential the pool hands it, with backoff in between. Under each failed attempt the chain now lists every one of those tries — its status, its key, how long it took, how long MCC then slept, and a redacted excerpt of the upstream’s own reply — plus one line per credential saying whether the pool charged it and why. The root-cause sentence above them is stored with the row, so the modal and every export say the same thing. Analytics → Upstream statuses counts the same tries across the window, which is why it can disagree with Top errors: that groups by the one message that survived a whole ladder. Attempts recorded before 6.12.0 show no ladder — not measured, not “there were no retries”.
How long each model took to say anything
A request that fell back waited for every model ahead of the one
that answered, and until 7.5.0 the whole of that wait was printed
against the model that rescued it. The Requests row now shows the
winner’s own first-token time, with a
+N s suffix for the time the chain lost —
300 ms +4.4 s reads “the model that answered took
300 ms; you waited 4.7 s”. Both numbers are in the
cell’s tooltip. The gap is not small: across a
290,074-request log the mean is 8.6 s when the
first model answered and 25.2 s when a fallback
did.
View breaks the same request into its attempts: each row of Route attempts carries its own TTFT, first reasoning time, generating time, tokens out, tok/s and what ended it. Output rate is measured from the winner’s first token, so a fallback request no longer reports a rate that is too high. A model that was never asked keeps its row with dashes and its bench reason underneath.
Analytics → Model latency groups every route
attempt by model and outcome: a model that answers in
300 ms and fails after 90 s is one model and two facts, and one
average would hide both. Each row leads with its median
(p50), keeps the p95 in the tooltip, and states how
many of its attempts were actually measured — a panel may
never report a percentile over twelve attempts as if it described
4,431. The panel answers over the window only, not the filters
above it: a failed attempt has no request row to filter on. The
Models page carries the same median per model as a chip, and no
chip at all for a model nothing has measured.
Nothing before 7.4.0 is measured, and none of it can be. The log never recorded when each attempt’s own stream began, so there is nothing to recover: those rows show dashes for ever, and the panel says so in words rather than leaving a blank column. New requests fill it in as they arrive.
When the numbers stop moving
REQUEST_LOG_MAX_ROWS caps stored rows, so once the log is
full one row is deleted for each one that arrives. Everything above
All time is therefore a rolling window, and at the cap
it stops rising however much traffic you run. Analytics tells you when
that happens rather than leaving a frozen counter looking broken.
All time is a small permanent tally kept per day, provider and model. It keeps counting after stored rows roll over, so per-model requests and token usage stay true. It ignores the filters and the time range deliberately. Upgrading seeds it from the history still retained — anything already pruned is gone — and Clear log erases it along with everything else.
Bodies dominate the disk cost — 99% of it, at roughly 30 KB of
text a request against 332 bytes of metadata. They are stored
compressed against a dictionary trained on your own traffic, which
works well here because consecutive requests repeat almost the same
system prompt and history: about 6x more retention for the
same disk, measured on real traffic. Set
REQUEST_LOG_COMPRESS_BODIES=false to store text inline
instead, or REQUEST_LOG_CAPTURE_BODIES=false to drop it
entirely and give up the drill-down.
No traffic, or no server?
A flat stretch in Requests over time means one of two very different things and the chart cannot tell them apart. My Claude Code records when a server was actually running, so a line under the chart says whether one covered the range you picked, or how much of it had none. Choose a time range to see it; over all time there is no bounded window to measure. Restarts are ignored, and periods before you upgraded report nothing rather than claiming downtime nobody measured.
Search content routinely includes private queries and page text.
WEBSEARCH_LOG_CAPTURE_CONTENT=false withholds the payloads
and the query text, keeping only lengths and hashes. API keys are
never written to either store — only masked labels.
What a request cost
Every request is priced once, at the moment it is written to the log, and the answer is stored beside it with the name of the source that produced it. It is never recomputed when you look at it: a price that changes next month must not silently rewrite last month's bill.
Pricing walks the same ladder every other model fact walks — the host's own answer first, then this provider's catalogue, then a reference catalogue, then a vote across other providers. The rung that answered is what tells you how much to trust the number, so it is carried back and shown:
| Rung | Shown as | Where it comes from |
|---|---|---|
| 1 | (no badge) | the host's own reported cost, when it returns one |
| 2 | est. models.dev | the models.dev price for this provider's own bucket |
| 3 | est. LiteLLM | LiteLLM's published price map, if you turn that source on |
| 4 | est. cross-provider | a vote across same-named rows in other providers' catalogues. The model is the same; the seller is not |
| — | — not priced | nothing published a rate |
“Not priced” is a dash, never
$0.00. Those are different facts.
$0.00 is a claim the request was free, and only a source
that actually publishes a zero — a :free model's own
catalogue entry — may make it. So every total carries an
“N of M priced” denominator beside it, and
a partial total can never be misread as a complete one.
Reported and estimated are never added together. One merged figure would launder a guess into a fact and afterwards nobody could tell which half was which. The panel shows two numbers, per provider, per model, per harness and per day.
Three things surprise people, so they are worth stating:
- One request is priced from one rung, whole. A source that names an input rate but no cache-read rate has not priced a request that read from cache — that request falls to the next rung entire rather than being stitched together from two sources into a number nobody published.
- Nothing is backfilled. Requests logged before 6.54.0 stay unpriced forever. Pricing them at today's rates would produce a confident number that was never anybody's bill.
- A describe hop is its own row. It is a different model on a different key; folding it into the answering model's figure would hide what describe mode actually cost.
The controls are on this page under Cost estimation:
the master switch (COST_ESTIMATION_ENABLED, on),
which sources may answer (COST_ESTIMATION_MODE —
auto, reported_only, or
computed_only, the last being how you audit a provider's
own billing against a published price), and the LiteLLM source
(COST_SOURCE_LITELLM_ENABLED, off — it costs one more
cached 2.3 MB file and one conditional fetch a day, and buys the
misses). The Models page shows the
resolved input, output, cache and reasoning rates per model in USD per
million tokens, each with the rung it came from — so you can see
what a request would be priced from before you send one.
Which agent sent this?
Every request in the log is attributed to the coding agent that made it, so Analytics can answer "what is Crush actually costing me" and not just "what did this model cost". The column is in the request table, it is a filter, it is a breakdown, and it is in the exports.
There are two ways a row gets its answer, and the difference matters when you are reading a number:
-
The agent said so. MCC's launchers configure the CLI
to send an
x-mcc-harnessheader naming itself. This is exact. - We recognised it. Five agents cannot send that header — Kimi Code, Aider, Droid, Goose and Antigravity — because nothing in their configuration format describes a request header. They are attributed by user-agent instead, which costs certainty and nothing else. Each row records which of the two it was.
Qwen Code and Pi are filed as Claude Code until they send the
header. Both emit Claude Code's user-agent verbatim —
claude-cli/<their own version> (external, cli) —
and there is no reliable way to tell the three apart from the
user-agent alone. A version-range rule would misfile the day Claude Code
ships a 0.x or Qwen Code a 2.x, so fingerprinting deliberately answers
claude for all three and the explicit header is what separates
them going forward. If your Claude Code numbers look too big and you run
Qwen Code or Pi, this is why.
An agent missing from Analytics never reached MCC. The log records what this server answered. A request that went straight to a vendor cannot appear here however long you look, so a missing agent is a configuration problem in that agent, not a reporting gap. Launch it from the Coding agents page and it will be configured correctly by construction.
Desktop & RTK
The desktop app starts the server itself, every ten seconds, on every path. Since 6.61.0 one controller on one ten-second tick decides everything the window does about the server: it checks, and if the server is not running it starts one — on that tick, with no attempt cap and no page that waits for you to press anything. Every page carries the same two numbers (last checked 3 s ago, next start attempt in 7 s), Retry now only brings the next check forward, and reloading the window shows you the page you were on rather than resetting it. It replaced seven separate waiting loops, six of which could reach a page with nothing running behind it.
A port conflict is now decided by the process, not by a bind
test. My Claude Code's own server binds its port about twenty
seconds before it answers anything, and a bind test cannot tell that
from a stranger — which is how the window came to tell one user
that My Claude Code was not My Claude Code. The holder is identified
by its pid, image and command line: ours and wedged gets a Take
port button, genuinely somebody else's is named and left alone,
and an unidentified holder is given
DESKTOP_FOREIGN_GRACE_SECONDS (45 s) before it is called
a conflict at all.
On Windows the desktop app owns the tray icon. There used to be two — the app's and the Python tray's — with different menus and two different answers to “restart the server”. The app's is the one that stays: it is the process that survives an update. Closing the window hides it to that icon and leaves the server running, Open dashboard brings it back, and Quit is the only thing that ends the app. The Python tray is still the fallback wherever no desktop app is installed, and on macOS.
The optional mcc-desktop tray app changes how the
server is owned, and its Server mode is set
here on the Deployment card (or from the tray menu): spawn
owns mcc-server as a child, attach connects
to an existing server and never spawns one, and off leaves
the server untouched. Start at Login registers a
different target per platform — the tray on Windows (HKCU Run key) and
macOS (LaunchAgent), and headless mcc-server under
systemd --user or an autostart .desktop entry
on WSL/Linux. The tray menu also covers Open Admin, Check Server
Status, Restart Server, Tray Enabled, and Quit; preferences live in
~/.mcc/desktop.json.
Since 6.44.0 the first thing Auto tries is the
My Claude Code desktop app — a ~1.5 MB
window this project builds and attaches to its own GitHub releases.
uv tool install cannot deliver a compiled binary, so the
first mcc-desktop launch after upgrading downloads it,
checks its SHA-256 against both the checksum file published on the
release and a digest pinned inside this build, and installs it
at ~/.local/bin/MyClaudeCode with a receipt beside it so it
never downloads twice. If any of that fails — no network, a proxy,
an architecture with no build — the launch is
not blocked: it warns once and falls back to app-mode.
Opt out with DESKTOP_SHELL=off or by picking
App-mode window here; MCC_DESKTOP_SHELL_DIR moves
the install somewhere other than ~/.local/bin. It is
unsigned, so Windows SmartScreen may warn on first run.
Since 6.60.0 the app keeps itself on that pin, however you
launch it. Until then the pin was checked in exactly one
place — the moment the tray created a window — so anyone
launching MyClaudeCode.exe from the Start Menu, the
taskbar or the Programs-folder install ran whatever build they first
received, forever. One person ran the v6.43.0 window for fifteen
releases while the wheel updated itself every time. The app now
compares the release stamped into it
(MyClaudeCode --version) with the
shell_release_tag the wheel reports and, when they
disagree, runs mcc-desktop --ensure-shell in the
background: one command that downloads the pinned build, checks both
digests, and puts it where it belongs — replacing the file
outright when nothing has the app open, and otherwise leaving it
beside the running executable as
MyClaudeCode.exe.new. It never writes over a window you
are running, and the operating system is what decides whether you
are: Windows refuses to replace a running image, and on Linux and
macOS a running process keeps the file it started from. The tray
then offers
Desktop app update ready — restart the app to use vX
with a Restart now; ignore it and the swap happens the next
time you start the app anyway. The previous build is kept for one run
as MyClaudeCode.old-<stamp>.exe.
The window can no longer save itself off-screen.
Until 6.60.0 the tray's Quit saved the window's geometry
after a close-to-tray had hidden it, and what Windows reports
for a hidden, minimized window is 0×0 at
-32000, -32000. That was written to disk and restored
verbatim on the next launch, so the app came back as a tray icon with
no window on any screen. Geometry is now never saved while the window
is hidden or minimized, and a remembered rectangle that is not at
least partly inside a connected monitor's work area — or is
below the window's own minimum size — is not restored at all:
the window opens at the configured
DESKTOP_WINDOW_WIDTH×DESKTOP_WINDOW_HEIGHT
(1400×900 by default), centred. Changing that size here now
applies on the next launch even after you have resized the window by
hand; resizing by hand still sticks otherwise. If you are ever left
with a tray and no window, the tray's Reset window position
— or MyClaudeCode --reset-window — forgets
the remembered geometry and opens centred.
You do not have to start the server first.
mcc-desktop in spawn mode probes the port:
a healthy server means it simply attaches to the one already running,
a free port means it starts one as its own child, and a
stranger on the port is an error rather than a silent
attach. It also no longer leaves a browser tab behind — the
server it spawns is told not to open one, so one launch produces one
window rather than a window and a tab. Launching it a second time
raises the window you already have. Closing the window does not stop
the server; the tray's Quit does.
A slow start is not a failed one, and giving up is not
final. The window gives the server it starts
DESKTOP_SERVER_START_TIMEOUT (15 s) to answer,
and since 6.58.1 it does that three times —
DESKTOP_SERVER_START_RETRIES (2) more attempts,
45 s in all — before it says anything went wrong, with
the countdown naming the attempt (attempt 2 of 3). A real
configuration takes 22–25 s to bind, because everything
loaded at startup happens before the port opens, and a
single 15 s budget gave up about seven seconds early. A retry
never starts a second server: a starting server has not bound its
port, so every check says free, and what the window
watches instead is whether the child it started is still running.
The page you get if all three attempts run out keeps checking on
the health poll, so a server that binds late loads the dashboard
on its own — Retry is an accelerator now, not the
only way out.
First launch, when MCC is not installed at all.
The window runs the project's installer and shows its output, and
since 6.58.1 it does that at most three times. An installer that
succeeds without putting mcc-desktop on the running
window's PATH — the ordinary Windows first
launch, since a PATH change reaches only new
processes — used to mean reinstalling every few minutes
forever under a spinner that said Checking the server.
It now says what it is doing, quotes the installer's last line,
tells you that quitting and reopening the window is the fix, and
keeps watching for mcc-desktop to appear. The
installer itself is stopped after 15 minutes rather than
holding the window for the session.
Getting the app without the command line. Every
release carries an installer for all three systems:
MyClaudeCode-Setup-windows-x86_64.exe (per-user, no
administrator rights, SmartScreen's More info → Run
anyway on first run),
MyClaudeCode-linux-x86_64.deb for Debian/Ubuntu and
MyClaudeCode-linux-x86_64.tar.gz with an
install-desktop.sh (and
install-desktop.sh --uninstall) for Fedora, Arch and
anywhere without root, and
MyClaudeCode-macos-universal.dmg, one file for Apple
silicon and Intel. Each digest is in
SHA256SUMS-desktop-shell.txt on the same release page.
The .dmg is unsigned, so Gatekeeper
blocks it: drag the app to Applications, then run
xattr -d com.apple.quarantine "/Applications/My Claude
Code.app" once and open it. Letting
mcc-desktop fetch the binary instead avoids that
entirely, because a file Python downloaded is never quarantined.
A winget manifest exists in the repository but has
not been submitted, so
winget install will not find this yet.
Two uninstallers, on purpose.
“My Claude Code (desktop app)” in Apps & Features (or
install-desktop.sh --uninstall on Linux) removes the
window and nothing else — your keys, your history and
mcc-server are untouched. Removing those is
uninstall.ps1 / uninstall.sh, which is a
separate, deliberate step. Set
MCC_DESKTOP_SKIP_AUTOSTART=1 to stop a launch
reconciling the machine-wide start-at-login registration —
useful when you run a second config directory and do not want it
fighting the first over one registry value.
The Window control on the same Deployment card picks
how mcc-desktop shows the dashboard: Auto
(recommended), App-mode window, Embedded webview, or
Browser tab. Auto walks
desktop app → app-mode → embedded webview → browser
tab, and a line under the select shows what it currently resolves
to, e.g. auto → app-mode (Microsoft Edge). App-mode
— a real Chromium browser process with no tabs or URL bar —
is the fallback, and a good one: window.open (both OAuth
logins), <a download> (the analytics export), and
navigator.clipboard (every copy button) all
break inside an embedded webview, and app-mode is a
real browser process so all three keep working. The CLI equivalent is
mcc-desktop --window auto|app-mode|pywebview|browser; an
explicit choice there falls back through the browser providers only,
never into the desktop app.
mcc-desktop --status prints those preferences as five
key=value lines. mcc-desktop --print-status
prints the same answers, plus the admin and health URLs, whether the
port is held by a healthy server, nothing, or a stranger, and the
reconnect budget, as one JSON document for scripts and
for a window that renders this dashboard. Both are read-only: neither
starts a server or changes a setting.
Embedded webview (pywebview) ships switched off and is not installed as a dependency — it cannot guarantee downloads and external links behave, and on macOS its run loop conflicts with the tray's. Install it yourself to opt in; expect OAuth login, the export, and copy buttons to potentially misbehave.
A --desktop flag on the installer (-Desktop
on PowerShell) adds a Start Menu shortcut on Windows, a
.desktop entry on Linux, or a minimal .app
bundle on macOS. It's opt-in and never fails the install if the
shortcut can't be created.
Nine DESKTOP_* settings under
Limits → Desktop tune polling intervals,
timeouts, initial window size, and an optional
DESKTOP_BROWSER_PATH override.
They apply on the next mcc-desktop launch, not to
a tray already running — it is a separate process that
reads them once at start.
WSL and headless Linux still have no tray. Run
mcc-server there instead and reach the dashboard from a
Windows browser at the URL it prints on startup. A Linux
desktop session now does, which is new: the old refusal was
about the tray rather than the window, and the fetched app carries its
own. It needs webkit2gtk-4.1 — Ubuntu 22.04+, Debian 12+,
Fedora 40+ — and still refuses correctly when there is no
DISPLAY or WAYLAND_DISPLAY, naming the
reason.
The Token optimizer card on the
Providers tab manages the RTK binary, which filters
noisy terminal output before it reaches the model. Toggle it per agent
(Claude Code, Codex, Pi); the state lives in
~/.mcc/rtk.json and applies immediately. The same controls
are available from the tray's Token optimizer submenu or the CLI:
Two different things share that name. This card is the RTK on/off switch, and it lives here. The Token Optimizer page in the sidebar is where you read what any of it saved — including RTK’s own savings figure, which now appears there as a card in the Ledger, taken from the binary’s own accounting rather than estimated. MCC pins a specific RTK version and installs that one the first time you enable an agent.
mcc-rtk status # installed binary + enabled agents
mcc-rtk enable claude,pi # install the hook for these agents
mcc-rtk disable codex # remove the hook for one agent
mcc-rtk uninstall # disable every agent and remove the binary
mcc-rtk apply # re-apply the stored state
The Token Optimizer page
This page answers one question: what did any of this actually save? Measurement comes first, and each control sits beside the number it moves — so nothing here is a switch you flip and hope.
Requests MCC answers itself
Some of what a coding agent sends is not work for a model at all. Claude Code asks a model to invent a name for your session, and it asks for the suggested next message it offers you. MCC recognises both and answers them locally: no provider call, no token spend, no latency. Two rules ship, both on by default, both toggled here — Title generation skip and Suggestion mode skip.
These used to be invisible. Every locally answered request is now
recorded with the rule that answered it and what it saved, and it credits
no provider, because none was involved. So in
Analytics the provider column reads
answered locally · title generation skip instead of
attributing free traffic to whichever provider you happen to have
configured. The token counts on those rows are measured now too, rather
than the fixed placeholder they used to report.
Filtering for them. Pick the
answered locally · … entry from the provider
filter’s list and it works. The value behind that label is
local:title_generation_skip — the underscored rule
name — so if you are typing rather than picking, type that.
Candidates: finding rules that do not exist yet
Scan the log reads your own request history, groups it into recurring prompt families, and shows which of them no rule covers. It is the honest way to decide whether another rule would be worth having.
- It runs only when you press it. Nothing is scheduled and nothing is cached; the log is not read until you ask.
- It is bounded. A scan reads 2,000 rows by default and you may raise that to 50,000. Above that ceiling the request is refused with a 422, rather than quietly scanning a sample and presenting it as if it were the whole log.
Tool-result trimming — off, and measured
Trimming shortens large Read, Grep and
Glob results before they reach the model. It is the only
feature in MCC that changes what the model is allowed to see, it is off
by default, and it is not a recommended optimization.
The measurements are the reason.
| Never trim the newest | Fresh input tokens | Cache hit rate |
|---|---|---|
| trimming off | 470,648 | 91.8% |
| 2 — the shipped default | 521,860 | 69.1% |
| 0 | 72,897 | 91.9% |
At the shipped default, trimming costs you tokens. Protecting the two newest results changes the prompt prefix on every turn, which throws prefix caching away: 10.9% more fresh input tokens than leaving trimming off entirely. Break-even is a baseline cache hit rate of about 90.9% — below it trimming wins, above it trimming loses, and a healthy session sits above it.
Switching it on part-way through a conversation costs one near-total
cache miss on that turn (a 3.8% hit). The full measurement table
— including why never trim the newest = 0 is not the
default despite looking best above — is in
.env.example and in the docstring of
core/anthropic/tool_result_trimming.py.
If you want it anyway, there is a master switch and then three states per rule:
- Off — results are left untouched.
- Observe — measures what would be removed and records it; the wire is unchanged and the model sees exactly what it saw before. Run a rule here against your own traffic first, and read the result before committing to it.
- On — performs the elision and marks it inline.
These controls used to live on Limits. They are on this page now, next to the figures that tell you whether they helped.
What the log keeps
The Request log storage card at the bottom of
this page decides what these tables can ever show.
Requests to keep is the retention cap: the
newest N requests are kept and older rows are deleted as new
ones arrive. All-time counters keep counting either way, so
raising or lowering it never changes your lifetime totals.
Bodies are about 99% of the stored bytes, so
Store prompts and replies is the setting that
decides whether content search works at all, and
Compress stored text applies to new rows —
run mcc-compact-log to convert existing history.
Limits & resilience
Every value on the Limits & Resilience tab is a ceiling on one request, or what happens when a model will not honour one. It is six cards now behind a section rail that follows you down the page — Budgets, Deadlines, Chain benching, Retries & throughput, Credential health and Diagnostics — each saying in one line what it decides. Each field states what the value costs and the range it accepts, beside the box rather than at the end of the paragraph.
Two cards left this page. Request-log storage is on Analytics, beside the numbers it bounds; desktop tray and window timing is on Providers, beside the desktop panel itself. The token-optimizer controls moved earlier, to the Token Optimizer page.
How long a stop may take
Graceful shutdown budget
(SERVER_GRACEFUL_SHUTDOWN_SECONDS, on
Deadlines) is the one field on this page that does not
bound a request — it bounds a stop, end to end, and
since 6.41.0 it is the only number that does. Ctrl+C, the reload that
follows Apply, the tray's Restart Server, and the
restart that finishes an update are all the same operation underneath
and all obey it. It ships at 20 seconds. At the
instant a stop begins the server stops taking new work: a request
arriving during the drain gets 503 with
Connection: close and a Retry-After, so a
busy client can no longer keep a closing server alive. Requests already
in flight get the whole budget; at the bound whatever is still open is
closed, an in-flight stream simply ends with no special frame, and your
agent's own retry finds the restarted server a moment later. The
process exits a few seconds past the bound whatever is still running.
The floor is 1 s and the ceiling 600 s.
If you set 300 here on an older install, lower it. Before 6.41.0 this bounded only one wait inside the stop and everything after it was unbounded, so a large value cost nothing in practice. Now it is the time you spend watching a tray icon after pressing Restart — and it is the number the tray and the Windows update helper both wait before they terminate the server themselves.
It slows every update, not just every stop. An update
waits this budget out twice over: the installer does not begin
until the old server has finished draining, and the reconnect budget the
desktop window counts down is this number plus the install allowance.
Measured on a real machine, 300 here added five silent
minutes to a fourteen-minute update and turned the window’s
“reconnecting for up to 17 minutes” into 22. If a restart
feels long, this is the first field to look at.
What one answer may cost
Budgets bounds the tokens one reply may spend.
MAX_OUTPUT_TOKENS_CEILING ships at
131,072 and is the cap on the room a
reasoning request is given;
blank falls back to that default and 0 means no
ceiling at all, which is a real choice rather than the absence
of one.
New in 6.47.0, and it changes what your install
sends. MAX_OUTPUT_TOKENS_FLOOR ships at
8,192 and is the smallest allowance any
request goes out with — the only bound on this card that
raises a number rather than lowering one. A client that
hardcodes max_tokens: 512 was getting a truncated
answer out of a model that could have finished it. It never
raises past the routed model's own published limit, it is
applied before the context bound so it can never re-inflate a
budget the context cannot hold, and it leaves an explicit
max_tokens: 0 alone. Set it to 0 to
raise nothing at all. It is not the same setting as
Smallest bounded budget
(MAX_OUTPUT_TOKENS_CONTEXT_FLOOR), which decides
when the prompt reserve gives up.
Two more numbers on this card became settings in 6.47.0.
ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS is the
last-resort max_tokens for a request that reached
a provider with none of its own and no published limit to take
one from; REASONING_EFFORT_BUDGET_RATIOS is the
six-value ladder deciding how much of the allowance each
effort spends on thinking. One ceiling is deliberately not
here: NVIDIA NIM's own max_tokens, in the nested
NIM settings, lowers whatever this card resolved on NIM routes
only. It ships unset, and the nested group has no flat
environment variable of its own to put on this card.
When to stop waiting
Every deadline on this card ships at 0, meaning no limit — so out of the box MCC never ends a silent or stalled upstream on its own. The fallback chain moves only on an error the provider actually returns. A model that thinks for forty minutes is left to think; a stream that goes quiet and never resumes stays open until the transport read timeout below ends it, or the client disconnects. That is deliberate: a deadline that kills real work is a worse failure than a stall you can see in Analytics, and this is your server to tune. Set the ones you want and the readout under the grid shows what each model on your routes then gets. An install that already sets any of these keys keeps its own values.
A fallback chain can only rescue a request while nothing has reached the client. A model that accepts a request and then goes silent holds it until the transport read timeout, minutes later, so the chain gets its turn long after the client has given up. These are what bound it, and they measure different things:
-
First-token deadline
(
FALLBACK_FIRST_TOKEN_TIMEOUT) — seconds a model may stay silent before any output before the next model on the chain takes over. Nothing has streamed yet, so the handover is invisible. This is the only deadline here that produces a failover rather than an ending. -
Stall deadline
(
FALLBACK_STALL_TIMEOUT) — seconds a stream that has started producing may then say nothing, measured from the last chunk that moved the answer forward. No model can replace a stream the reader is already looking at, so this ends the request rather than moving the chain. -
Total request budget
(
FALLBACK_TOTAL_TIMEOUT) — the whole request, across every attempt, retry and recovery. Once output has started no fallback can replace it, but it can still stop. -
Thinking before the chain moves
(
FALLBACK_REASONING_ANSWER_TIMEOUT) — seconds a model may think without starting an answer, while reasoning is being held back so the attempt is still abandonable. -
Silent-attempt floor
(
FALLBACK_ATTEMPT_SHARE_FLOOR) — the smallest slice of the total budget the chain-side division may hand one model. It bounds each model's first-token allowance, never a retry of the same model. Ships at 3600 — an hour, wider than any interactive request — and is moot either way while the total budget is 0, because there is no budget to divide. - Retries before the chain — attempts against one model on the same key after an upstream 5xx or a dropped connection, before the next model is tried. Each retry waits longer than the last. A 429 uses none of them: it routes around the model instead, unless you turn Route around a rate-limited model off on the Credential health card.
Once you set a total budget, the first-token deadline becomes a ceiling rather than an allowance: the budget is divided by the number of models still to try, so on a ten-model chain with a 600 s budget each model gets 60 s, not 120 — and the box reading 120 is inert for that route. The silent-attempt floor is what stops that share falling below the number you typed. The Deadlines card computes this for your own routes, one row each, and says which total budget would honour the number you asked for — a total budget of 1200s would give every model the 120s you asked for. At the shipped zeros every row reads no limit.
Every error one of these limits raises names its own
knob. A request MCC ended reaches your client as, for
example, Provider 'open_router/qwen/qwen3-max' produced no
output within 300s. (FALLBACK_FIRST_TOKEN_TIMEOUT -- change it
on the dashboard under Limits & Resilience ->
Deadlines). The hint names the limit that
actually ended the attempt, so a model cut short by its
slice of the request budget names the silent-attempt floor
instead. The same sentence appears on the attempt row in the
request detail.
It answers one question exactly — what each model on this route is allowed before the handover — and deliberately does not model time already spent, a first model that was retried, benched models shortening the chain, or the reasoning answer deadline taking over.
When to stop trying a model
-
Bench failures — the master switch, and it
ships off: every model in the chain is
tried every time and a failing model is retried at every
request. With it off every other control in the card is
inert. The same switch is at the top of
Model Config, beside the routes it governs
— one setting, two places to reach it. A default only
applies where the key is unset: an install whose
.envalready carriesFALLBACK_BENCH_ENABLED=truekeeps benching on until that line is removed. -
What counts as a failure, once it is on:
an upstream 5xx, an
overloaded response, and a
401/403 from the provider. Those say
something about the model. Timeouts (first token, stall,
budget), 429s,
quota,context_length,model_rejectedand malformed requests are facts about the request — they fail identically on a healthy model — and never bench one. Counting them is how a single oversized prompt used to eject an entire chain. -
Eject mode —
rate_basedbenches a model when a share of a recent window of requests fails (with a minimum number of samples first, so one bad minute cannot bench anything);legacycounts consecutive failures instead. The controls the unselected mode does not use stay on the page, dimmed and disabled, each saying why beside itself — Not used while eject mode is legacy, Not used while benching is off. They are not saved, so switching mode drops a pending edit to them from the next Apply, and the unsaved-change counter drops with it. - How long a benched model sits out applies in both modes. A chain is never emptied: if every model on a route is benched they are tried in order anyway.
Which failure kinds end a route outright rather than trying the
next model is FALLBACK_SKIP_KINDS, and it stays on
the Model Config page — it is a routing
decision, and it belongs beside the chains it ends. The
Benching card links to it.
The client-side pace
Provider rate limit
(PROVIDER_RATE_LIMIT, on
Retries & throughput) ships at
300 per PROVIDER_RATE_WINDOW of
2 seconds since 6.68.0 — 150 requests a second, per
provider. That is far above anything a person at a keyboard
generates, so requests go upstream as fast as your client
sends them, while a runaway loop still meets a ceiling. It
used to ship at 40 requests per 60 seconds, per provider,
against limits no provider had published — and because
every routing attempt spends a slot, a route averaging three
attempts began throttling after about thirteen client requests
a minute. On one measured machine, 24 concurrent requests took
a median of 938 ms and a 95th percentile of
54 seconds, all of it MCC waiting for its own
window. 6.62.0 answered that by shipping 0; 6.68.0 puts a
number back, at a pace that cannot be what holds you up.
Nothing about a real limit changed. A provider that is over
quota answers 429 with a Retry-After,
and Credential health obeys it either way. Set
a positive number only to hold a metered key back on purpose
— and when you do, the queue is fair: waiters are
admitted in the order they arrived, so a paced request waits
its turn instead of losing a lottery.
PROVIDER_MAX_CONCURRENCY (default 300 since
6.68.0, was 5) was never the problem — it is an ordinary
semaphore and it was always fair. Lower it if your machine or
your link is the bottleneck rather than the provider.
What a key is judged on
The Credential health card holds the only two
penalties a key can earn.
CREDENTIAL_LOCKOUT_TIERS is the ladder a
401/403 walks — 300,3600,86400,
so five minutes, then an hour, then a day for each successive
auth failure — and RATE_LIMIT_COOLDOWN_SECONDS is
only the fallback for a 429 that arrives
without a Retry-After header, capped at an hour
either way. That 429 bench is scoped to the one model it
happened on;
CREDENTIAL_MODEL_BENCH_ESCALATION is how many
different models have to be rate-limited on the same key at
once before the key itself is benched.
RATE_LIMIT_ROUTES_AROUND_MODEL decides what
happens next: on (the default) the request goes straight to
the next model on the same provider rather than
retrying the one that refused and then spending the rest of
the pool on it — one measured request spent 51 of its 57
seconds asleep between those retries. Off restores
retry-then-rotate.
Nothing else on this page benches a key; see
Use more than one key safely.
CREDENTIAL_CIRCUIT_THRESHOLD no longer exists —
the breaker it configured was removed along with the fixed
cooldown ladder and half-open probing. A stale line for it in
~/.mcc/.env is ignored rather than fatal, but it
is doing nothing; delete it.
What to keep
What the request log keeps is set on Analytics now, at the bottom of the page whose tables it decides the contents of.
Blank means default
Clearing a field is a way of saying "stop specifying this", so
a blank value falls back to the built-in default rather than
failing. A value outside its range is refused here with the
range quoted; a value edited directly into
~/.mcc/.env is clamped at startup with a warning
in the log, because a proxy that will not start is worse than
one running a sane number. A setting you have never chosen is
written to ~/.mcc/.env as a commented placeholder
rather than a value, and Use default removes
the line again — so the file records what you chose, not what
the defaults happened to be on the day you saved.
When a provider runs out of credit
"Out of credits" is its own failure kind, quota, separate
from a rate limit and separate from a bad key. It gets its own kind
because the three want opposite responses: a rate limit means wait, a
bad key means stop, and an empty balance means try something
else.
Running out of credits never ends the route.
quota is deliberately absent from the default
FALLBACK_SKIP_KINDS: an account with no balance has said
nothing about your request, and the next key or the next model may
answer it perfectly well. The chain keeps going. Add quota
to that setting only if you would rather a request fail than be served
by a different model.
The exhausted key is benched for the cooldown so the next request does not re-spend a second on a balance that is still zero, and the card on Providers says which key and why. It comes back on its own; you do not have to re-enter anything.
Use more than one key safely
Both model and web search providers take several keys in one variable:
EXA_API_KEY="key-a,key-b,key-c"
EXA_API_KEY_ROTATION=failover
Policies are single, round_robin,
least_used and failover —
failover by default with several keys,
single with one.
The order is the failover order. Every policy reads the
pool top to bottom: failover serves the first healthy key,
single serves the first key and nothing else, and
round_robin cycles in that order. Open Manage keys
on any provider card and drag a key by its grip, or use
Move up / Move down (or the arrow keys on a focused
grip). It applies at once and needs no restart, but it rebuilds the pool,
so that pool's health counters and benches start again — exactly as
they do when you add or remove a key.
You can name a key. Type a name in the box beside it, or
in the optional Name field when you add one. A named key then reads as its
name wherever a credential is shown — the card, the request log, the
retry ladder, the per-key analytics and the CSV — with its masked
label still there on hover and still in the key_label export
column beside the new key_name one. Names live in
~/.mcc/credential_names.json, keyed by a hash of the key and
never by the key itself; they never reach an upstream request, the request
log or a rollup. A name is display only, so renaming a key rewrites no
history. Renaming never restarts or rebuilds anything.
Health is tracked per key, and only the provider's own verdict on the
key changes it: a 401/403 locks the key out on an
escalating ladder, and a 429 benches it for exactly as
long as the provider asks via its Retry-After header, never
an invented delay, and only for the model that was
rate-limited, because a gateway that limits one model says
nothing about the key's others. Once
CREDENTIAL_MODEL_BENCH_ESCALATION different
models are limited on the same key at the same time, the
limit is the key's and the key itself is benched. Anything else — a timeout, a 5xx, a
410, a 400 — leaves every key untouched, because the
same keys serve every model in your chain and none of those is the key's
doing. A model that will not answer is handed to the next
model on the chain instead. How long each step of that
ladder lasts is CREDENTIAL_LOCKOUT_TIERS
(300,3600,86400) on the
Limits & Resilience page, and
RATE_LIMIT_COOLDOWN_SECONDS beside it is used only
when a 429 arrives with no header to obey; the policy itself is
the rotation select on each provider's own card.
The 429 cooldown is yours to set, all the way down to
nothing. Three settings on that same card, and they
only mean anything together.
RATE_LIMIT_COOLDOWN_SECONDS is the bench used when
the provider published no wait.
RATE_LIMIT_COOLDOWN_MAX_SECONDS is the longest
wait a provider may ask for in a header and have MCC obey; an
hour was hard-coded until 7.22.0 and is still the default, and
0 removes the ceiling.
RATE_LIMIT_COOLDOWN_MODE decides what a 429 costs
the key at all: provider obeys the header under that
ceiling and is exactly what every earlier release did,
fixed ignores the header and always uses your number,
and off benches nothing — no key bench, no
(key, model) bench, no provider-wide pause. Off still rotates
to the next key and still moves down the fallback chain,
because rotating and benching are separate questions, so the
consequence is the plain one: the provider goes on answering
429 and MCC goes on spending attempts on it. There was never a
15 → 60 → 3600 ladder in the code: 15 s is a
wait a provider asked for, 60 s is this cooldown, and
3600 s was that hard-coded ceiling.
Rotating away is not the same as marking a key bad. Auth failures, 429s and connection errors move to the next key; anything else raises and the request moves to the next model, so a pool cannot be emptied by a model that is simply broken. The deliberate cost: a key failing 5xx or transport on every request is retried once per request rather than benched, because the failures that could have identified it were the same ones emptying healthy pools thousands of times a day. A 401/403 still locks it out.
Update the server
The version panel announces releases with their notes inline. Update now downloads the wheel, verifies the SHA-256 GitHub publishes for it, and installs it; a mismatch aborts. Extras such as voice support are preserved. Upgrading never restarts the server, so it cannot drop an in-flight stream.
An update has two halves, and since 6.60.0 the banner says
so. The wheel — server, CLI, dashboard — updates
itself. The desktop app is a compiled binary the
wheel pins rather than carries, and nothing used to move it
unless you happened to launch from the tray. The version panel now
reports it (v6.43.0 → v6.60.0 on next restart), a
banner says Desktop app vX available — it updates the next
time you restart the app, and Update now's success
message names both halves rather than declaring you finished when
half of you is not. See
Desktop & RTK for the mechanism.
Coming from a version before 6.60.0 takes one manual
step, because the thing that fetches the app is part of what
is being fixed — the window you are running has no swap step
in it, so nothing staged beside it would be picked up. Close the app
and run mcc-desktop --ensure-shell once (with
--target if the copy you launch is not the one in
~/.local/bin). Every update after that is automatic.
Since 6.61.0 you do not take that step, and neither does
anyone else. Just after the server becomes ready it compares
the desktop app installed on this machine with the release this build
pins, and brings it up to date itself — replaced in place when
nothing is running it, staged beside it as
MyClaudeCode.exe.new when something is, which the app's
own next start renames in. One line in the server log either way, and
the panel above says the same thing. Turn it off with Update the
desktop app automatically on the Desktop card. Nothing is
downloaded on a machine that has no desktop app installed.
The installer owns the restart, wherever you pressed Update. Inside the desktop app the flag the window sends now means only “a window is watching, so do not open a browser” — not “I will start the server”. Two owners of “restart the server” is how one update came to start two of them, and how another started none.
The install command restarts the server — by default,
since 7.1.0. Every install and every update finishes the job
rather than printing a hint: it reads PORT and
HOST from the configuration directory it installed for,
asks the product what holds that port, stops that one server by its
exact process ids inside the
graceful shutdown budget, starts
mcc-server detached, and waits for /health to
answer. A listener answering is what counts as success
— not an install that exited 0. 6.73.0 made this possible behind
a -Restart switch, which is still accepted and now does
nothing; shipping it opt-in kept the failure it was built to close. On
2026-09-11 two installs finished cleanly fifteen minutes apart with no
server running through either of them, because nobody had typed the
flag. -NoRestart / --no-restart never stops a
running server, and -NoStart / --no-start
(or MCC_INSTALL_NO_START=1) stops and starts nothing.
…and it opens the desktop app (7.1.0). When the
installer starts mcc-server it also starts
mcc-desktop, so an update ends with a working window
rather than a working port. Only when the app is installed on this
machine — the binary and the install receipt this product writes
beside it — only when it is not already running (during
an update driven from the app it is running and watching, and a second
launch would be handed straight back to it), never on a headless box
and never in CI. It is found by executable path, never by a process
name. -NoDesktop / --no-desktop (or
MCC_INSTALL_NO_DESKTOP=1) turns it off.
Exactly one server, and never anyone else's. The
restart means the server bound to the port of the configuration
directory the install is for. Every other My Claude Code server —
another port, another configuration directory, the instances you have
agents waiting on — is listed in the transcript and left
running. A port held by something that is not ours is reported and
never touched, and so is a holder that could not be identified:
“cannot tell” is never “ours”.
-NoStart / --no-start (or
MCC_INSTALL_NO_START=1) starts nothing at all.
One update at a time, and the receipt keeps every
episode. Both update paths take one exclusive lock,
update.lock, beside the receipt. A second updater does not
queue and does not install — it names the owner, points at the
transcript that owner is writing, and exits. A lock whose owner is gone
is reclaimed rather than waited on. And nobody truncates
progress.json any more: every writer appends, and an
episode opens with a marker record naming the writer, so a watcher that
looks a minute late still sees what happened. That truncation is how, at
15:04 on 2026-09-11, a hand-run install erased the whole record of the
update that had finished two minutes earlier.
On Windows the install is deferred. Windows holds the running interpreter open, so the environment cannot be replaced under a live process. The verified wheel is staged and a background helper installs it once you stop the server. Your working install is untouched until then, so a failed update cannot strand you.
If updating used to appear to do nothing, 6.41.0 is the
fix. The helper that waits for the server used to poll for a
full hour and then give up without installing anything, and the stop it
was waiting for could never finish: a client retrying against a
draining server kept handing it new work. Both halves are closed now.
New requests are refused with 503 from the moment a stop
begins, and the helper waits the
graceful shutdown budget — not an
hour — then terminates the server it was waiting for by its exact
process id, never by name, and installs. The tray's
Restart Server follows the same budget, and the installer
script waits 600 s rather than six hours.
A deferred update is not a silent one. Since 6.50.0
the background helper appends a line per stage to a progress file under
the staging directory in ~/.mcc, and the version panel
reads the last complete line back. So a Windows update that is waiting
for you to stop the server, or installing, or finished, says which
— rather than looking like nothing happened. A half-written final
line is skipped rather than treated as the end of the story.
A restart no longer destroys the log that would explain
it. Until 6.58.1 every server start emptied
logs/server.log, so the one file worth opening after a
restart went wrong had been erased by that restart. A start now
moves the previous log aside as
logs/server.<timestamp>.log, and the same
SERVER_LOG_RETAIN_FILES that caps rotations caps these
— the newest ten survive, older ones are swept on the next
start.
An update can no longer race itself. Until 6.58.3
the helper moved every launcher aside before installing,
mcc-desktop included — the one the desktop window
asks for its status on every pass. The window therefore believed My
Claude Code was gone and, by design, ran the installer itself: two
installers, one tool directory. Now the helper never moves that
launcher, and every line it writes carries its own process id, so the
window can ask whether an installer is running rather than guess from a
stage name. While one is, it says Updating to X… (installer
running, N s) and waits.
You can see everything happening during an update
(6.71.0). The installer writes two files in
updates/ inside your configuration directory as it goes:
progress.json, one JSON line per stage with a timestamp
and the seconds since the update started, and
install-<stamp>.log, everything uv
printed, appended a line at a time as it prints it. The
desktop window shows the stage timeline
(waiting-for-parent → stopping →
installing → verifying →
starting or handing-off →
done), the elapsed time, and the last fifteen lines of
that log, refreshed every tick — including through the seconds
in which the environment is genuinely gone. This tab cannot: a page in
a browser has no way to read a file on your disk and no server to ask
while the server is the thing being replaced, so Update
now prints both paths here before the server stops and then
counts down. That is the reason to run the desktop app during an
update.
An open mcc-claude window no longer costs the
install its fast path. Windows refuses to rename a running
launcher's .exe, and one refusal used to send the whole
update down the slow staged path. The fast path now always runs, and a
launcher that truly could not be replaced is kept and named —
kept: mcc-claude.exe (in use) — restart it to pick up
X. It keeps working either way: the launcher is a stub that runs
the interpreter in the tool directory the update just replaced.
A failed update brings the previous version back. Starting the server used to happen only on the success branch, so a failed install left a good previous version on disk with nothing running it. The helper now starts one on both branches and the banner says which happened.
Lock it down
| Surface | Default bind | Access control |
|---|---|---|
| Proxy API | 127.0.0.1:8082 | bearer token, if set |
| Admin UI | same port | loopback callers only |
The proxy binds this machine only by default
(HOST=127.0.0.1), and the token in a freshly written
.env is generated on this machine rather than shipped.
To reach the server from another machine, set
HOST=0.0.0.0 and give
ANTHROPIC_AUTH_TOKEN a secret: mcc-server
refuses to start on a non-loopback host with an empty token, because
that combination lets anyone who can reach the port spend your
provider credits.
Troubleshooting
mcc-servernot found after installing — close and reopen your terminal; the installer extendsPATHand an open shell will not see it.- Claude Code still talks to Anthropic — the
settings file wins over shell exports. Check
/status; see Point Claude Code here. - 401 from the proxy — the agent's token does not
match this server's
ANTHROPIC_AUTH_TOKEN. - Validation fails with 404 — usually the model id, not the key.
- Desktop test buttons fail — the server must be running; they make real calls.
- Two configs on Windows — installing under both
PowerShell and WSL gives separate
.mccdirectories. The startup log prints which one is in use. - Update now finishes, but nothing updates and nothing
restarts — fixed in 6.41.0; upgrade once by hand
(re-run the install command) and it will not recur. If you carried
SERVER_GRACEFUL_SHUTDOWN_SECONDS=300across from an older install, set it back to 20 on Limits & Resilience — that is now how long every restart waits. - My data is still in a legacy directory — from
6.65.0 the server moves it for you, once, on its first start:
~/.fccbecomes~/.mccby a single atomic rename, nothing is copied, merged or deleted, and a~/.fcc-migrated.txtnote says how to move it back. It refuses — changing nothing — while anything still holds the directory, so quit the desktop app before updating: it holdsdesktop.lockfor its whole lifetime.mcc-migratestill runs the same move by hand. - The desktop app sits on “Starting the server…”
— from 6.66.0 it does not. After three attempts, or
DESKTOP_SERVER_START_TIMEOUT× (DESKTOP_SERVER_START_RETRIES+ 1) seconds, the window replaces the spinner with the server's exit code, its last lines of output, and two paths:logs/server.logand the app's ownlogs/desktop-server-start.log. It keeps starting the server every ten seconds regardless, and the page goes away by itself the moment one answers. Installer transcripts land beside them aslogs/desktop-install-<timestamp>.log. - macOS refuses to open the downloaded app —
the
.dmgis unsigned, so Gatekeeper blocks it. Drag the app into Applications, runxattr -d com.apple.quarantine "/Applications/My Claude Code.app"once, then open it. Installing the server with the one-line command and runningmcc-desktopavoids this entirely. - Web search returns nothing useful — open the attempt detail; it shows exactly what was sent and returned.
- “Request exceeds this model's context window.”
The session outgrew the model you routed it to. The message names how
many tokens were needed and how many the model holds. A
fallback chain carries these through to a
roomier model automatically; if you have set
FALLBACK_SKIP_KINDS=context_length, it will not. - “This model rejected the request.”
The provider answered
400without saying the request was malformed — a model it does not serve on that endpoint, a parameter that model pins, a field longer than that host allows. A fallback chain carries these to the next model automatically; the attempt detail names the provider’s own words.
Every document that ships with this install — the README, the full written guide, the architecture notes — is on the Docs tab, rendered from the copy bundled with the version you are running. Each one links out to the latest on GitHub.