Skip to content

Latest commit

 

History

History
207 lines (166 loc) · 9.54 KB

mcp-server.md

File metadata and controls

207 lines (166 loc) · 9.54 KB

MCP Server

Programa ships an MCP (Model Context Protocol) server so coding agents can drive the app directly: list and switch workspaces, split panes, send text to a terminal, and read the output of a pane they are not running in.

It is a separate binary, programa-mcp, embedded in the app bundle. It talks to the running app over the same v2 JSON control socket the programa CLI uses (see socket-api.md). It is not part of the app process, so it cannot affect terminal rendering or typing latency.

Automatic setup

Agents started inside Programa get the server without any setup:

  • Claude Code. The claude wrapper Programa puts on PATH in its panes passes the bundled programa-mcp to every session as a server named programa, through --mcp-config={"mcpServers":{"programa":{"command":"<bundle>/Contents/Resources/bin/programa-mcp"}}}. Nothing is written to your Claude Code config. A programa server in your own config is replaced by this one for that session, so it always talks to the app the pane belongs to. The wrapper skips this when you pass --mcp-config or --strict-mcp-config yourself, when the bundle has no programa-mcp, or when the installed claude does not support the flag. It also appends a short system prompt telling the session it runs inside Programa and to follow the programa skill, unless you pass --system-prompt, --append-system-prompt, or their -file forms. programa claude install-integration does not register the server, because the wrapper already covers every Claude Code session in a Programa pane.
  • Codex. programa codex install-integration runs codex mcp add programa -- <bundle>/Contents/Resources/bin/programa-mcp after you confirm, alongside its hooks and skill. It leaves an existing programa server that runs something other than programa-mcp unchanged, and programa codex uninstall-integration removes the server only when it still runs programa-mcp. Codex starts MCP servers with a filtered environment, so this server finds the socket through discovery (the default Programa instance) rather than the pane's PROGRAMA_SOCKET_PATH. To point it at a tagged build, add env_vars = ["PROGRAMA_SOCKET_PATH", "PROGRAMA_SOCKET_PASSWORD"] under [mcp_servers.programa] in ~/.codex/config.toml.
  • OpenCode. programa opencode install-integration adds "programa": {"type": "local", "command": ["<bundle>/Contents/Resources/bin/programa-mcp"], "enabled": true} under mcp in ~/.config/opencode/opencode.json (or $OPENCODE_CONFIG_DIR/opencode.json) after you confirm, alongside its plugin and skill, keeping every other key in the file. It leaves an existing programa server that runs something other than a bare programa-mcp (extra settings, enabled: false, another command) unchanged, and programa opencode uninstall-integration removes the entry only when it is still that bare form. It never rewrites opencode.jsonc or an opencode.json that is not strict JSON, since that would drop comments; it prints the entry to add instead. OpenCode merges config files, so a project opencode.json can still override the entry. This path is untested against a live OpenCode.

In password mode the automatic server logs in with the password saved in Settings → Automation, the same one the programa CLI uses.

Manual setup

Point your MCP client at the binary inside the app bundle:

/Applications/Programa.app/Contents/Resources/bin/programa-mcp

For Claude Code, that is:

{
  "mcpServers": {
    "programa": {
      "command": "/Applications/Programa.app/Contents/Resources/bin/programa-mcp"
    }
  }
}

The server finds the running app's socket on its own, using the same rules as the CLI. No arguments or configuration are needed in the common case. To target a specific instance (a tagged dev build, for example), set PROGRAMA_SOCKET_PATH:

{
  "mcpServers": {
    "programa-dev": {
      "command": "/path/to/Programa DEV my-tag.app/Contents/Resources/bin/programa-mcp",
      "env": { "PROGRAMA_SOCKET_PATH": "/tmp/programa-debug-my-tag.sock" }
    }
  }
}

If Settings → Automation uses password-protected socket access, programa-mcp logs in with PROGRAMA_SOCKET_PASSWORD when it is set, and otherwise with the password saved in Settings → Automation. It authenticates with auth.login before sending each tool request and returns an explicit authentication_error when the password is missing or rejected. To use a different password, set it in the server's environment:

{
  "mcpServers": {
    "programa": {
      "command": "/Applications/Programa.app/Contents/Resources/bin/programa-mcp",
      "env": { "PROGRAMA_SOCKET_PASSWORD": "your configured password" }
    }
  }
}

Note that inside a Programa terminal, PROGRAMA_SOCKET_PATH is already set and points at the app you are running in. That is usually what you want, but it means a dev build's server will talk to your production instance unless you override it explicitly.

On startup the server prints the socket it chose to stderr, and says whether that came from an environment override or from discovery:

programa-mcp: control socket /tmp/programa-debug-my-tag.sock (via PROGRAMA_SOCKET_PATH override)

Most MCP clients keep that in a server log. Worth a look if a project you did not write ships its own MCP config: anything that can set this process's environment can choose which socket it talks to, and a substituted socket sees every tool argument and picks what the agent reads back.

Tools

Each tool is named after the socket method it calls, with . replaced by _: surface.read_text becomes surface_read_text, review.comment.add becomes review_comment_add.

The focus_ prefix

Ten tools can pull macOS focus to Programa and raise its window. They are named with a focus_ prefix instead of their method name, so the name alone tells you the tool is disruptive before you read its description:

Tool Socket method
focus_window window.focus
focus_workspace_select workspace.select
focus_workspace_next workspace.next
focus_workspace_previous workspace.previous
focus_workspace_last workspace.last
focus_surface surface.focus
focus_pane pane.focus
focus_pane_last pane.last
focus_review_open review.open
focus_worktree_open worktree.open

Every other tool leaves your focus alone. This is enforced by the app, not by the server: the socket layer only permits focus changes for the methods in focusIntentV2Methods (Sources/TerminalController.swift), so a tool cannot steal focus by accident regardless of what the server asks for. The prefix is there to make the boundary visible to an agent choosing which tool to call.

worktree_create is a deliberate exception. The underlying method accepts a focus argument, but the tool does not expose it, so creating a worktree never moves your focus. Use focus_worktree_open if you want to jump to it.

Leaving focus alone does not make a tool harmless. surface_send_text, surface_send_key and surface_send_paste can type into any terminal pane in any workspace, not only the agent's own, and surface_read_text can read any of them. Allowing surface_send_text or surface_send_key without a prompt in your agent's permission settings is the same as allowing it to run any shell command.

Resources

URI Contents
programa://tree The full window, workspace, pane and surface tree. Start here to find ids.
programa://surface/{surface_id}/text Visible text plus scrollback for one terminal surface.

Add ?lines=n to bound the read: programa://surface/<id>/text?lines=200. Useful when a pane has a large scrollback and you are polling it. Reads are capped at 10,000 lines whatever you ask for, so one call cannot pull an unbounded buffer into an agent's context.

Reading a surface does not focus it, which is the point: an agent in one pane can watch what another agent is doing in a different pane, in a different workspace, or in a different window.

Resources are read on demand. There are no push notifications when a pane's output changes, so poll if you need to follow along.

What is not exposed

Deliberate omissions, not oversights:

  • Debug methods (debug.*). DEBUG builds only, and mostly UI-test hooks that can synthesize keystrokes and activate the app.
  • App chrome (auth.login, settings.open, feedback.*, markdown.open) and app-wide test hooks (app.*).

The authoritative method list is contracts/v2/methods.json, from which Sources/V2CommandCatalog.swift is generated. The tool table that mirrors it is CLI-MCP/ToolCatalog.swift.

Troubleshooting

No response at all. The server holds a session open on stdin. Piping a single request with echo ... | programa-mcp will usually print nothing, because the process reaches end-of-input and exits before the reply is flushed. Real MCP clients keep the pipe open, so this only affects hand testing.

transport_error: Socket not found. Programa is not running, or the server resolved a different instance's socket than you expected. Check PROGRAMA_SOCKET_PATH.

authentication_error. The running app requires socket authentication. Save a password in Settings → Automation, or set PROGRAMA_SOCKET_PASSWORD to it and verify that the value matches.

A tool is missing from tools/list. Confirm the method exists in the running build via the system_capabilities tool. The server and the app can be different versions if you have more than one Programa installed.