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
- 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 thansync_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. - Sessions. For each
mcp[]endpoint it asks tempa-apiPOST /v1/mcp/sessionwith 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'smcp_credential:<endpoint>secret (auth.md), else for the home tenant onlyTEMPA_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. - Forwarding. It connects to each endpoint's
urlover streamable HTTP with the session headers on every request. Only anhttpsurl 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 beforeexpires_inruns 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: truemakes tempa-api renew it after the server refused it. Until the person signed in there, tempa-api answers 428 withconnect_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 asksGET /v1/me/connections, at most 1.5 s, only for a profile with a per-user endpoint), andtempa statusandtempa synclist the endpoints still to connect with their link. An older CLI shows the 428'sdetail, which has the link too, as the reason the endpoint cannot be reached. - Names. Upstream tools are exposed as
<endpoint>__<tool>, so Claude Code shows them asmcp__tempa__<endpoint>__<tool>. Only tools on the endpoint's allowlist are listed or callable: the signed profile'stools(null = all), narrowed by the session'stools. Prompts are named the same way. Resources get the name<endpoint>__<name>and the URItempa://<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). - 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 likeABC-123or#123when 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 asdevelop.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.jsonruns./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.jsonor.vscode/mcp.json. - Profile.
--harness copilotsends"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'smcp.jsonpasses the session's directory asTEMPA_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 nowork_itemtool. - Names. The proxy exposes the same
<endpoint>__<tool>names. Copilot calls themtempa-<endpoint>__<tool>(recorded:tempa-second-brain__list_collections,tempa-work_item), so the local MCP allowlist applies (hooks.md);tempa/…andmcp__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.jsonin.github-private) must allow Tempa's server, for example"allowedMcpServers": [{ "serverName": "tempa" }]. GitHub callsserverNamea convenience rather than a security control (a developer can rename a server); aserverCommandmatcher 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 (andtempa 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'] # <<< tempaThe 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
tempawould only duplicate this one. - Which tempa it runs (TEMPA-195). It must know
mcp --harness codex, and thetempaon 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:- 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; - the
tempaon the PATH when it is the very CLI that renders (an npm install links it); ~/.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 doctorshows the command (Codex MCP server) and warns when it is the/tempa:install-cliwrapper or missing. - the command a lock-down's MCP identity allows (
- 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_HOMEis 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_HOMEand other variables of the shell that started Codex do not reach it, only what[mcp_servers.tempa.env]sets. So aTEMPA_HOMEother than~/.tempais written into~/.tempa/codex-mcp.sh(TEMPA-195). Before TEMPA-195 the command was thetempaon 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.tomlallows 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 systemrequirements.tomland writes that command (TEMPA-195), so under lock-downtempais 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 thattempa, 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.tomlin 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.jsonrunsnode "${extensionPath}${/}bin${/}tempa-launch.mjs" mcp --harness gemini-cliwithcwd${workspacePath}, so the proxy asks/v1/mcp/sessionfor 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 onlyHOME,PATH,TMPDIRand declared settings from the environment; the proxy needs nothing else (~/.tempa). A server namedtempain the developer's ownsettings.jsonwould win over the extension's. - Names. Gemini calls the tools
mcp_tempa_<endpoint>__<tool>(it splits at the first_aftermcp_, andtempahas 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.configis an allowlist that replaces an allowed server's connection with the admin'surlandtypeand dropscommand,args,envandcwd(applyAdminAllowlist), so Tempa's stdio proxy would not start under it;admin.mcp.requiredConfiginjects servers byurlonly ("stdio not supported for required servers",applyRequiredServers). Tempa has no remote MCP endpoint for the proxy's tools, so it is not used;mcp.allowedlists 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()inclient/src/lockdown/gemini.ts), and Tempa's hook denies tools of every other server when the profile setsallow_external_mcp: false. Both are by name: a server a developer namestempapasses, and the command-line flag--allowed-mcp-server-namesreplaces 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 thatmcp.allowedin 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.jsonrunsnode ${PLUGIN_ROOT}/bin/tempa-launch.mjs mcp --harness antigravity(${PLUGIN_ROOT}is expanded inargs), so the proxy asks/v1/mcp/sessionfor Antigravity's profile. The plugin build checks that the declaration names--harness antigravityand no other harness (a missing--harnesswould serve Claude Code's profile). Antigravity renames a plugin's servers<plugin>_<server>, so the CLI, the App and the IDE know it astempa_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:- the client's MCP roots, when Antigravity declares the
rootscapability: after the handshake the proxy asks for them (2 s) and serves the firstfile: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; - 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; - its working directory.
tempa doctorsays 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. - the client's MCP roots, when Antigravity declares the
- No prompt per call. MCP tools ask by default.
permissions.allowin the CLI settings (~/.gemini/antigravity-cli/settings.json) getsmcp(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 thetempa_tempatools once, by always allowing the first call. - Names. Recorded on
agy -p1.5.2 (TEMPA-185): the model calls every MCP tool through Antigravity's owncall_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 alsomcp__tempa_tempa__<endpoint>__<tool>,tempa_tempa/<endpoint>__<tool>andmcp_tempa_tempa_<endpoint>__<tool>as a tool name), and maps a sanitised endpoint or tool (second_brain, the way Codex reportssecond-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/*)inpermissions.allowdoes 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
idand aurl; an unlisted server shows "Unauthorized / Blocked by Admin Policy". Tempa's entry namestempa_tempaas itsid:tempa lockdown antigravitywrites it intogemini-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 itsurlwould 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
agy1.5.2 (TEMPA-185): the server starts astempa_tempaand 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; thatmcp(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_changedandtools/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.