Borrowing it
Nothing to install: this file belongs to pacnpal/mcpelevator. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.
curl -O https://raw.githubusercontent.com/pacnpal/mcpelevator/main/CLAUDE.mdgit clone --depth 1 https://github.com/pacnpal/mcpelevatorWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/instructions/pacnpal/mcpelevator/claude-md)<a href="https://agentmods.dev/instructions/pacnpal/mcpelevator/claude-md"><img src="https://agentmods.dev/badge/instructions/pacnpal/mcpelevator/claude-md/github.svg" alt="Measured on agentmods" height="20"></a>Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.
<a href="https://agentmods.dev/instructions/pacnpal/mcpelevator/claude-md"><img src="https://agentmods.dev/badge/instructions/pacnpal/mcpelevator/claude-md.svg" alt="Reviewed on agentmods" width="80" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.06098 | $0.06098 |
| Opus 5.5 | $0.02439 | $0.02439 |
| Sonnet 5.5 | $0.01220 | $0.01220 |
| Haiku 4.5 | $0.00610 | $0.00610 |
Grade A, and why
mcpelevator CLAUDE.md scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured today.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 293 lines — stays where its author put it; the contents beside it link to each section on GitHub.
mcpelevator — self-hosted control plane that elevates stdio MCP servers into authenticated HTTP endpoints
One container runs stdio (or already-remote) MCP servers and exposes each as a remote
Streamable-HTTP endpoint (/s/<slug>/mcp) with a SvelteKit UI, process supervision, and auth.
Commands
Run backend commands from backend/, frontend commands from frontend/. The Makefile
wraps the common ones.
- Backend dev (autoreload, http://127.0.0.1:8080):
cd backend && uv run uvicorn app.main:app --reload --port 8080—make dev-backend - Frontend dev (HMR, http://localhost:5173, proxies /api and /s to :8080):
cd frontend && npm install && npm run dev—make dev-frontend - Build SPA into
frontend/build:cd frontend && npm ci && npm run build—make build - Backend tests:
cd backend && uv run pytest -q—make test - Frontend tests:
cd frontend && npm run test(vitest) - Frontend typecheck:
cd frontend && npm run check(svelte-check) - Refresh the uv lockfile:
cd backend && uv lock—make lock - Build + run everything in Docker:
docker compose up --build—make docker
Architecture
One FastAPI process serves three surfaces in a single port (backend/app/main.py):
/api/* control plane, /s/<slug>/mcp reverse proxy, and the built SPA as a catch-all mount.
- Desired-state reconciliation. SQLite is the source of truth. A background supervisor task
(
supervisor/) converges running processes to the desired state (Kubernetes-style), so the system is idempotent and survives restarts. Operator lifecycle actions write desired state or queue an activation — they never spawn or kill directly.Supervisor.restartis the ONE restart primitive (stop the unit, queue a fresh activation, no config write);POST /api/servers/{id}/restartandPOST /api/groups/{name}/restart(per enabled member) both go through it, and the UI's singleRestartButtoncalls those. The supervisor also owns idle quiescence: an enabled server with no proxy traffic inside its idle window (per-serveridle_timeout_s, else theidle_timeout_sruntime setting; 0 = off) is stopped into anidlestate, and the proxy wakes it on the next/srequest, holding the request until readiness (ADR-0002). - One bridge process per enabled server (
bridge/,runners/): each runs its own uvicorn on a loopback port hosting a FastMCP proxy of the stdio command (or an upstream HTTP/SSE URL), fault-isolated with a real PID and logs. When the server'srest_openapiexposure is on, the same bridge also serves each tool as plain REST (/rest/<tool>+ a generated/rest/openapi.json), reached through the same/s/<slug>/proxy path and auth. A server's Per-tool policy —disabled_tools(names to hide) andtool_overrides(upstream name -> replacement name/description) — is applied by ONE FastMCPToolTransformin the bridge (_tool_transform), so hiding, renaming, and re-describing all reach every surface at once (MCP, REST, and the group hub, which all resolve tools through this proxy): a hidden tool is dropped fromtools/listand refused on call, and a renamed one answers to its new name only. A renamed tool carries its pre-rename name in_metaunderUPSTREAM_META_KEY— a contract between the bridge and the discovery probe (supervisor.unit.tool_summary), which lifts it toupstream_nameso the UI keys per-tool state off a stable identity instead of reversing the rename map (an exposed name isn't unique). Both are part ofconfig_hash, so a change restarts the bridge. A stdio child is ONE kept-alive connection shared by every client, so what it sends back (roots/sampling/elicitation requests, logs) can't be attributed by the connection:_StdioRoutingreplaces the session's callbacks and reads a single caller slot, neverget_context(). By default nothing reaches a client (roots[], sampling/elicitation declined, logs to the bridge log); the per-serverserialize_client_requestsflag runs one proxied request at a time (_SerializeClientRequestsmiddleware) and fills the slot, so the child can only be asking that caller (issue #143). Progress is bound per call by its token. With serialization on, the opt-inattach_stderr_on_errortees the child's stderr (_StderrTee: a raw byte ring addressed by stream offset, so a window is exact, with the server's secret values hidden over the window's bytes before it is split into lines) and appends the lines a failedtools/callwindow saw to its error result, scrubbed (_with_stderr, issue #227). Pushing needs a back-channel, which only handshake-era client connections have. An interactive request (tools/call,resources/read,prompts/get) from a client on the sessionless2026-07-28protocol (FastMCP 4's default) runs as a_Turninstead: the proxied request runs as its own task, the upstream's request comes back to the caller as anInputRequiredResult, and the retry (matched by therequest_stateFastMCP seals) feeds the answer to the waiting request; logs and progress follow the live leg. On a serialized stdio child the turn fills the caller slot and owns the lock (_SerializeClientRequests); for a remote upstream and for groups (_ModernTurns), each request's own upstream session finds its turn through_CURRENT_TURN(_TurnAwareProxyClient). A group's parked turns outlive its membership changes; a change ends the turns whose members (_Turn.members) it no longer mounts, and a parked turn holds the members it reached in flight (GroupHub._hold) so the idle sweep can't stop one under it; on/sthe control plane can't see a park, so an idle timeout under_PARKED_TURN_TTL_Scan still lose one (documented). At most_MAX_PARKED_TURNSpark at once. The upstream leg is pinned to the handshake era (ProxyClientdefaults tomode="legacy"), and caller headers reach it only viaProxyClient(forward_incoming_headers=...), which FastMCP 4 turns ON by default. - Secret env (
sealed.py): a per-key choice, for env vars and remote/openapi headers alike. A secret value is stored inenvas a Fernet token (PREFIX), keyed by<data_dir>/env.key, so a value is secret exactly when it's sealed (no flag column). The API masks it as<hidden>and takes<hidden>back to keep it;runners.build_specis the ONE place it's decrypted.sealed.resolverefuses to carry a kept secret through a change of what the server runs (service._RETARGET_FIELDS, plus, for a local runner, any other env edit:NODE_OPTIONSand friends execute code; for remote/openapi, aHostheader change) or through making it visible..mcpbexports turn secrets into sensitiveuser_configprompts, and every manifest is validated against the vendored official MCPB 0.2 schema before it's served. - Source repositories (
runners/repo.py,catalog/repository.py): a local-runner server can launch from a git checkout mcpelevator owns (repo_url/repo_ref/post_clone_script, all inconfig_hash). The unit runs a generated checkout script as its owncheckoutstartup phase (underrepo_build_timeout_s, before setup): clone/fetch into<data_dir>/repos/<id>, keep the old checkout if the fetch fails, and run the post-clone script only when the commit or script changed (a stamp beside the checkout). The runner stays pure — the unit resolvescwdinside the checkout.GET /api/catalog/repository(and the control MCP'sget_repository_draft/install_from_repository) detect the steps from a GitHub repo's manifests; detection is best effort and every field stays editable. - Per-server cache (
runners/cache.py): each local (npx/uvx/command) server's setup/checkout shells and child seeXDG_CACHE_HOME=<data_dir>/cache/<id>andPLAYWRIGHT_BROWSERS_PATHunder it (serverenvwins). The unit resolves the dir (like the checkout cwd, builders stay pure) and hands it to the bridge in its own env (MCPE_BRIDGE_CACHE_DIR), not the size-capped spec; not inconfig_hash.POST /api/servers/{id}/cache/cleardeletes it insideSupervisor.restart'swhile_stoppedhook (orstop's, when disabled), so nothing writes into it meanwhile; delete removes it too. - Upstream OAuth (
auth/oauth_store.py,auth/oauth_flow.py): aremoteserver can authenticate to its upstream via OAuth instead of staticenvheaders. The interactive authorization-code grant (DCR + PKCE) runs in the control plane (/api/servers/{id}/oauth/authorize→ public/api/oauth/callback, anchored on the OAuthstate) using the MCP SDK'sOAuthClientProvider. Client identity is provider-adaptive: a static client id/secret when set, else the instance's CIMD client-metadata document (public/api/oauth/client-metadata.json, offered only from an https base whose document a self-probe confirms is publicly fetchable — an auth-gating proxy such as Cloudflare Access in front of the instance 401s the provider's server-side fetch, so the flow falls back instead; theupstream_oauth_client_moderuntime setting pins the choice explicitly tocimdordcr, probe-free, and a server'soauth_client_modecolumn overrides it per server,inheritdeferring to the setting) where the provider advertises URL-based client ids, else DCR; tokens land in a0600file store (<data_dir>/oauth/<id>.json) shared with the bridge, which reads them and auto-refreshes. Tokens live off the DB, so authenticating never re-hashes the row or bounces the bridge. - Runners (
runners/):npx,uvx,command,remote(proxy an already-remote MCP URL),openapi(no MCP upstream: the bridge fetches a REST API's OpenAPI document at startup and serves each operation as a tool via FastMCP'sOpenAPIProvider,bridge/openapi.py, under the same_tool_transform;command= document URL, or blank with a pasted/uploaded document inopenapi_document(handed to the bridge on stdin since it can outgrow the spec env var);args=[]or[base_url],env= API headers, which always win and are dropped from tool arguments — and the caller's request headers are never forwarded to the API),graphql(no MCP upstream either: the bridge loads a GraphQL schema — introspection of the endpoint incommand, a schema URL inargs[0], or SDL/introspection JSON stored ingraphql_schema— and serves each Query/Mutation field as a tool,bridge/graphql.py; arguments go as variables, a generated selection set is replaceable per call viaselection_set,graphql_queryruns any operation, every operation is validated against the schema first;env= API headers, caller headers never forwarded, no redirects), anddocker(image-packaged servers — opt-in + root-equivalent behind thedocker_runnersetting). Each runner is a pureServer -> ProcessSpecbuilder;dockerstores the canonical image+container-args+env shape and synthesizes a hardeneddocker run(the bridge scrubs the child env for docker units so a-e KEYpassthrough can't reach the control plane's secrets). - Built-in control MCP (
control_mcp.py,runners/builtin.py):/api/mcpserves the control plane as MCP tools, each a thin in-process call to one/apiroute carrying the caller's headers, so auth and policy are never re-derived. Clients reach it as themcpelevatorserver, one row (runner="mcpelevator", outsideRUNNERS) thatservice.ensure_builtinprovisions on boot (MCPE_CONTROL_MCP=falseskips that). It runs through an ordinary remote bridge that forwards the caller's Authorization (ProcessSpec.forward_authorization, on for this runner only) plus a per-process internal header. Only MCP exposure, tool policy, name, slug, auth (none/bearer) and idle are editable (REST stays off: its in-process proxy call can't forward the caller's token). It can't be deleted, cloned or reassigned (it stays admin-owned, since itsenabledis a global setting). It joins a group only when named ("*"skips it); the hub forwards the caller's headers to that member alone, and such a group also opens to an ADMIN's control credential. The bearer provider accepts any control credential for the server itself (principal.credential_principal), and the/sproxy runscontrol_mcp.authorize_edgeso an unusable credential gets a 401/403 with a reason instead of a failedinitialize. Behind Cloudflare Access (auth/cf_access.py, opt-in viaMCPE_CF_ACCESS_TEAM_DOMAIN+MCPE_CF_ACCESS_AUD) the signedCf-Access-Jwt-Assertionmaps its email to the user of that name;principal.resolvetrusts it only on requests carrying the bridge's internal header (runners.builtin.is_internal), never on the rest of/api(CSRF). The global switch is the row'senabled(surfaced as thecontrol_mcpsetting) and the per-user switch isUser.control_mcp./api/mcpchecks both on every request. - Standard logging (
logconfig.py): with debug off, one scrubbed stderr handler on theapplogger atMCPE_LOG_LEVEL(INFO); uvicorn's access log is silenced unlessMCPE_ACCESS_LOG(debug re-opens it). The supervisor logs each server's start, failed attempts, terminal failure (hint + the tail of its own log), crash, idle stop and stop there, so a failure's reason reaches the container log without debug mode; debug mode removes that handler and takes over. A startup attempt's failure also lands inlast_errorwhile retrying (cleared on running). Setup/checkout shells run behindunit.DPKG_REPAIR_PREAMBLE, which runsdpkg --configure -aas root when dpkg reports an interrupted run (a setup killed mid-apt otherwise breaks every later attempt). Per-serverstart_timeout_s/repo_build_timeout_soverride the instance deadlines, outsideconfig_hash. - Health checks (
supervisor.unit.HealthCheck, issue #225): an optional per-server tool call (through the bridge's loopback/mcp, so never usage or idle traffic) or, for npx/uvx/command, a shell command run like setup (DPKG_REPAIR_PREAMBLE, child env and cwd). It runs at the end of_await_readyas its ownhealthphase under the same deadline (a failure is an ordinary_AttemptFailed), and, withhealth_check_interval_s, the reconcile sweep hands each unit its current check (tick_health_check), which runs a due one against a live running unit off the sweep. A periodic failure setsunhealthywithhealth_failing(no restart: the sweep only re-derives a crashedunhealthy), and a passing check returns it torunning. Outsideconfig_hash;health_check_commandis a retarget field for sealed env and needs the local-runner permission. - Debug mode (
debug.py,MCPE_DEBUG=true): deliberately maximal. TRACE logging for every logger in the control plane and every bridge (they inherit the env; bridges also run uvicorn attrace), every SQL statement (values hidden byhide_parameters),TraceMiddlewarelogging each request's arrival and headers, its body as the app reads it, response start, every streamed event and a summary, excepthooks + faulthandler + asyncio debug, and the admin-onlyGET /api/debugbundle. Every line passes throughdebug.scrub(and headers throughdebug.fingerprint): never log a usable credential. Off, none of it is installed and the route 404s. New failure paths should log through a moduleloggerso debug mode picks them up. - Group registry (
groups/): thegroupsruntime setting is the single source of truth mapping a group name to its members — either"*"(every registered server, present and future, except the built-in control MCP server) or an ordered list of server ids. Each group is served at/g/<name>/mcp: a control-plane-hosted FastMCP mounting a proxy per running member, tools namespaced by slug, kept current after each reconcile via the supervisor'son_convergedhook. The members sit in oneAggregateProviderper group, and a membership change builds a new one and swaps it into the bundle in one assignment (GroupHub._install), so the group's stateful sessions (the handshake-era back-channel for pushed roots/sampling/elicitation) survive it and see it at once, a request in flight keeps the snapshot it started with (never mutate a live provider list: FastMCP re-indexes it after awaiting), and the replaced snapshot's extension runtime is closed rather than left in the bundle's scope. A parked turn resumes only while every member it reached is still mounted. Every membership change is announced aslist_changed(GroupHub._announce): to the handshake-era connections_Sessionsremembers (the SDKConnection, never the per-requestServerSessionfacade), on their standalone stream, and on a2026-07-28caller'ssubscriptions/listenstream (the bundle serves it with the SDK'sListenHandler, which is also what makes it advertiselistChangedto those callers). An instance is rebuilt only after a teardown (an auth-affecting write or a failed sync), and the torn-down one drains its in-flight requests before closing, so a call that tears down its own group still returns; standalone GET streams andsubscriptions/listenPOSTs (byMcp-Method, trusted only on a non-handshakeMCP-Protocol-Version, where the SDK checks it) don't hold members awake; a turn that would park after a teardown detached its table (_ParkedTurns.closed) has its input request refused instead (the hub owns each sub-app's lifespan; Starlette doesn't run mounted lifespans). There is no special-case name —allis just a conventional entry with members"*". The registry is validated at write time and again at startup (registry.validate_at_startupfails the boot on an unknown member id, naming the offending group + server); an unknown group name 404s at request time and an empty group serves a valid tool-less bundle. Auth runs through the sameenforce()with a syntheticgroup:<name>pseudo-server: bearer requires a matchinggroup:<name>-scoped (orall) token, and members stricter than the default provider are excluded when the default isnone. There is no/s/all— single servers live only under/s/<slug>, groups only under/g/<name>, soallis now an ordinary server slug. - Auth (
auth/): two independent layers per request — a Host/Origin allowlist middleware (DNS-rebinding defense) plus pluggable per-server bearer auth on/sand control-plane bearer auth that gates the sensitive/apirouters only when enforcement is on (defaultauto: when the box is exposed off-host);/api/health*and/api/auth/statusstay public. See README "Security" for the full model. - Multi-user control plane (
auth/principal.py,auth/policy.py,api/users.py): WHO is resolved in exactly one place (principal.resolve— enforcement off ⇒ synthetic local admin,MCPE_ADMIN_TOKEN⇒ env admin, a user-less control token ⇒ legacy admin, a user-bound one ⇒ that user's role/flags) and WHAT they may do lives entirely inpolicy(server/token visibility, the local-runner permission, member token-scope limits) — routers call those predicates, never re-derive rules. Users hold no passwords: credentials arecontroltokens bound viaToken.user_id;Server.owner_id(NULL = admin-owned, the upgrade default) drives visibility, and non-visible resources 404 like nonexistent ones. Ownership is identity, not launch config — it stays out ofconfig_hash, so reassigning never bounces a bridge. This is authorization, not isolation: local runners execute as the process user (README "Security", trust caveat). - Catalog (
catalog/): backend proxies public MCP directories (official registry, Glama) into reviewable launch specs; the SPA stays same-origin.catalog/mirror.pykeeps a local copy of each directory (<data_dir>/catalog-mirror.db, a cache, never desired state) synced in the background, and sources read it before going live, so browsing survives an upstream outage. - Frontend (
frontend/src/): SvelteKit (Svelte 5) SPA,adapter-static, no SSR — rendered entirely in the browser, served by the backend catch-all. Lifecycle controls are shared components, not per-page copies:ServerActionButton(start/stop/retry, withprimaryServerActiondeciding which) andRestartButton(a server or a whole group) are rendered by the dashboard card, the server page header, and the group member rows alike; both expose a bindablebusyso a page can gate its own work on the op it started. Tool labels are edited in exactly one place too —ToolLabelModal, a dialog holding a local draft that Save stages into the page's batch (the tool rows clamp long descriptions and only open it), and the batch's Apply bar floats above the page so it is reachable from anywhere in a long tool list. Charts are LayerChart (the Svelte 5 charting library shadcn-svelte's charts are built on); it takes its colours from CSS variables, whichapp.cssmaps onto this app's tokens (.lc-root-container/.lc-tooltip-root) so charts inherit the one-accent design system rather than importing a second one. Fixed-geometry visuals (the usage punchcard, proportional row bars) stay plain DOM — a charting library adds nothing there.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- today Changed · +41 lines · +855 tokens per session 9617a0d42d05
- 3d ago Changed · +30 lines · +694 tokens per session 142125b6de2d
- 4d ago Changed · +10 lines · +235 tokens per session 27e5118e0490
- 6d ago Changed · +21 lines · +438 tokens per session 86eeee80f0b5
- 13d ago Changed · +2 lines · +54 tokens per session ee79104a36f3
- 13d ago Changed · +25 lines · +676 tokens per session 4f3d08378545
- 15d ago Changed · +6 lines · +148 tokens per session c81a43d24219
- 1mo ago Changed · +13 lines · +257 tokens per session f9cbb1e6d0f1
- 2mo ago Changed · +5 lines · +115 tokens per session c2843c9002a4
- 2mo ago First seen · 140 lines · 2,626 tokens per session scan A 3c20364bf40d
mcpelevator CLAUDE.md is an instructions file published in the GitHub repository pacnpal/mcpelevator (7 stars, last pushed today), licensed MIT. It adds 6,098 tokens to every session, about $0.0244 per session on Opus 5.5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.
mcpelevator CLAUDE.md compared with instructions from other repositories
| Mod | Type | Stars | Last push | Licence | Scan | Tokens | Works with |
|---|---|---|---|---|---|---|---|
| mcpelevator CLAUDE.mdpacnpal/mcpelevator | Instructions file | 7 | today | MIT | A | 6,098 | Claude Code |
| vscode buildNext.instructions.mdmicrosoft/vscode | Instructions file | 195k | yesterday | MIT | A | 9,719 | GitHub Copilot |
| next.js AGENTS.mdvercel/next.js | Instructions file | 143k | yesterday | MIT | A | 7,461 | Codex, OpenCode |
| langchain AGENTS.mdlangchain-ai/langchain | Instructions file | 149k | yesterday | MIT | A | 4,680 | Codex, OpenCode |
| vscode oss-third-party-notices.instructions.mdmicrosoft/vscode | Instructions file | 195k | yesterday | MIT | A | 5,516 | GitHub Copilot |
| vscode copilot-instructions.mdmicrosoft/vscode | Instructions file | 195k | yesterday | MIT | A | 3,093 | GitHub Copilot |
| deepseek-harness AGENTS.mddeepseek-ai/deepseek-harness | Instructions file | 274k | 2d ago | MIT | A | 4,313 | Codex, OpenCode |