Skip to content
Docs menu

tempa mcp: the local MCP proxy

The Claude Code plugin declares one MCP server, tempa, that runs tempa mcp over stdio (through the plugin's launcher). The GitHub Copilot plugin declares the same server as tempa mcp --harness copilot (TEMPA-164; see Copilot), and the Gemini extension and the Antigravity plugin with their own --harness (Gemini CLI, Antigravity). The proxy gives each repo exactly the MCP endpoints of that repo's profile, so one client's context never shows up in another client's repo. How endpoints are registered behind the Orq MCP gateway is in mcp.md.

What it does

  1. Profile. On start it loads the profile of the directory Claude Code runs in (CLAUDE_PROJECT_DIR, else the working directory; for Antigravity see Which repo): the cache while it is younger than sync_interval, else a sync (5 s), falling back to a still-valid cache. Without a profile it starts anyway, with no tools, and says why in its instructions.
  2. Sessions. For each mcp[] endpoint it asks tempa-api POST /v1/mcp/session with the device token and {"endpoint", "repo": "<raw remote or null>", "harness": "<the --harness, default claude-code>"}. The answer {endpoint, url, headers, tools, expires_in} carries the upstream headers (for example the Orq gateway key): the tenant's mcp_credential:<endpoint> secret (auth.md), else for the home tenant only TEMPA_MCP_CREDENTIALS, only while the endpoint's url is on the origin the credential is for (409 otherwise, TEMPA-220); another tenant without its own secret gets 503 for an endpoint that needs authentication. They stay in the proxy's memory only: nothing upstream is ever written to disk or to .mcp.json. A 403 (endpoint not in the profile for this repo) or any other failure skips that endpoint; it is retried after 30 s.
  3. Forwarding. It connects to each endpoint's url over streamable HTTP with the session headers on every request. Only an https url is used (plain http only to this machine, TEMPA-237): the endpoint is skipped otherwise, so its credentials never go over the network unencrypted. A session is renewed a minute before expires_in runs out, and at once when the upstream answers 401 (the request is retried once, the new session asked with "refresh": true). Per-user endpoints (auth: per-user, TEMPA-283, auth.md): the session carries the person's own access token (Authorization: Bearer), in memory only, like every session header; refresh: true makes tempa-api renew it after the server refused it. Until the person signed in there, tempa-api answers 428 with connect_url: the proxy then lists the endpoint's allowlisted tools (or one <endpoint>__connect) with "Not available yet" and the link, and a call answers a tool error the agent shows: "Atlas is not connected for you yet. Open <link> to connect (one click), then try again." After connecting, the next call works (no restart). SessionStart says the same once per session (it asks GET /v1/me/connections, at most 1.5 s, only for a profile with a per-user endpoint), and tempa status and tempa sync list the endpoints still to connect with their link. An older CLI shows the 428's detail, which has the link too, as the reason the endpoint cannot be reached.
  4. Names. Upstream tools are exposed as <endpoint>__<tool>, so Claude Code shows them as mcp__tempa__<endpoint>__<tool>. Only tools on the endpoint's allowlist are listed or callable: the signed profile's tools (null = all), narrowed by the session's tools. Prompts are named the same way. Resources get the name <endpoint>__<name> and the URI tempa://<endpoint>/<url-encoded upstream URI>. Resource templates and the upstreams' list-changed notifications are not forwarded yet; the proxy sends its own when it switches to another repo's profile (Antigravity).
  5. Tempa's own tools. Inside a git checkout the proxy also offers, without an endpoint:
    • work_item (mcp__tempa__work_item): the agent calls it with an issue URL, a key like ABC-123 or #123 when it starts on an issue, and the work board links to it (telemetry.md).
    • work_step (mcp__tempa__work_step): the step the work is in, such as develop.test; its description lists the profile's steps and it refuses unknown ones (config.md).

    The server instructions ask for both. They are allowed by the hook and by the server's MCP rule.

The PreToolUse hook applies the same allowlist to mcp__* calls and denies every other MCP server, so tools from servers a developer added by hand are refused (see hooks.md).

GitHub Copilot

  • Declared by the plugin. The Copilot plugin's mcp.json runs ./bin/tempa-launch mcp --harness copilot. The Copilot CLI and VS Code both start a plugin's MCP servers, so nothing is written into ~/.copilot/mcp-config.json or .vscode/mcp.json.
  • Profile. --harness copilot sends "harness": "copilot" to /v1/mcp/session. The Copilot CLI starts a plugin's MCP server in the plugin's own folder (recorded with 1.0.105 and 0.0.400, TEMPA-167), so the plugin's mcp.json passes the session's directory as TEMPA_PROJECT_DIR (${PWD}, which Copilot expands from its own environment) and the proxy takes the repo from it; a value left unexpanded (VS Code, unverified) falls back to the working directory. Before this, the proxy served the profile of the plugin's folder (no repo: the login's default node) and offered no work_item tool.
  • Names. The proxy exposes the same <endpoint>__<tool> names. Copilot calls them tempa-<endpoint>__<tool> (recorded: tempa-second-brain__list_collections, tempa-work_item), so the local MCP allowlist applies (hooks.md); tempa/… and mcp__tempa__… are read too.
  • Organisation policy. A Copilot plan whose organisation turns off third-party MCP servers blocks the plugin's server: the session warns "Third-party MCP servers are disabled by your organization's Copilot policy" and offers no tempa-* tools (recorded, TEMPA-167). The guardrails do not depend on it: the hooks still run.
  • Managed allowlist. An organisation that restricts MCP servers in Copilot's managed settings (copilot/managed-settings.json in .github-private) must allow Tempa's server, for example "allowedMcpServers": [{ "serverName": "tempa" }]. GitHub calls serverName a convenience rather than a security control (a developer can rename a server); a serverCommand matcher needs the plugin's install path. The full lock-down pack is TEMPA-166.

Codex

TEMPA-146, from Codex's MCP and managed configuration docs (read 2026-11-02).

  • In the user config, not the plugin. The Codex IDE extension loads no plugins, but the CLI, the IDE extension and the desktop app share ~/.codex/config.toml. So a Codex session start (and tempa sync --harness codex) merges Tempa's block into it, next to the developer's own settings:
    # >>> tempa (managed by tempa sync; do not edit this block)
    [mcp_servers.tempa]
    command = '/Users/dev/.tempa/codex-mcp.sh'
    args = ['mcp', '--harness', 'codex']
    # <<< tempa

    The block is the same for every repo, because the proxy reads the profile of the directory Codex starts it in, so renders from different repos never fight over it. The plugin declares no MCP server, since a second tempa would only duplicate this one.

  • Which tempa it runs (TEMPA-195). It must know mcp --harness codex, and the tempa on the PATH may not: ~/.local/bin/tempa (/tempa:install-cli) runs whichever plugin's CLI ran last, possibly an older Claude Code plugin's, and Codex then reports a failed handshake and offers no Tempa tools. So the command is, in this order:
    1. the command a lock-down's MCP identity allows (identity = { command = … } under [mcp_servers.tempa] in /etc/codex/requirements.toml, or %ProgramData%\OpenAI\Codex\requirements.toml), since Codex accepts no other;
    2. the tempa on the PATH when it is the very CLI that renders (an npm install links it);
    3. ~/.tempa/codex-mcp.sh, whose path never changes between releases: it runs the newest installed Codex plugin's launcher (~/.codex/plugins/cache/devleaps/tempa/<version>/bin/tempa-launch, which carries the CLI of the plugin's hooks and finds Node without the developer's PATH), else the CLI that rendered it, the one behind the user-level hooks. A plugin update therefore never leaves the server pointing at a removed version folder.

    tempa doctor shows the command (Codex MCP server) and warns when it is the /tempa:install-cli wrapper or missing.

  • Not yet in the config: the first Codex session writes the block, so the server is there from the second session on, or right away after tempa sync --harness codex. CODEX_HOME is not followed: the block goes to ~/.codex/config.toml.
  • How the CLI starts it (Codex CLI 0.157.1, TEMPA-149). In the session's working directory, so the proxy reads that repo's profile, with a small environment of its own (HOME, LANG, LOGNAME, PATH, SHELL, TERM, TMPDIR, USER, …): TEMPA_HOME and other variables of the shell that started Codex do not reach it, only what [mcp_servers.tempa.env] sets. So a TEMPA_HOME other than ~/.tempa is written into ~/.tempa/codex-mcp.sh (TEMPA-195). Before TEMPA-195 the command was the tempa on the PATH, and an older one there (the Claude Code plugin's CLI behind ~/.local/bin/tempa) exited with a usage error.
  • Names. Codex calls the tools mcp__tempa__<endpoint>__<tool>, as Claude Code does, but with every character but [A-Za-z0-9_] turned into _ (mcp__tempa__second_brain__list_collections). The hook maps the names back to the profile's before the local MCP allowlist applies (hooks.md).
  • Managed allowlist. An administrator who restricts MCP servers in requirements.toml allows Tempa's by name and command (an empty [mcp_servers] table disables every server):
    [mcp_servers.tempa]
    identity = { command = '/usr/local/bin/tempa' }

    codexMcpRequirements(tempaPath) (client/src/render/codex.ts) writes these lines, and the Codex lock-down pack carries them (TEMPA-148). The identity's command must be the one in the developer's config: the render reads it from the system requirements.toml and writes that command (TEMPA-195), so under lock-down tempa is installed at that path on every machine. Requirements delivered only through the MDM payload are not read; there the config names the identity's command only when that tempa, on the PATH, did the render (the managed hooks run it), so deploy the file as well.

  • Unverified (TEMPA-149 could not check it): how Codex compares an identity's command (exact text or resolved path; it needs requirements.toml in a system folder), and which working directory the IDE extension and the desktop app start the server in.

Gemini CLI

TEMPA-155, from Gemini's extensions and configuration docs and its source (packages/core/src/code_assist/admin/mcpUtils.ts, packages/cli/src/config/settings.ts; read 2026-11-04).

  • Declared by the extension. gemini-extension.json runs node "${extensionPath}${/}bin${/}tempa-launch.mjs" mcp --harness gemini-cli with cwd ${workspacePath}, so the proxy asks /v1/mcp/session for Gemini's profile of the repo the session runs in. The Node launcher is the same as the hooks' (one command for macOS, Linux and Windows, harnesses.md). Gemini starts extension servers with only HOME, PATH, TMPDIR and declared settings from the environment; the proxy needs nothing else (~/.tempa). A server named tempa in the developer's own settings.json would win over the extension's.
  • Names. Gemini calls the tools mcp_tempa_<endpoint>__<tool> (it splits at the first _ after mcp_, and tempa has none), so the local MCP allowlist applies (hooks.md).
  • *Locked tenants: mcp.allowed, not `admin.mcp.`.** Gemini's admin MCP settings cannot carry a local server:
    • admin.mcp.config is an allowlist that replaces an allowed server's connection with the admin's url and type and drops command, args, env and cwd (applyAdminAllowlist), so Tempa's stdio proxy would not start under it;
    • admin.mcp.requiredConfig injects servers by url only ("stdio not supported for required servers", applyRequiredServers). Tempa has no remote MCP endpoint for the proxy's tools, so it is not used;
    • mcp.allowed lists server names and is intersected across every settings file, so in the system settings it is the most a developer can have, and it leaves the connection alone.

    So the lock-down pack (tempa lockdown gemini --only-tempa-mcp, TEMPA-157, install.md) sets "mcp": { "allowed": ["tempa"] } (geminiMcpLockdown() in client/src/lockdown/gemini.ts), and Tempa's hook denies tools of every other server when the profile sets allow_external_mcp: false. Both are by name: a server a developer names tempa passes, and the command-line flag --allowed-mcp-server-names replaces the list, so this is advisory like the rest of Gemini's lock-down.

  • Unverified (TEMPA-158): that ${workspacePath} is the session's repo, and that mcp.allowed in the system settings blocks a server the user adds.

Antigravity

TEMPA-181, from Antigravity's MCP, plugin and permissions docs (read 2026-11-05; the TEMPA-176 spike).

  • Declared by the plugin. The plugin's root mcp_config.json runs node ${PLUGIN_ROOT}/bin/tempa-launch.mjs mcp --harness antigravity (${PLUGIN_ROOT} is expanded in args), so the proxy asks /v1/mcp/session for Antigravity's profile. The plugin build checks that the declaration names --harness antigravity and no other harness (a missing --harness would serve Claude Code's profile). Antigravity renames a plugin's servers <plugin>_<server>, so the CLI, the App and the IDE know it as tempa_tempa.
  • Which repo. Antigravity documents no workspace variable for MCP servers (Gemini's ${workspacePath} has no counterpart), and one App window can hold conversations in several folders. The proxy takes, in this order:
    1. the client's MCP roots, when Antigravity declares the roots capability: after the handshake the proxy asks for them (2 s) and serves the first file: root's repo; requests wait for that answer, and when the roots change (notifications/roots/list_changed) the proxy switches to the new repo's profile and tells the client its tools, resources and prompts changed;
    2. the workspace Tempa's hook saw last: the Antigravity hook records workspacePaths[0] at SessionStart and at every prompt (~/.tempa/antigravity-workspace.json), so this is the most recently active conversation's folder, if it still exists;
    3. its working directory.

    tempa doctor says which it used last (Antigravity MCP workspace, from ~/.tempa/antigravity-mcp-workspace.json), and warns when it was the working directory, which may not be the repo.

  • No prompt per call. MCP tools ask by default. permissions.allow in the CLI settings (~/.gemini/antigravity-cli/settings.json) gets mcp(tempa_tempa/*), a JSON list merge next to the developer's own entries (cli.md), since the profile already decides which endpoints and tools the proxy offers. It names Tempa's server only, and the hook still checks every call against the profile's MCP allowlist. Where the App and the IDE keep their permission rules is not documented, so Tempa writes nothing there: a developer allows the tempa_tempa tools once, by always allowing the first call.
  • Names. Recorded on agy -p 1.5.2 (TEMPA-185): the model calls every MCP tool through Antigravity's own call_mcp_tool, and the hook gets {"ServerName": "tempa_tempa", "ToolName": "second-brain__list_collections", "Arguments": {}}: the endpoint is not sanitised. The hook reads the server and tool from there (and also mcp__tempa_tempa__<endpoint>__<tool>, tempa_tempa/<endpoint>__<tool> and mcp_tempa_tempa_<endpoint>__<tool> as a tool name), and maps a sanitised endpoint or tool (second_brain, the way Codex reports second-brain) back to the profile's before the MCP allowlist applies (profileMcp). A name that matches no endpoint of the profile is not mapped, so a mapping never reaches an endpoint or tool the profile does not allow (hooks.md). mcp(tempa_tempa/*) in permissions.allow does not skip the hook: Tempa's PreToolUse still ran, and its deny held, for a call that entry allowed.
  • Locked tenants on Gemini Enterprise. Admins allowlist MCP servers in the Google Cloud console (Gemini Enterprise, Settings, AI developer tools, "Allowed server configuration"), as JSON entries with an id and a url; an unlisted server shows "Unauthorized / Blocked by Admin Policy". Tempa's entry names tempa_tempa as its id: tempa lockdown antigravity writes it into gemini-enterprise-mcp.json, with the tenant's other servers from --allow-mcp id=url (TEMPA-183, install.md). But Tempa's server is a local stdio process with no URL, and whether the allowlist can admit one (and what its url would be) is not documented: until a Gemini Enterprise tenant checks it (TEMPA-208), a tenant that turns the allowlist on may lose Tempa's MCP endpoints in Antigravity (the hooks keep working).
  • Checked on agy 1.5.2 (TEMPA-185): the server starts as tempa_tempa and offers the profile's tools to the model (second-brain__list_collections); the CLI sends MCP roots, and the proxy served the conversation's folder from them (tempa doctor: "from the MCP roots Antigravity sent"). Still open (TEMPA-208): the server's working directory; that mcp(tempa_tempa/*) stops the prompts (Tempa's own offline rules denied every MCP call first, because the dev control plane refused /v1/decide); notifications/roots/list_changed and tools/list_changed; one server per App window or per conversation; whether the Gemini Enterprise allowlist can admit a stdio server.

Logs

The proxy writes problems (an endpoint it cannot reach, a failed list) to stderr, which Claude Code keeps in its MCP logs (claude --debug or /mcp). stdout carries only MCP messages.