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
70 changes: 70 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"attribution": {
"commit": "",
"pr": ""
},
"permissions": {
"allow": [
"Bash(git *)",
"Bash(uv *)",
"Bash(uv run *)",
"Bash(npm *)",
"Bash(npx *)",
"Bash(pnpm *)",
"Bash(pytest *)",
"Bash(python *)",
"Bash(node *)",
"Bash(docker *)",
"Bash(docker-compose *)",
"Bash(gh *)",
"Bash(make *)",
"Bash(cat *)",
"Bash(ls *)",
"Bash(find *)",
"Bash(grep *)",
"Bash(rg *)",
"Bash(sort *)",
"Bash(head *)",
"Bash(tail *)",
"Bash(wc *)",
"Bash(echo *)",
"Bash(mkdir *)",
"Bash(cp *)",
"Bash(mv *)",
"Bash(pwd)",
"Bash(which *)",
"Bash(black *)",
"Bash(ruff *)",
"Bash(mypy *)",
"Bash(eslint *)",
"Bash(tsc *)",
"Bash(tsc --noEmit *)",
"Read",
"Edit",
"Write",
"WebSearch"
],
"deny": [
"Bash(rm -rf /*)",
"Bash(rm -rf .)",
"Bash(git push * master)",
"Bash(git push * main)",
"Bash(git push --no-verify *)",
"Bash(git checkout master)",
"Bash(git checkout main)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./infra/credentials.md)"
],
"defaultMode": "acceptEdits"
},
"enabledPlugins": {
"superpowers@claude-plugins-official": true
},
"plansDirectory": ".claude/plans",
"statusLine": {
"type": "command",
"command": "bash D:/mcp-comet/.claude/statusline-command.sh"
}
}
51 changes: 51 additions & 0 deletions .claude/statusline-command.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
input=$(cat)

cwd=$(echo "$input" | jq -r '.workspace.current_dir // "."')
project_dir=$(echo "$input" | jq -r '.workspace.project_dir // "."')
model=$(echo "$input" | jq -r '.model.display_name // "unknown"')
pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0')
ctx_size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')

REMOTE=$(git -C "$cwd" remote get-url origin 2>/dev/null | sed 's/git@github.com:/https:\/\/github.com\//' | sed 's/\.git$//')
BRANCH=$(git -C "$cwd" branch --show-current 2>/dev/null || echo "")
cost_fmt=$(printf '$%.2f' "$cost")
mins=$((duration_ms / 60000))
secs=$(((duration_ms % 60000) / 1000))

ESC=$'\033'
CYAN="${ESC}[36m"
GREEN="${ESC}[32m"
YELLOW="${ESC}[33m"
RED="${ESC}[31m"
DIM="${ESC}[2m"
RESET="${ESC}[0m"

# RIGA 1
if [ -n "$REMOTE" ]; then
echo "${CYAN}${REMOTE}${RESET} ${DIM}|${RESET} ${GREEN}${BRANCH}${RESET} ${DIM}|${RESET} ${model} ${DIM}|${RESET} ${cost_fmt} ${DIM}|${RESET} ${mins}m ${secs}s"
else
echo "${CYAN}${project_dir##*/}${RESET} ${DIM}|${RESET} ${GREEN}${BRANCH}${RESET} ${DIM}|${RESET} ${model} ${DIM}|${RESET} ${cost_fmt} ${DIM}|${RESET} ${mins}m ${secs}s"
fi

# RIGA 2: barra contesto
pct_int=$(printf "%.0f" "$pct")
if [ "$pct_int" -lt 50 ]; then BAR_COLOR="$GREEN"
elif [ "$pct_int" -lt 80 ]; then BAR_COLOR="$YELLOW"
else BAR_COLOR="$RED"
fi

filled=$((pct_int * 20 / 100))
empty=$((20 - filled))
bar=""
for ((i=0; i<filled; i++)); do bar+="▓"; done
for ((i=0; i<empty; i++)); do bar+="░"; done

if [ "$ctx_size" -ge 1000000 ]; then ctx_label="$((ctx_size / 1000000))M"
elif [ "$ctx_size" -ge 1000 ]; then ctx_label="$((ctx_size / 1000))k"
else ctx_label="$ctx_size"
fi

echo "${BAR_COLOR}█${RESET} ${bar} ${pct}% ${DIM}of ${ctx_label} | ${pct}% used${RESET}"
30 changes: 25 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
# Local configuration and state
mcp-comet.config.json
.mcp.json
.claude/settings.local.json
.claude/plans

tools/
# Internal Documentation & Plans (local only)
docs/claude-code-guide/
docs/plans/
docs/uat/
docs/uat-checklist.md
docs/superpowers/

# Temporary Assets
mcp-comet-screenshot-*.jpg
mcp-comet-screenshot-*.png

# Dependencies and Build
node_modules/
dist/
coverage/
*.js.map
*.d.ts.map
asteria.config.json
.DS_Store
.claude/settings.local.json
.claude/plans
.mcp.json
docs/superpowers
.crush/
# Remotion video project
video/
.playwright-mcp/
# worktrees
.worktrees/*
124 changes: 124 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# AGENTS.md — MCP Comet

MCP Comet is a TypeScript MCP (Model Context Protocol) server that automates the Perplexity Comet browser via Chrome DevTools Protocol (CDP). It exposes 13 tools over stdio for prompting, polling, screenshots, tab management, source extraction, and mode switching.

## Commands

```bash
npm run build # tsc → dist/
npm test # vitest run
npm run test:watch # vitest in watch mode
npm run test:ci # vitest run --coverage (CI uses this)
npm run lint # biome check .
npm run format # biome write . (auto-fix)
npm run typecheck # tsc --noEmit
npm start # node dist/index.js (stdio MCP server)
```

CI pipeline (`.github/workflows/ci.yml`): `npm ci → build → lint → vitest run --coverage` on Node 22.

**Before committing**: run `npm run lint && npm test`.

## Architecture

Four layers, top to bottom:

```
MCP Tools (server.ts) → UI Automation (src/ui/) → CDP Transport (src/cdp/) → Comet Browser
```

- **server.ts** — Single file defining all 13 tools via `McpServer.tool()`. Contains `startServer()`, tool definitions, Zod schemas, and all handler logic. This is the main file to edit when adding/modifying tools.
- **src/ui/** — Functions that return JavaScript strings (evaluated in the browser via `Runtime.evaluate`). Each `build*Script()` function returns a self-contained IIFE string. **Do not** pass complex objects — everything must serialize to a JS expression.
- **src/cdp/client.ts** — `CDPClient` singleton (`CDPClient.getInstance()`) managing WebSocket connections, auto-reconnect with exponential backoff, and an operation queue (`enqueue()`) to serialize concurrent CDP calls.
- **src/selectors/** — Version-keyed CSS selector sets (`SelectorSet`). `v145.ts` is the current set. New Comet/Chrome versions get a new `v{version}.ts` file registered in `index.ts`. Unknown versions fall back to the latest known set.

### Key flow: `comet_ask` (fire-and-forget) + `comet_wait` (blocking poll)

`comet_ask` types the prompt via `execCommand('insertText')` and submits immediately — it does NOT wait for a response. `comet_wait` (or `comet_poll`) must be called separately to retrieve results. This is a deliberate design: decoupled prompt submission from response polling.

### Auto-connect

Every tool handler calls `ensureConnected()` first, which lazily launches/connects to Comet if no session exists. Connection health is verified by evaluating `1+1` via CDP with a timeout.

## Code Style

- **Formatter**: Biome (2-space indent, single quotes, no semicolons, trailing commas, 100 char line width)
- **Module system**: ESM only (`"type": "module"`, `Node16` module resolution, `.js` extensions in imports)
- **Imports**: Always use `.js` extension in import paths (TypeScript ESM requirement)
- **Strict mode**: TypeScript strict enabled
- **Lint rules**: `noExplicitAny: error` in source (off in tests), `noConsole: warn` in source (off in tests, suppress with `// biome-ignore lint/suspicious/noConsole: ...`)
- **Zod for schemas**: Tool parameters defined as Zod raw shapes; `buildInputSchema()` converts to JSON schema for the exported registry

## Testing

Vitest with v8 coverage. Coverage thresholds: 75% statements/lines, 70% branches, 80% functions.

### Test structure

- **`tests/unit/`** — Mocked CDP, isolated component tests. UI script tests validate that `build*Script()` functions produce valid JS containing expected patterns.
- **`tests/integration/tools/`** — End-to-end tool handler tests using the **harness pattern** (`tests/integration/tools/harness.ts`).

### Integration test harness

The harness (`harness.ts`) is critical to understand:

1. Mocks `McpServer` to capture handler functions during `startServer()` into `capturedHandlers`
2. Mocks `CDPClient.getInstance()` to return a controllable `mocks` object
3. Mocks `loadConfig` and `detectCometVersion`
4. Tests call `registerHandlers()` in `beforeAll`, then `getHandler('tool_name')` to get the handler

When writing new tool tests:
```ts
import { getHandler, mocks, registerHandlers, resetHarness } from './harness.js'

beforeAll(async () => { await registerHandlers() })
beforeEach(() => { resetHarness() })
// Override mocks as needed per test, then call the handler
```

### Unit test patterns

- **CDPClient tests**: Mock `chrome-remote-interface` and `globalThis.fetch` for HTTP endpoints (`/json/version`, `/json/list`)
- **UI script tests**: Call `build*Script()` functions and assert on the returned string content (no browser needed)
- Reset singleton between tests: `CDPClient.resetInstance()`

## Gotchas and Non-Obvious Patterns

- **UI scripts are strings, not functions**: Everything in `src/ui/` returns a JS string that gets evaluated remotely via `Runtime.evaluate`. You cannot pass closures or use Node APIs inside these scripts. All context must be embedded in the string via string interpolation.

- **Prompt injection uses `execCommand('insertText')`**: Comet uses a Lexical editor that ignores standard `value` assignment. Prompts are JSON.stringify'd before injection to prevent XSS/injection attacks.

- **Singleton CDPClient**: `CDPClient.getInstance()` returns one instance. Tests must call `CDPClient.resetInstance()` to clear state between test runs.

- **Operation queue**: `CDPClient.enqueue()` serializes all operations. Nested calls within an `enqueue` block are fine, but two concurrent external calls will be sequenced.

- **Auto-reconnect**: `withAutoReconnect()` wraps operations with retry logic. Health checks evaluate `1+1` and reconnect on failure. The reconnect itself is deduplicated via `reconnectPromise`.

- **Tab categorization**: Tabs are classified by URL patterns. `perplexity.ai` with `sidecar` in URL → Sidecar. `chrome://` tabs must never be closed (crashes Comet). Only `agentBrowsing` tabs are closed during cleanup.

- **`comet_mode` requires navigation to home**: Mode switching only works on a new chat page. The tool navigates to `https://www.perplexity.ai` before attempting the slash-command typeahead.

- **Collapsed citations**: Sources with empty URLs and text containing `+` (e.g., "wsj+3") are detected as collapsed. `comet_get_sources` does a two-pass extraction: first pass collects what's visible, second pass clicks collapsed items (via `buildExpandCollapsedCitationsScript`) and re-extracts, then merges by deduplicating on URL.

- **No `src/server.test.ts`**: Server logic is tested via the integration test harness in `tests/integration/tools/`, not via a unit test file.

- **`index.ts` is the stdio entry point**: It imports and calls `startServer()` from `server.ts`. The `cli.ts` file is the `mcp-comet` binary with subcommands (`start`, `call`, `detect`).

- **Logger writes to stderr**: All logging goes to stderr to avoid corrupting MCP stdio JSON-RPC messages on stdout.

- **Release**: Triggered by pushing `v*` tags. Publishes to npm as `@onestepat4time/mcp-comet`. Uses `release-please` for changelog/version management.

## Adding a New Comet Version

When Comet updates its Chrome version and CSS selectors change:

1. Run `mcp-comet detect` to get the Chrome major version
2. Inspect Comet DOM with DevTools
3. Create `src/selectors/v{version}.ts` implementing `SelectorSet` interface
4. Register in `src/selectors/index.ts` selector map
5. Add unit tests in `tests/unit/selectors/`
6. Update `docs/comet-compatibility.md`

## Commit Conventions

Conventional commit prefixes: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `chore:`. CI requires passing lint + tests before merge.
Loading
Loading