Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .codex/config.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# Project-local Codex config. Applies to sessions started from this repo, the
# same scoping .mcp.json gives Claude Code — no flag required.
#
# The token is NOT stored here. It is read at spawn time from the gitignored
# .env, so this file carries no credential and is safe to commit.
# The token is NOT stored here. Gullet resolves the global token file (or the
# gitignored .env compatibility fallback), so this file is safe to commit.

[mcp_servers.tabglutton]
command = "bash"
args = [
"-c",
"export TABGLUTTON_TOKEN=\"$(grep -m1 '^TABGLUTTON_TOKEN=' .env | cut -d= -f2-)\"; exec bun run ./gullet/gullet.ts",
]
command = "bun"
args = ["run", "./gullet/gullet.ts"]
startup_timeout_sec = 30
# A first tool call can legitimately hold ~90s: up to 45s waiting for the
# browser to dial in (BRIDGE_CONNECT_WAIT_MS) and up to 45s for the browser to
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Package
run: bun run package

- name: Package Gullet
run: bun run package:gullet

- name: Upload extension zip
uses: actions/upload-artifact@v7
with:
Expand Down
7 changes: 2 additions & 5 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
{
"mcpServers": {
"tabglutton": {
"command": "bash",
"args": [
"-c",
"export TABGLUTTON_TOKEN=\"$(grep -m1 '^TABGLUTTON_TOKEN=' .env | cut -d= -f2-)\"; exec bun run ./gullet/gullet.ts"
]
"command": "bun",
"args": ["run", "./gullet/gullet.ts"]
}
}
}
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ This is a Bun-powered TypeScript WebExtension for Zen Browser, Firefox, and Chro

## Gullet (agent bridge sidecar)

`gullet/` is a sibling package, not part of the extension bundle: an MCP server over stdio on one side, a loopback WebSocket hub on the other. It shares `src/bridge-protocol.ts` with the extension so both ends are typechecked against one definition, has its own `gullet/tsconfig.json` (`lib: ES2022`, `types: bun-types` — no DOM, no `browser`), and has zero dependencies. It is excluded from the extension `tsconfig.json`'s `include`, so it never reaches `dist-*`. Setup and troubleshooting live in `gullet/README.md`.
`gullet/` is a sibling package, not part of the extension bundle: an MCP server over stdio on one side, a loopback WebSocket hub on the other. It shares `src/bridge-protocol.ts` with the extension so both ends are typechecked against one definition; its agent-only listing renderer is `gullet/src/tabs-view.ts`, not extension source. It has its own `gullet/tsconfig.json` (`lib: ES2022`, `types: bun-types` — no DOM, no `browser`) and zero dependencies. It is excluded from the extension `tsconfig.json`'s `include`, so it never reaches `dist-*`; `bun run build:gullet` instead bundles the executable and shared modules for the `tabglutton-gullet` npm package. Setup and troubleshooting live in `gullet/README.md`.

Global Gullet settings live at `${XDG_CONFIG_HOME:-$HOME/.config}/tabglutton/config.json`, with the token in a separate `0600` file by default. The config is deliberately safe to commit: an inline `"token"` key is rejected even when a CLI or environment token would otherwise win. Keep the additive token precedence (`--token` → env → `./.env` → `tokenCommand` → `tokenFile` → default file). `tokenCommand` is bounded and lazy; its timeout/nonzero error, including stderr, goes through `Supervisor.fault()`, and the supervisor retries with backoff so unlocking a secret manager heals the existing MCP session. Do not move command execution ahead of that recoverable startup path.

Tests that stand up a real socket bind to port 0 for an ephemeral port. Diagnostics in gullet go to **stderr only** — stdout is the MCP transport and a stray `console.log` corrupts the session.

Expand All @@ -57,6 +59,7 @@ The election must **settle, or say why**. `main` awaits `backend.start()` before

- `bun install`: install dependencies.
- `bun run build`: build both `dist-firefox/` and `dist-chrome/`.
- `bun run build:gullet`: bundle the publishable `tabglutton-gullet` executable.
- `bun run build:firefox` / `build:chrome`: single-target builds.
- `bun run typecheck`: typecheck the extension (`typecheck:ext`, `tsconfig.test.json` over `src/` + `tests/`) then the sidecar (`typecheck:gullet`).
- `bun run test`: run the Bun test suite under `tests/` and `gullet/tests/`.
Expand All @@ -67,6 +70,7 @@ The election must **settle, or say why**. `main` awaits `backend.start()` before
- `bun run start:firefox`: build firefox and launch regular Firefox with a persistent dev profile.
- `bun run start:chrome`: build chrome and launch Chromium via `web-ext --target=chromium`.
- `bun run package`: produce both `tabglutton-firefox-<version>.zip` and `tabglutton-chrome-<version>.zip` in `web-ext-artifacts/`.
- `bun run package:gullet`: build and dry-run the publishable `tabglutton-gullet` package.

## Coding Style & Naming Conventions

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ throughout — `j`/`k` to move, `space` to toggle, `d` to devour, `x` to close,

This is the part that isn't like other tab extensions.

Tabglutton ships **Gullet**, a local MCP server. Turn the bridge on in settings, point
Claude Code or Codex at it, and your agent can work your actual open tabs:
Tabglutton ships **Gullet**, a local MCP server. Turn the bridge on in settings, run its
one-time token setup command, and point Claude Code or Codex at
`bunx tabglutton-gullet`; your agent can then work your actual open tabs:

| Tool | What it does |
| ------------ | ------------------------------------------------------- |
Expand Down
8 changes: 5 additions & 3 deletions docs/BRIDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,9 +678,11 @@ Strategy, in order:
- No new host permissions (`*://*/*` already covers the clipper).
- `gullet/` is a sibling package with its own `tsconfig.json`, sharing
`src/bridge-protocol.ts` and the repo's check pipeline (`bun run typecheck` covers both
projects; `bun test` picks up `gullet/tests/`). It has no dependencies of its own —
Bun's built-in WebSocket server and a hand-rolled tools-only MCP server are enough, so
`bun run gullet/gullet.ts` works with nothing installed.
projects; `bun test` picks up `gullet/tests/`). Its agent-only tab renderer now lives in
`gullet/src/tabs-view.ts`, so it no longer compiles into either extension target. The
package has no dependencies of its own — Bun's built-in WebSocket server and a hand-rolled
tools-only MCP server are enough — and bundles those shared sources into the published
`tabglutton-gullet` executable for `bunx` / `npx` use without a checkout.
- `build.ts` gains nothing target-specific: the bridge module is shared source; the only
Chrome divergence is the keepalive note above. `gullet/` is outside the extension
tsconfig's `include`, so it never lands in `dist-*`.
Expand Down
8 changes: 4 additions & 4 deletions docs/LAUNCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,15 @@ Several agent sessions can share one browser — the first process to bind the p
the hub and later ones attach as peers and proxy through it, because nothing guarantees
one sidecar per session (a single `codex` process was observed spawning two).

Setup is a token from the extension's settings page and one `.mcp.json` block:
Setup is a token from the extension's settings page, written once with its copyable setup
command, and one MCP entry:

```json
{
"mcpServers": {
"tabglutton": {
"command": "bun",
"args": ["run", "/path/to/tabglutton/gullet/gullet.ts"],
"env": { "TABGLUTTON_TOKEN": "<from the settings page>" }
"command": "bunx",
"args": ["tabglutton-gullet"]
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions gullet/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Michael Simon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
104 changes: 81 additions & 23 deletions gullet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ way to a coding agent. One side is an **MCP server over stdio**, spawned by what
harness you use; the other is a **WebSocket server on loopback** that browsers running
Tabglutton dial into.

Architecture, trust boundary, and phasing live in [`../docs/BRIDGE.md`](../docs/BRIDGE.md). This file
is the setup guide.
Architecture, trust boundary, and phasing live in
[`docs/BRIDGE.md`](https://github.com/mlsimon734/tabglutton/blob/main/docs/BRIDGE.md).
This file is the setup guide.

```
Claude Code ──MCP (stdio)──► Gullet ──WebSocket (automatic loopback port)──► browsers
```

Zero dependencies: it runs on Bun's built-ins alone, so there is nothing to install beyond
having the repo checked out.
Zero dependencies: it runs on Bun's built-ins alone. Install Bun, then let your MCP client
launch the published package with `bunx tabglutton-gullet`; no checkout is required.

**Two names, one product.** "Gullet" is the internal name for this sidecar; everything a
user or an agent sees says **Tabglutton**. So the MCP server registers as `tabglutton`, the
Expand All @@ -26,29 +27,49 @@ tools appear under that namespace, and the token is `TABGLUTTON_TOKEN`. `GULLET_
then **Generate** a token and copy it. The bridge is off until you do this, and no
socket is opened while it is off.

2. **Register Gullet with your agent.** The settings page renders a ready-made config with
your token filled in — _Copy config_ and paste it into `.mcp.json` (or
`~/.claude.json`), replacing the placeholder path with wherever you cloned this repo:
2. **Write the global token file.** The settings page renders a shell command with your
token filled in. _Copy setup command_ and run it once. It creates the directory privately
and writes the secret to `~/.config/tabglutton/token` with mode `0600` (under
`$XDG_CONFIG_HOME` instead when set). The token does not go into an MCP config.

3. **Register Gullet with your agent.** Pick the shape your client accepts.

Claude Code, for every project:

```sh
claude mcp add --scope user tabglutton -- bunx tabglutton-gullet
```

Codex CLI and Codex desktop, in `~/.codex/config.toml`:

```toml
[mcp_servers.tabglutton]
command = "bunx"
args = ["tabglutton-gullet"]
startup_timeout_sec = 30
tool_timeout_sec = 120
```

Claude Desktop/Cowork and clients that accept the standard JSON shape:

```json
{
"mcpServers": {
"tabglutton": {
"command": "bun",
"args": ["run", "/path/to/tabglutton/gullet/gullet.ts"],
"env": { "TABGLUTTON_TOKEN": "<token from the settings page>" }
}
"tabglutton": { "command": "bunx", "args": ["tabglutton-gullet"] }
}
}
```

For Claude Code specifically:
`npx -y tabglutton-gullet` is also supported when a client already standardizes on
`npx`; the package still requires Bun because its executable uses Bun's runtime.

To run an unpublished checkout while developing, replace the command with:

```sh
claude mcp add tabglutton --env TABGLUTTON_TOKEN=<token> -- bun run /path/to/tabglutton/gullet/gullet.ts
bun run /path/to/tabglutton/gullet/gullet.ts
```

3. **Start a session.** The agent spawns Gullet, Gullet elects an approved port, and the
4. **Start a session.** The agent spawns Gullet, Gullet elects an approved port, and the
extension's reconnect loop finds it — typically within a few seconds (it rotates probes
every 3s while the browser's extension page is awake), worst case ~30 seconds (the alarm
cadence, when the page had suspended). The toolbar badge shows a terracotta dot while the
Expand All @@ -61,10 +82,44 @@ once — a Zen window and a Chrome profile, say — and each tool call picks one

## Configuration

| Flag | Env | Default | Notes |
| --------- | ------------------ | --------- | ---------------------------------------------------------------------------------- |
| `--port` | `TABGLUTTON_PORT` | automatic | Use `auto` or omit it; a number pins one port and must match fixed browser mode. |
| `--token` | `TABGLUTTON_TOKEN` | — | Required. Prefer the env var: process arguments are readable by other local users. |
Gullet reads settings from `${XDG_CONFIG_HOME:-$HOME/.config}/tabglutton/config.json`.
The file is safe to keep in a dotfiles repository because Gullet rejects an inline
`"token"` key; it may contain only settings and a pointer to the secret.

```jsonc
{
"port": "auto",
"tokenFile": "token",
}
```

`tokenFile` defaults to `${XDG_CONFIG_HOME:-$HOME/.config}/tabglutton/token`. Relative
paths are resolved from the directory containing `config.json`. To read from a secret
manager instead, use `tokenCommand` in place of `tokenFile`:

```jsonc
{
"port": "auto",
"tokenCommand": "op read op://Private/Tabglutton/token",
}
```

The command runs through `sh` from the config directory and its trimmed stdout is the
token. Gullet bounds each attempt at five seconds. A timeout or nonzero exit is reported
to the MCP client with stderr attached while the process keeps retrying with backoff;
unlocking the secret manager heals the same MCP session.

Resolution is additive, so existing setups keep working. The first configured token wins:

```text
--token -> TABGLUTTON_TOKEN / GULLET_TOKEN -> ./.env
-> tokenCommand -> tokenFile -> the default global token file
```

Port selection uses `--port`, then `TABGLUTTON_PORT` / `GULLET_PORT`, then the global
config, then automatic discovery. A fixed number must match the browser's fixed-port
setting. Process arguments are visible to other local users, so `--token` is best kept for
temporary diagnosis; prefer the global file, a secret-manager command, or the environment.

Automatic mode uses the ordered candidate set shared with the extension: `4589`, `20317`,
`17483`, `27613`, and `24193`. It discovers an existing same-token hub before binding, so
Expand All @@ -85,7 +140,9 @@ Diagnostics go to **stderr**; stdout is the MCP transport and carries nothing el

Deliberately absent: navigate, click, type, evaluate. The agent can read what you already
chose to open, file it, and clean up — it cannot act as you. Adding anything richer means
revisiting the prompt-injection posture in `../docs/BRIDGE.md` first.
revisiting the prompt-injection posture in
[`docs/BRIDGE.md`](https://github.com/mlsimon734/tabglutton/blob/main/docs/BRIDGE.md)
first.

`tabs_load` is the one tool that acts on a page rather than observing it, so it has its own
switch — **Agent bridge → "Let agents load unloaded tabs"** in Tabglutton's settings — and
Expand Down Expand Up @@ -164,7 +221,7 @@ pass that number with `--port`.
by hand to watch it:

```sh
TABGLUTTON_TOKEN=<token> bun run gullet/gullet.ts
TABGLUTTON_TOKEN=<token> bunx tabglutton-gullet
```

Then poke the selected socket directly (stderr prints the chosen port; `4589` is shown here):
Expand All @@ -184,5 +241,6 @@ bun run typecheck:gullet # from the repo root
bun test # protocol, config, selection, MCP, and a live-socket hub test
```

The wire contract lives in [`../src/bridge-protocol.ts`](../src/bridge-protocol.ts) and is
imported by both halves, so extension and sidecar are typechecked against one definition.
The wire contract lives in
[`src/bridge-protocol.ts`](https://github.com/mlsimon734/tabglutton/blob/main/src/bridge-protocol.ts)
and is imported by both halves, so extension and sidecar are typechecked against one definition.
29 changes: 26 additions & 3 deletions gullet/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
{
"name": "gullet",
"name": "tabglutton-gullet",
"version": "0.1.0",
"private": true,
"description": "Tabglutton's agent bridge: an MCP server over stdio, a WebSocket hub on loopback.",
"keywords": [
"browser",
"mcp",
"obsidian",
"tabglutton",
"tabs"
],
"homepage": "https://github.com/mlsimon734/tabglutton/tree/main/gullet#readme",
"bugs": "https://github.com/mlsimon734/tabglutton/issues",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/mlsimon734/tabglutton.git",
"directory": "gullet"
},
"bin": {
"gullet": "./gullet.ts"
"tabglutton-gullet": "./dist/gullet.js"
},
"files": [
"dist",
"README.md"
],
"type": "module",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun build ./gullet.ts --target=bun --outfile=dist/gullet.js",
"prepack": "bun run build",
"typecheck": "bunx tsc --noEmit -p tsconfig.json"
}
}
Loading