TophatMCP
by B-A-M-N
README.md
# TophatMCP
TophatMCP is an **authenticated OpenAI-compatible inference gateway** backed by an
**async job broker** that bridges a coding agent to a ChatGPT MCP worker.
A coding agent talks to TophatMCP exactly as if it were any OpenAI-compatible
provider (`POST /v1/chat/completions`). TophatMCP enqueues the request as an
`InferenceJob` and **blocks the HTTP request** until a ChatGPT worker — connected
over a tunnel-presented `/mcp` surface — claims the job and submits a result.
```mermaid
flowchart TD
A[Coding Agent e.g. Qwen Code] -- "POST /v1/chat/completions" --> B(TophatMCP Gateway)
B -- "enqueue InferenceJob (blocks HTTP)" --> J[(Job Broker / SQLite)]
J -- "pending claim resolves atomically" --> C{ChatGPT MCP Worker}
C -- "submit_inference_result" --> J
J -- "resolve waiter / HTTP 200" --> A
```
TophatMCP never executes the coding agent's tools. It merely preserves the protocol
that lets the coding agent execute them locally and feed results back in.
## Why a job broker (not a synchronous proxy)
The OpenAI Secure MCP tunnel presents TophatMCP's own `/mcp` surface **to**
ChatGPT — ChatGPT dials *in*. A synchronous `/v1` call therefore cannot traverse
the tunnel by calling an outbound function. The rendezvous is inverted into an
**async job queue**:
1. The coding agent hits `POST /v1/chat/completions`. TophatMCP enqueues an
`InferenceJob` and blocks the HTTP request.
2. The ChatGPT worker (via tunnel → TopHat `/mcp`) calls `claim_inference_job`.
The MCP call stays **pending** (bounded long-poll, capped at 30 s) until a job
arrives, then resolves with the job payload (messages + tool defs).
3. ChatGPT reasons, then calls `submit_inference_result(job_id, …)`. TophatMCP
converts the result into exact OpenAI `tool_calls` / content and resolves the
waiting HTTP request.
4. The loop continues: the agent executes tools locally, sends the next `/v1`
request → new job → ChatGPT claims → submits.
Both the `/v1` handler and the MCP worker tools talk to **one shared Job Broker**
(durable SQLite job state). There is no sync fetch to a WebUI — ChatGPT is reached
*only* through the tunnel-presented MCP server.
## Firefox extension worker ownership
The supported browser adapter is a user-operated Firefox extension connected to
the actuator through Native Messaging. A toolbar click on a ChatGPT tab is an
explicit worker-selection action: it selects that tab/profile as the only
browser worker allowed to receive inference prompts. This avoids silently
choosing a personal ChatGPT conversation.
More than one Firefox profile may have the extension connected at the same
time. The actuator keeps a durable selected-installation record and routes
`worker.ensure`, `turn.execute`, and `turn.stop` **only** to that selected
profile. Selecting a worker in another profile explicitly transfers ownership
and revokes the prior profile; disconnecting the selected profile leaves the
worker offline rather than falling through to a different personal browser.
This makes it practical to keep separate work, test, and personal profiles
connected without accidentally sending a coding request to the wrong tab.
After the initial selection, TopHat persists both the selected extension
installation and the selected conversation URL. It can reopen that thread when
the same Firefox profile reconnects. The current implementation is a
Linux/Firefox private-beta adapter: do not treat it as a public service or a
replacement for an official API. Each user must operate their own logged-in
account, connector authorization, and browser session.
## Waking a ChatGPT worker
A connector exposes tools to a ChatGPT turn; it does not wake a dormant turn.
For unattended use, run a local controller (PTY or browser driver) that long-polls
`GET /internal/jobs/wait?model=<alias>`, sends one exact-job worker prompt when it
receives a queued notice, and keeps that browser turn owned until the broker reaches
`completed`, `failed`, or `cancelled`. Prompt acceptance is not job completion. The
endpoint is loopback-only and can additionally require `TOPHAT_INTERNAL_TOKEN`.
Each wake prompt tells ChatGPT to claim one exact `expected_job_id`, so a stale or
duplicate wake cannot consume another queued request. A lease is authenticated by
`worker_id + job_id + lease_token`, so claim and submit may use separate transient
MCP connections. Browser liveness renews a short lease, always capped by the hard
job deadline; a retry is refused when too little deadline budget remains.
The actuator is a separate process: `npm run dev:actuator` (development) or
`npm run start:actuator` after building. In `extension` mode it owns queue wakes
and retries; the Firefox extension owns narrowly scoped ChatGPT-tab interaction.
`playwright` remains a development fallback. `tophat on` starts the actuator only
in `job-broker` mode and reports inference as ready only after a browser worker
is connected and selected.
## MCP worker tools
- `claim_inference_job` — out: `idle{retry_after_ms}` | `assigned{job_id, lease_token, lease_expires_at, request}`.
- `submit_inference_result` — in: `job_id`, `lease_token`, result payload.
- `fail_inference_job` — in: `job_id`, `lease_token`, error.
- `heartbeat_inference_worker` — keep-alive.
`submit`/`fail` **require** `job_id` + `lease_token`, preventing stale, duplicate,
cross-worker, or corrupt submissions (conditional `UPDATE … WHERE lease_token_hash=?
AND lease_expires_at>? AND deadline_at>?` — 0 rows affected = race lost).
## Run it
```bash
cp .env.example .env
npm install
npm run build # tsc
```
Mock backend (in-process scripted worker, no tunnel, no ChatGPT):
```bash
TOPHAT_BACKEND=mock npm run dev:http
```
Real backend (ChatGPT over tunnel):
```bash
# set TOPHAT_BACKEND=job-broker (default) and aliases in .env, then:
/home/bamn/bin/tophat on # boots gateway (tmux: tophat) + tunnel (tmux: tophat-tunnel)
```
## Launch with `tophat` (one command)
`/home/bamn/bin/tophat` is a launch controller that boots the stack in tmux and
gates the tunnel daemon on gateway readiness:
```bash
tophat on # gateway (tmux: tophat) + OpenAI tunnel (tmux: tophat-tunnel), backend from .env
tophat mock # gateway in mock mode (no tunnel)
tophat status # gateway /health, tunnel readiness, ChatGPT discovery check
tophat off # tears both daemons down
tophat logs # attach to the gateway tmux session
```
`tophat on` prints the inference proxy endpoint + a curl snippet:
```text
=== inference proxy (point your agent here) ===
base_url: http://127.0.0.1:6767/v1
curl http://127.0.0.1:6767/v1/chat/completions \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${TOPHAT_BEARER_TOKEN}" \
-d '{"model":"chatgpt-5.6-sol-high","messages":[{"role":"user","content":"hi"}],"stream":false}'
```
The tunnel daemon needs `CONTROL_PLANE_API_KEY` (an OpenAI *organization* runtime
key from `platform.openai.com/settings/organization/api-keys`) in the environment.
The tunnel profile (`~/.config/tunnel-client/tophat_mcp.yaml`) references it via
`api_key: "env:CONTROL_PLANE_API_KEY"` and **never contains the credential itself**.
The key must own (or have access to) the configured `tunnel_id`, in the same
OpenAI organization/project context. For multi-org accounts, also set
`CONTROL_PLANE_ORGANIZATION_ID`. Export it before `tophat on`:
```bash
export CONTROL_PLANE_API_KEY=<org runtime key>
# export CONTROL_PLANE_ORGANIZATION_ID=<org-id> # only for multi-org accounts
tophat on
```
The launcher passes the key to the tunnel session through tmux's environment
mechanism, never as a command-line argument.
Create/verify the ChatGPT connector at `chatgpt.com/#settings/Connectors` while the
tunnel daemon is running.
## OpenAI-compatible surface
```json
{
"base_url": "http://127.0.0.1:6767/v1",
"api_key": "<TOPHAT_BEARER_TOKEN, unset in mock mode>",
"model": "chatgpt-5.6-sol-high"
}
```
Endpoints:
```text
POST /v1/chat/completions # stream: true → SSE; false → single JSON
GET /v1/models
GET /health
GET /ready?model=… # browser-actuator admission readiness
```
Tool-call round trip (the gateway does not execute tools):
```mermaid
sequenceDiagram
participant Agent as Coding Agent
participant Tophat as TophatMCP (Job Broker)
participant Worker as ChatGPT MCP Worker
Agent->>Tophat: POST /v1 (tools declared)
Tophat->>Tophat: enqueue InferenceJob, block HTTP
Worker->>Tophat: claim_inference_job (pending)
Tophat-->>Worker: assigned{request + tool defs}
Worker->>Tophat: submit_inference_result(tool_calls)
Tophat-->>Agent: HTTP 200 OpenAI tool_calls
Note over Agent: Executes tools locally
Agent->>Tophat: POST /v1 (role: tool result)
Tophat->>Worker: claim resolves with next job
Worker->>Tophat: submit_inference_result(final text)
Tophat-->>Agent: HTTP 200 final content
```
## Authentication
Two modes (`TOPHAT_AUTH_MODE`):
- `oauth` (default): MCP SDK OAuth with an Owner-password approval page. The OpenAI
surface is gated by `TOPHAT_BEARER_TOKEN` (constant-time compare).
- `tunnel`: OAuth disabled on `/mcp`; access is delegated to a reverse tunnel and
guarded by `TOPHAT_TUNNEL_TOKEN`. `HOST` must be a loopback address.
## Tests
`npm run test:broker` runs `src/test-job-broker.ts` — self-contained broker
integration tests covering atomic exact-job claims, deadline-capped leases, hard
deadline enforcement, typed waiter rejection, cancellation, tool-call validation,
and startup recovery. `npm run test:actuator` and the extension-driver suites cover
browser-turn ownership, bounded wake retries, and extension bridge routing.
## Production requirements
- Keep the HTTP server bound to localhost unless remote access is intentional.
- Require authentication for remote access.
- Treat the tunnel token / control-plane key as credentials; never log them.
- Maintain a separate MCP session per worker connection; idle sessions are reaped.
- A browser wake remains owned until the authoritative broker job is terminal; do
not infer completion from DOM activity or prompt acceptance.
- Hard job deadlines win over leases. Retries require a configured minimum remaining
budget and terminally fail once their wake budget is exhausted.
- Submit/fail are guarded by lease + deadline; queued/leased rows are recovered to
`failed(server_restarted)` on startup (no durable continuation advertised until the
HTTP client has a resume protocol).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues