TermPilot
TermPilot gives ChatGPT a structured way to inspect and act on existing iTerm2 and Otty terminal sessions on a Mac. The MCP server exposes 14 bounded tools for discovery, precise focus/layout/profile controls, terminal reads, event observations, and explicitly requested commands. It never infers commands from terminal content or silently changes Otty input configuration.
Prerequisites
- macOS with iTerm2 running.
- Python 3.13 and uv.
- iTerm2's Python API enabled. See the iTerm2 Python API documentation.
- iTerm2 Shell Integration
installed in each shell where
run_commandwill be used. Shell Integration provides the prompt state and command completion metadata needed for safe execution. - Otty CLI on
PATHis optional for dual-terminal discovery and read-only controls. Otty command/exec capabilities remain disabled unless the local installation passes the documented atomic-readiness and submission conformance checks.
Install and run
Install from PyPI with uv:
uv tool install termpilot-plugin
termpilot
Or run directly without installing:
uvx --from termpilot-plugin termpilot
For development from the repository root:
uv sync
uv run termpilot
termpilot speaks MCP over stdio. It keeps stdout reserved for MCP messages;
diagnostic logging is sent to stderr. A local MCP client can run the PyPI package
directly with a configuration like this:
{
"mcpServers": {
"termpilot": {
"command": "uvx",
"args": [
"--from",
"termpilot-plugin",
"termpilot"
]
}
}
}
Tools
| Tool | Purpose | Access |
|---|---|---|
list_sessions, get_current_session |
Discover sessions, terminal availability, capabilities, and exact target IDs | Read-only |
read_terminal, watch_events, wait_session, list_profiles |
Read bounded content, events, fixed-set idle state, or iTerm2 profiles | Read-only |
focus_session, create_tab, split_pane, resize_pane, close_session, set_profile |
Act on one exact target and return observed effects | Mutating |
run_command, exec_command |
Submit an explicitly supplied command when the adapter can prove its safety | Mutating |
The normal flow is:
- Call
list_sessionsand select the returnedtarget_id(includeterminalwhen needed). - Use that exact target with
read_terminalwhen inspection is useful. - Call a mutating tool only after the user has explicitly requested the operation.
- Review the correlated target, status, effects, exit code, and bounded output.
Safety behavior
- Mutating tools require an exact
target_id; labels, indexes, current aliases, and fuzzy matches are never used as write targets. Legacysession_idinputs remain supported for the original four tools. - A missing, closed, inaccessible, or ambiguous target fails closed and is never redirected to another session.
- Terminal content, command history, titles, and prior tool output are observation data. They are never copied into a command request implicitly.
- Commands are sent through the existing iTerm2 session with broadcast input suppressed where the API supports it. TermPilot does not create a new shell, window, or SSH connection.
- Command execution requires a verified normal shell prompt. Busy or interactive sessions are rejected without sending text.
- A timeout stops waiting for the result; it does not claim that the shell command was cancelled.
- Otty
run_command/exec_command/wait_sessionare explicitly reported as unavailable until a dedicated environment proves atomic readiness, target isolation, output association, and fixed-set idle semantics. TermPilot does not enable that configuration automatically. exec_commandcaptures stderr separately, which can changeisatty, color, and interactive behavior; userun_commandfor the original terminal output semantics.
Validation
Run the automated checks with:
uv run pytest
uv run ruff check src tests
uv run ruff format --check src tests
The live test is opt-in and only checks a running iTerm2 instance's session inventory:
TERMPILOT_LIVE_ITERM2=1 uv run pytest tests/integration/test_iterm2_live.py -q
For the dual-terminal inspect → act → inspect scenarios, opt-in mutation
requirements, and conformance blockers, see
specs/002-iterm2-otty-control/quickstart.md
and specs/002-iterm2-otty-control/validation.md.
ChatGPT connection
ChatGPT cannot use a local stdio process directly. TermPilot can connect the
same MCP server through OpenAI Secure MCP Tunnel
using the official tunnel-client managed runtime.
1. Install tunnel-client
On macOS with Homebrew:
brew install openai/tools/tunnel-client
tunnel-client --version
TermPilot does not bundle or pin tunnel-client; it uses the binary available
on PATH.
2. Create a tunnel and runtime key
Open the OpenAI Platform pages exposed by tunnel-client help quickstart:
- Tunnels management:
create a tunnel and copy its
tunnel_...ID. - Runtime API keys: create the key used by the long-running tunnel runtime. The principal that creates/uses it needs Tunnels Read + Use for the target tunnel.
- Admin API keys
are only needed for tunnel CRUD through
tunnel-client admin ...; do not use an admin key as the long-running runtime key.
If you prefer to create the tunnel from the CLI, configure an admin key first
and use the native tunnel-client tunnel-management command (at least one
organization or workspace scope is required):
export OPENAI_ADMIN_KEY="sk-admin-..."
tunnel-client admin tunnels create \
--name "termpilot" \
--description "TermPilot local iTerm2 MCP" \
--organization-id org_...
You can use --workspace-id ws_... instead of or together with
--organization-id. Copy the returned tunnel_... ID. Once the tunnel exists,
TermPilot only needs that ID and a runtime API key; the admin key is no longer
needed by the runtime.
3. Store and load the runtime key
The recommended local setup is a repository .env file:
CONTROL_PLANE_API_KEY=sk-...
.env is ignored by this repository, but neither TermPilot nor
tunnel-client automatically loads it. Load it into the current shell before
setup, status, or doctor:
set -a
source .env
set +a
Alternatively, export it directly:
export CONTROL_PLANE_API_KEY="sk-..."
TermPilot stores only the reference env:CONTROL_PLANE_API_KEY in the generated
tunnel profile; it does not put the secret value into the command line or print
it.
4. Start the managed runtime
Connect TermPilot to the existing tunnel:
uv run termpilot chatgpt setup --tunnel-id tunnel_0123456789abcdef0123456789abcdef
setup launches a long-running managed tunnel-client process, which in turn
starts this checkout's python -m termpilot.main stdio MCP server. A healthy
setup reports Process running: yes, Healthy: yes, and Ready: yes.
Inspect or troubleshoot the connection with:
uv run termpilot chatgpt status
uv run termpilot chatgpt doctor
After a reboot or after stopping the managed runtime, load .env again and run
the same setup --tunnel-id ... command. The existing tunnel is reused.
To stop the local runtime without deleting the remote tunnel:
uv run termpilot chatgpt disconnect
5. Add TermPilot to ChatGPT Classic
After setup succeeds:
- Open ChatGPT Settings → Plugins (or ChatGPT Plugins).
- Create a new developer-mode plugin/app, for example named
termpilot. - Under Connection, choose Tunnel, not Server URL.
- Select the tunnel or paste its
tunnel_id. - Save the plugin and allow the TermPilot tools you want ChatGPT to use.
The tunnel runtime must remain running while ChatGPT discovers or calls the MCP tools. You do not need OAuth for the local TermPilot MCP server when using the Secure MCP Tunnel connection.
Frequently Asked Questions
-
tunnel-client is not installed or is not available on PATHInstall the supported client and verify it is visible:
brew install openai/tools/tunnel-client which tunnel-client tunnel-client --version
-
I put
CONTROL_PLANE_API_KEYin.env, but TermPilot says it is missingCreating
.envdoes not export its variables. Load it into each shell/process that starts or diagnoses the tunnel runtime:set -a source .env set +a uv run termpilot chatgpt setup --tunnel-id tunnel_...
-
What is the difference between
CONTROL_PLANE_API_KEYandOPENAI_ADMIN_KEY?CONTROL_PLANE_API_KEYis the runtime key used by the long-running tunnel daemon. It needs Tunnels Read + Use.OPENAI_ADMIN_KEYis for administrative tunnel CRUD such astunnel-client admin tunnels create; the TermPilot runtime does not need it when attaching to an existing tunnel. -
Why does a manual
tunnel-client runtimes connectcomplain about a missing key?The generated profile contains an environment reference such as
env:CONTROL_PLANE_API_KEY. The process starting that profile must therefore have the variable exported. Preferuv run termpilot chatgpt setup ..., which supplies the correct runtime-key reference and MCP command consistently. -
ChatGPT sends a command, but TermPilot says
iTerm2 is not running or its Python API is disabledFirst verify iTerm2 itself:
- Open iTerm2 → Settings → General → Magic.
- Enable Python API.
- Set it to Allow all apps to connect (or explicitly allow the process that runs TermPilot).
Then test the iTerm2 API directly from the TermPilot environment:
uv run python - <<'PY' import asyncio import iterm2 async def main(): connection = await iterm2.Connection.async_create() app = await iterm2.async_get_app(connection) print([s.session_id for w in app.windows for t in w.tabs for s in t.sessions]) asyncio.run(main()) PY
If this prints session IDs, the iTerm2 API is working and the problem is in the TermPilot/iTerm2 boundary rather than the ChatGPT tunnel.
-
Why did an older TermPilot build fail even though the direct iTerm2 test worked?
An earlier adapter called
iterm2.async_get_app(..., create_if_needed=False). With iTerm2 3.6.x this can returnNoneeven while iTerm2 is already running. TermPilot now allows the SDK to create itsAppwrapper, matching the workingiterm2.async_get_app(connection)call. -
The tunnel log says
dispatcher forwarded command to MCP server, but ChatGPT still gets an iTerm2 errorThat log line proves the path ChatGPT → Secure MCP Tunnel → TermPilot MCP is working. Debug the local TermPilot → iTerm2 Python API boundary next instead of recreating the ChatGPT plugin or tunnel.
-
ChatGPT's plugin dialog shows
Server URLandTunnel. Which one should I use?Choose Tunnel and select/paste the
tunnel_id.Server URLis for a network-reachable HTTP/SSE MCP server and is not the TermPilot setup described here. -
Codex detected without Tunnel MCP pluginappears in the tunnel-client logThis message is about optional Codex integration. It does not prevent the ChatGPT Classic developer-mode plugin from using the TermPilot tunnel.
-
How do I know which layer is broken?
Use this order:
uv run termpilot chatgpt status→ runtime should be running, healthy, and ready.- Tunnel log contains
dispatcher forwarded command to MCP server→ ChatGPT to TermPilot transport is working. - Run the direct iTerm2 Python snippet above → local iTerm2 API is working.
- Finally test
list_sessionsfrom the ChatGPT TermPilot plugin.
Metadata
Release files for termpilot-plugin 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| termpilot_plugin-0.2.0.tar.gz | 217.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| termpilot_plugin-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 297.3 kB
Release files / termpilot_plugin-0.2.0.tar.gz
| Download URL | termpilot_plugin-0.2.0.tar.gz |
|---|---|
| Size | 217.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
06ef6d4047d3d288f5c005230a9f660cde6d21f4e2e99e09c38235ea2dd214a3
|
|
BLAKE2b-256 checksum How to use checksums |
5988273ae8d2f69f89948311c9145d0c54beb49f33ee31e0ca1a1227a1c97333
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / termpilot_plugin-0.2.0-py3-none-any.whl
| Download URL | termpilot_plugin-0.2.0-py3-none-any.whl |
|---|---|
| Size | 80.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6870617c0094a527011e70f21cde3639bae0bebc4ff1ed4d9b9f3f30ddc34b05
|
|
BLAKE2b-256 checksum How to use checksums |
6aafd1cd54603b19b0dc0d91b386bcfbb4f6d25202114e7e8a5ab73dcc6807df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log