mcpelevator: Instructions file for Claude Code

CLAUDE.md

mcpelevator CLAUDE.md is an instructions file for Claude Code from pacnpal/mcpelevator. It costs 6,098 tokens per session, scanned A, original, MIT.

A repository-specific instruction file for mcpelevator, a self-hosted service that turns local MCP servers into authenticated web endpoints and includes a web interface.

In plain words
What is it for?
Use it when changing the service’s Python backend, SvelteKit frontend, authentication, server supervision, tests, builds, or container setup.
Why use it?
It tells coding agents how the backend, frontend, tests, development servers, and Docker setup fit together.

Instructions file for Claude Code

Written for Claude Code: the file is CLAUDE.md.

This is pacnpal/mcpelevator's own configuration. It tells Claude Code how to work on mcpelevator itself, so it is not a mod to install elsewhere. Copy it as a starting point and replace the rules that are about this project. Everything mcpelevator configures →

Reuse

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.

Copy the file
curl -O https://raw.githubusercontent.com/pacnpal/mcpelevator/main/CLAUDE.md
Clone the repo
git clone --depth 1 https://github.com/pacnpal/mcpelevator

Made for: Claude Code.

Wrote 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.

agentmods badge for mcpelevator CLAUDE.md

README.md
[![agentmods](https://agentmods.dev/badge/instructions/pacnpal/mcpelevator/claude-md/github.svg)](https://agentmods.dev/instructions/pacnpal/mcpelevator/claude-md)
Your own site
<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.

agentmods 80×15 button for mcpelevator CLAUDE.md

Your own site · 80×15
<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>
Per session 6,098 This file is loaded in full into every session.
When invoked 6,098 The same file — it is already loaded in full.
Security scan A 0 findings. A grade says what 26 rules found in the file — not that it is safe.
Origin original No closer match found in the catalogue.
Token cost

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.

ModelPer sessionOnce 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

Measured today against content hash 9617a0d42d05, method: parsed. Prices are Anthropic first-party input rates as of 2026-10-07, from the pricing page.

Security

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.

CLAUDE.md · 293 lines

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.restart is the ONE restart primitive (stop the unit, queue a fresh activation, no config write); POST /api/servers/{id}/restart and POST /api/groups/{name}/restart (per enabled member) both go through it, and the UI's single RestartButton calls those. The supervisor also owns idle quiescence: an enabled server with no proxy traffic inside its idle window (per-server idle_timeout_s, else the idle_timeout_s runtime setting; 0 = off) is stopped into an idle state, and the proxy wakes it on the next /s request, 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's rest_openapi exposure 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) and tool_overrides (upstream name -> replacement name/description) — is applied by ONE FastMCP ToolTransform in 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 from tools/list and refused on call, and a renamed one answers to its new name only. A renamed tool carries its pre-rename name in _meta under UPSTREAM_META_KEY — a contract between the bridge and the discovery probe (supervisor.unit.tool_summary), which lifts it to upstream_name so 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 of config_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: _StdioRouting replaces the session's callbacks and reads a single caller slot, never get_context(). By default nothing reaches a client (roots [], sampling/elicitation declined, logs to the bridge log); the per-server serialize_client_requests flag runs one proxied request at a time (_SerializeClientRequests middleware) 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-in attach_stderr_on_error tees 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 failed tools/call window 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 sessionless 2026-07-28 protocol (FastMCP 4's default) runs as a _Turn instead: the proxied request runs as its own task, the upstream's request comes back to the caller as an InputRequiredResult, and the retry (matched by the request_state FastMCP 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 /s the control plane can't see a park, so an idle timeout under _PARKED_TURN_TTL_S can still lose one (documented). At most _MAX_PARKED_TURNS park at once. The upstream leg is pinned to the handshake era (ProxyClient defaults to mode="legacy"), and caller headers reach it only via ProxyClient(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 in env as 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_spec is the ONE place it's decrypted. sealed.resolve refuses 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_OPTIONS and friends execute code; for remote/openapi, a Host header change) or through making it visible. .mcpb exports turn secrets into sensitive user_config prompts, 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 in config_hash). The unit runs a generated checkout script as its own checkout startup phase (under repo_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 resolves cwd inside the checkout. GET /api/catalog/repository (and the control MCP's get_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 see XDG_CACHE_HOME = <data_dir>/cache/<id> and PLAYWRIGHT_BROWSERS_PATH under it (server env wins). 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 in config_hash. POST /api/servers/{id}/cache/clear deletes it inside Supervisor.restart's while_stopped hook (or stop's, when disabled), so nothing writes into it meanwhile; delete removes it too.
  • Upstream OAuth (auth/oauth_store.py, auth/oauth_flow.py): a remote server can authenticate to its upstream via OAuth instead of static env headers. The interactive authorization-code grant (DCR + PKCE) runs in the control plane (/api/servers/{id}/oauth/authorize → public /api/oauth/callback, anchored on the OAuth state) using the MCP SDK's OAuthClientProvider. 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; the upstream_oauth_client_mode runtime setting pins the choice explicitly to cimd or dcr, probe-free, and a server's oauth_client_mode column overrides it per server, inherit deferring to the setting) where the provider advertises URL-based client ids, else DCR; tokens land in a 0600 file 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's OpenAPIProvider, bridge/openapi.py, under the same _tool_transform; command = document URL, or blank with a pasted/uploaded document in openapi_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 in command, a schema URL in args[0], or SDL/introspection JSON stored in graphql_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 via selection_set, graphql_query runs any operation, every operation is validated against the schema first; env = API headers, caller headers never forwarded, no redirects), and docker (image-packaged servers — opt-in + root-equivalent behind the docker_runner setting). Each runner is a pure Server -> ProcessSpec builder; docker stores the canonical image+container-args+env shape and synthesizes a hardened docker run (the bridge scrubs the child env for docker units so a -e KEY passthrough can't reach the control plane's secrets).
  • Built-in control MCP (control_mcp.py, runners/builtin.py): /api/mcp serves the control plane as MCP tools, each a thin in-process call to one /api route carrying the caller's headers, so auth and policy are never re-derived. Clients reach it as the mcpelevator server, one row (runner="mcpelevator", outside RUNNERS) that service.ensure_builtin provisions on boot (MCPE_CONTROL_MCP=false skips 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 its enabled is 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 /s proxy runs control_mcp.authorize_edge so an unusable credential gets a 401/403 with a reason instead of a failed initialize. Behind Cloudflare Access (auth/cf_access.py, opt-in via MCPE_CF_ACCESS_TEAM_DOMAIN + MCPE_CF_ACCESS_AUD) the signed Cf-Access-Jwt-Assertion maps its email to the user of that name; principal.resolve trusts 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's enabled (surfaced as the control_mcp setting) and the per-user switch is User.control_mcp. /api/mcp checks both on every request.
  • Standard logging (logconfig.py): with debug off, one scrubbed stderr handler on the app logger at MCPE_LOG_LEVEL (INFO); uvicorn's access log is silenced unless MCPE_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 in last_error while retrying (cleared on running). Setup/checkout shells run behind unit.DPKG_REPAIR_PREAMBLE, which runs dpkg --configure -a as root when dpkg reports an interrupted run (a setup killed mid-apt otherwise breaks every later attempt). Per-server start_timeout_s / repo_build_timeout_s override the instance deadlines, outside config_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_ready as its own health phase under the same deadline (a failure is an ordinary _AttemptFailed), and, with health_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 sets unhealthy with health_failing (no restart: the sweep only re-derives a crashed unhealthy), and a passing check returns it to running. Outside config_hash; health_check_command is 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 at trace), every SQL statement (values hidden by hide_parameters), TraceMiddleware logging 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-only GET /api/debug bundle. Every line passes through debug.scrub (and headers through debug.fingerprint): never log a usable credential. Off, none of it is installed and the route 404s. New failure paths should log through a module logger so debug mode picks them up.
  • Group registry (groups/): the groups runtime 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's on_converged hook. The members sit in one AggregateProvider per 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 as list_changed (GroupHub._announce): to the handshake-era connections _Sessions remembers (the SDK Connection, never the per-request ServerSession facade), on their standalone stream, and on a 2026-07-28 caller's subscriptions/listen stream (the bundle serves it with the SDK's ListenHandler, which is also what makes it advertise listChanged to 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 and subscriptions/listen POSTs (by Mcp-Method, trusted only on a non-handshake MCP-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 — all is just a conventional entry with members "*". The registry is validated at write time and again at startup (registry.validate_at_startup fails 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 same enforce() with a synthetic group:<name> pseudo-server: bearer requires a matching group:<name>-scoped (or all) token, and members stricter than the default provider are excluded when the default is none. There is no /s/all — single servers live only under /s/<slug>, groups only under /g/<name>, so all is 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 /s and control-plane bearer auth that gates the sensitive /api routers only when enforcement is on (default auto: when the box is exposed off-host); /api/health* and /api/auth/status stay 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 in policy (server/token visibility, the local-runner permission, member token-scope limits) — routers call those predicates, never re-derive rules. Users hold no passwords: credentials are control tokens bound via Token.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 of config_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.py keeps 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, with primaryServerAction deciding which) and RestartButton (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 bindable busy so 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, which app.css maps 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.

Read the full file on GitHub · 293 lines

Changes

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.

  1. today Changed · +41 lines · +855 tokens per session 9617a0d42d05
  2. 3d ago Changed · +30 lines · +694 tokens per session 142125b6de2d
  3. 4d ago Changed · +10 lines · +235 tokens per session 27e5118e0490
  4. 6d ago Changed · +21 lines · +438 tokens per session 86eeee80f0b5
  5. 13d ago Changed · +2 lines · +54 tokens per session ee79104a36f3
  6. 13d ago Changed · +25 lines · +676 tokens per session 4f3d08378545
  7. 15d ago Changed · +6 lines · +148 tokens per session c81a43d24219
  8. 1mo ago Changed · +13 lines · +257 tokens per session f9cbb1e6d0f1
  9. 2mo ago Changed · +5 lines · +115 tokens per session c2843c9002a4
  10. 2mo ago First seen · 140 lines · 2,626 tokens per session scan A 3c20364bf40d

Subscribe to this mod's changes

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.

Related

mcpelevator CLAUDE.md compared with instructions from other repositories

mcpelevator CLAUDE.md next to 6 instructions from other repositories
ModTypeStarsLast pushLicenceScanTokensWorks with
mcpelevator CLAUDE.mdpacnpal/mcpelevatorInstructions file7todayMITA6,098Claude Code
vscode buildNext.instructions.mdmicrosoft/vscodeInstructions file195kyesterdayMITA9,719GitHub Copilot
next.js AGENTS.mdvercel/next.jsInstructions file143kyesterdayMITA7,461Codex, OpenCode
langchain AGENTS.mdlangchain-ai/langchainInstructions file149kyesterdayMITA4,680Codex, OpenCode
vscode oss-third-party-notices.instructions.mdmicrosoft/vscodeInstructions file195kyesterdayMITA5,516GitHub Copilot
vscode copilot-instructions.mdmicrosoft/vscodeInstructions file195kyesterdayMITA3,093GitHub Copilot
deepseek-harness AGENTS.mddeepseek-ai/deepseek-harnessInstructions file274k2d agoMITA4,313Codex, OpenCode