Repository navigation
feat(mcp): embedded MCP endpoint for managing the running server - #419
Merged
Merged
Conversation
The bounds on a probe or keep-warm interval, the threshold table, the distribution modes and the route checks lived inside the CLI commands, next to argv parsing and process.exit. Nothing else could apply the same rules without copying them, and index.js cannot be imported: it runs the command dispatch as it loads. config-ops.js holds them as changes to a config object that throw a ConfigOpError carrying the message to show. The commands keep their own argv handling and output. One difference: a bucket assignment that is wrong twice over (an unknown bucket and a value that is not a percentage) now gets the usage text instead of the bucket message. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
The protocol layer for a management endpoint, kept apart from the tools it will serve so it can be tested as a function from a request to a reply, and written against Node alone: the package has no runtime dependencies and the SDK would be the first. Clients are split between two shapes of the protocol. The 2026-07-28 revision is stateless: every request carries its version and client identity in the body, mirrors them into HTTP headers, and the server must refuse a request whose headers and body disagree, since a gateway may have routed on the headers. Claude Code speaks it to an HTTP server, opening with server/discover. The revisions before it open with an initialize handshake instead. One endpoint answers both: a request carrying the body metadata is held to the stateless rules, anything else is served as the handshake era. No session id is issued, which the handshake revisions allow, so nothing is kept between requests. A list or discover result on the stateless revision carries the caching hints that revision makes mandatory. They are not decoration: Claude Code validates the result and drops the whole tool list when they are missing, while still reporting the server as connected. Two more things the stateless revision asks for. Every result names the server in its _meta, since there is no session to have learned it from. And a request whose _meta lacks the required client capabilities is malformed: -32602, with a 400. Every reply is a single JSON object. There is no GET stream and no server-initiated traffic, because a tool call here is one short operation with nothing to report until it is done. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ault POST /teamclaude/mcp answers MCP clients such as Claude Code with the proxy's own control plane as tools: the fleet at a glance, quota, and the settings that steer rotation. It sits behind the same gates as the other /teamclaude/ routes — proxy key or loopback, no cross-origin POST, a Host header naming this machine — with one addition. A config with no key at all admits every caller as authenticated, which is the condition under which the rebinding check on key-less loopback requests is skipped, so this route asks it again. The endpoint is off until proxy.mcp says "read" or "full", read per request so a reload can flip it, and any other value keeps it off. Off means a 404 answered here: an unclaimed path falls through to the upstream forwarder with a fleet credential attached, which is the wrong place for a client probing the endpoint to land. For the same reason the route also matches a trailing slash or a query string: the URL is typed into a client by hand. The read tools are built from the status payload rather than handing it over: that payload names every session, client and usage dimension, carries raw error text from upstream, and is far larger than a model context should pay for. Settings are listed by name, because the same object holds the proxy keys and every account credential. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
With proxy.mcp set to "full", the endpoint also changes things: it switches, enables, disables, reprioritises and removes accounts, and sets the threshold, distribution, probe interval, keep-warm, routes, blocked models and default client mode — everything the CLI's management commands can, through the same rules in config-ops. Settings go the way the CLI's do: written to the file under the config lock, then applied by a reload. Account changes go the way the TUI's do, in process and then saved: a reload never drops an account, and it applies a disk-side priority or disabled flag to the manager without mirroring it onto the entry the next save is built from. The account half of the TUI's save is split out and handed to the endpoint, since a headless server otherwise has nothing to save with. The settings half stays with the TUI, so an account change does not pin this server's resolved defaults into a file that never spelled them out. A name that fits several accounts is refused with the candidates rather than resolved to the first match; for a removal, the first match is the wrong one often enough. Write tools run one at a time: a client may issue several calls at once, and a reload started by one reads the disk before a removal made by another has been saved, then re-adds the removed account from that stale read. Each turn of that queue is bounded by a timeout: a reload can hang on an upstream that accepts the connection and never answers, and one caller waiting on that is one thing, every later write waiting behind it until a restart is another. Every write logs one line naming the tool, the caller and the arguments. Arguments that do not fit a tool's schema come back as a tool error, not a protocol one. The protocol keeps -32602 for a call that is malformed as a call and files a refused value under input validation, which is the kind of error a client passes on to the model so it can correct itself. Nothing runs and nothing is logged for such a call. proxy.mcp is copied on reload like the other proxy.* fields read per request, so the endpoint can be opened, narrowed or closed without a restart. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
Where it is documented follows where the reader looks: the usage page next to the browser dashboard for what it does and how to connect Claude Code to it, the configuration table for the setting, the example config, the help text, and a line in the README's feature list and documentation table. Two consequences of the shared gates are spelled out rather than left to be discovered: every proxy key holder can use the endpoint, so "full" on a shared proxy means every client can remove accounts; and a browser-based MCP client is refused as cross-origin. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
…d no open endpoint without a key A request authenticated with a named proxy.clientKeys key is now served the "read" tools even when proxy.mcp is "full": everywhere else a client key can only switch, reload and probe, and the write tools reach as far as deleting an account's credentials, so they stay with the shared proxy.apiKey and key-exempt loopback callers. Route names, match globs, route accounts, buckets and blocked-model patterns are refused when they carry a C0/C1 control character, because the TUI draws them raw and an escape sequence would repaint the operator's terminal. With no proxy key configured the key gate is open and a non-browser caller on a non-loopback bind can write Host: localhost itself, so the endpoint now also requires what the loopback exemption requires (loopback peer, no forwarding header, trustLoopback not off) and answers 403 otherwise. A reload now reads an absent proxy section or proxy.mcp key as off, instead of keeping the last mode it saw in memory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # src/server.js
This was referenced Sep 20, 2026
Merged
MagicalTux
added a commit
that referenced
this pull request
Sep 20, 2026
Thirty-six commits since 1.1.20. Several change what a client sees on a failure, so read the first section before upgrading a shared deployment. Behaviour changes #439 a 401 from upstream is no longer relayed to the client: the request fails over like a 403, and an API-key account or an OAuth account with no refresh token leaves rotation (it used to be picked again on every request). With nothing left the client gets the proxy's own error #439 a path under /teamclaude/ that no control route claims — a typo, or the wrong verb — answers 404 locally instead of being forwarded upstream under a fleet credential #429 the synthetic 429's retry-after is the real reset of the windows blocking the request's candidate accounts, not a flat 60s, and the message counts only those candidates; #408 names accounts that need a re-login instead of calling them "at quota" #438 session pins are per conversation (session id plus a digest of the first message), so a session's subagents spread across accounts. `sessions.items[].id` in status is the composite key, load is counted per conversation, and a persisted concurrency cap re-learns #378 a `thread: continue` bound for a per-account third-party upstream is refused with the 400 Anthropic gives, so the client resends the whole conversation; `messageThreads: true` opts a relay out #434 a 429 whose x-codex-* headers show a spent account-wide window holds the account like an Anthropic rejection; a spent model-scoped bucket only moves the request #437 a Codex response head is awaited for five minutes (Anthropic unchanged) #411 idle keep-alive connections are held 120s on both listeners #389 with session distribution on, requests carrying no session id rotate on a cursor of their own instead of all resting on the current account #405 `defaultClientMode` ("mitm" | "base-url") sets what `run` and `env` do without a flag; `--mitm` / `--no-mitm` decide per launch, and in base-URL mode `env` unsets a stale proxy export naming this proxy #439 route `--bucket` is validated; an array `switchThreshold` reads as the default with one line saying so Features #419 an MCP management endpoint at POST /teamclaude/mcp, off unless `proxy.mcp` is "read" or "full"; a named client key is read-only even in full mode, and with no proxy key it serves only this machine #428 per-account `switchThreshold`, a number or a per-bucket table #406 a Claude+Codex pool is drawn as two panes on a wide terminal, each with its own current marker; #392 names the provider in a mixed list; #418 lets the operator arrange the list (`displayOrder`); #376 draws loopback-served accounts last; #435 shows the percentage beside a bar's countdown; #394 shows the running version in the header #430 free Codex rate-limit reset credits in status, the TUI and the dashboard #385 `proxy.terminalOnly` tunnels chatgpt.com so ChatGPT Desktop stays out Fixes #404 a TUI paint can no longer block the proxy (stdout non-blocking, frames dropped while the terminal is behind); #410 a dead terminal no longer takes the proxy with it, and SIGHUP shuts down cleanly #433 token usage is booked from Codex Responses streams #386 #387 #388 the Codex five-hour window is read from the model-scoped family and the usage probe, and extra limits are named from their entries #431 a headerless 429 that follows the request is retried once #432 #439 startup and collaborator log lines reach the TUI's activity pane and log file instead of the covered terminal #415 #403 #439 reload mirrors `priority`, `disabled`, `stripRequestFields` onto the config entry, and a reload during a removal does not re-add it #439 the Host check uses the address actually bound; sx.org calls time out #381 the dashboard polls status before asking for a key #403 session outcome accounting classifies the decoded path; account names in daemon log lines are sanitised Tooling #371 #372 #373 `npm run typecheck` (tsc over the JS sources) in CI, with a strict-mode ratchet: per-file strict diagnostics may not grow past the pre-merge commit (2006 at introduction, 1735 now) #401 docker workflow actions bumped Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opt-in MCP endpoint at
POST /teamclaude/mcp, served by the proxy itself. Claude Code (or any other MCP client) can read the fleet state and change rotation settings from inside a session.Controlled by
proxy.mcp, off by default."read"givesget_status,get_quotaandget_settings."full"adds the write tools: switch, enable/disable, priority, remove, threshold, distribute, probe, keep-warm, routes, blocked models, default client mode. That is what the CLI management commands can do. There is no login, import orapitool, so no credential goes through a tool call.Design notes:
initializehandshake. Claude Code already uses the new one over HTTP./teamclaude/routes. With no key in the config at all the rebinding Host check is skipped for every caller, so this route checks Host itself.config-ops.js. CLI output is the same except for one doubly-invalidthresholdinput, details in the commit.get_statusis a summary. The raw status payload names every session and client, and is too big for a model context.One thing I'm not sure about:
"full"lets every proxy key holder remove accounts. It is documented and it is why the default is off. If you prefer writes gated differently (loopback only, or the shared key only), I can change it.Tested with unit tests, an e2e test against a headless server, and by hand with Claude Code 2.1.276 calling a read and a write tool. The manual run found a bug my tests missed: the 2026-07-28 revision requires caching hints on
tools/list, and Claude Code drops the whole tool list without them.