diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ab3b987 --- /dev/null +++ b/.claude/settings.json @@ -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" + } +} diff --git a/.claude/statusline-command.sh b/.claude/statusline-command.sh new file mode 100644 index 0000000..7b48aea --- /dev/null +++ b/.claude/statusline-command.sh @@ -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 { 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. diff --git a/README.md b/README.md index 0c20735..69c19a3 100644 --- a/README.md +++ b/README.md @@ -1,246 +1,265 @@ -

- Asteria Banner -

- -

- version - CI - license - coverage - node - MCP - platform -

- -

- Give any AI agent direct control over Perplexity Comet via the Model Context Protocol. -

+ +
+

MCP Comet

+

+ MCP Comet banner +

+

Turn Perplexity Comet into a production-grade MCP research engine

+

+ npm + build + license +

+

7 modes · 13 tools · zero-friction setup · full browser control

+

+ Quick Start · + Tool Reference · + Architecture +

+
+ --- -## How It Works +## Why MCP Comet -Asteria is an [MCP server](https://modelcontextprotocol.io) that bridges AI assistants with [Perplexity Comet](https://comet.perplexity.ai/) — the agentic browser that researches, browses, and answers questions autonomously. Unlike simple search APIs, Comet navigates pages, follows links, and reasons over live web content. Asteria exposes all of that through 13 MCP tools via Chrome DevTools Protocol (CDP), with no Puppeteer or Playwright dependencies. +MCP Comet gives your agent more than a chat box. It gives your agent a complete research cockpit. -```mermaid -graph LR - A["AI Agent\n(Claude / GPT / Gemini)"] -->|"MCP stdio"| B["Asteria\nMCP Server"] - B -->|"CDP WebSocket"| C["Perplexity\nComet Browser"] - C -->|"HTTP requests"| D["Web Pages\n& Sources"] - D -->|"Response + Sources"| C - C -->|"Research results"| B - B -->|"Formatted response"| A -``` - -The agent sends a prompt through MCP. Asteria connects to Comet over CDP, submits the query, monitors Comet's agentic research cycle, and returns the full response with cited sources. - ---- +- Run high-quality web research in Comet directly from MCP clients. +- Switch between 7 purpose-built research modes instantly. +- Pull sources, screenshots, conversations, tabs, and full page content. +- Stay resilient with auto-connect, reconnect logic, and selector fallback strategies. -## Features - -| | Feature | Description | -|---|---------|-------------| -| 🔌 | **13 MCP tools** | Connect, ask, poll, wait, stop, screenshot, mode switch, tab management, source extraction, conversation history, page content | -| ⚡ | **Non-blocking polling** | Submit a prompt and poll for completion — the agent keeps working while Comet researches | -| ⏳ | **Blocking wait** | `comet_wait` blocks until Comet finishes, ideal when `comet_ask` times out mid-response | -| 🔍 | **Auto-detect Comet** | Finds Comet on Windows, macOS, and Linux; launches it with the correct debug port | -| 🔄 | **Auto-reconnect** | Exponential backoff with health checks — survives Comet restarts without dropping the session | -| 🧠 | **Version-aware selectors** | Auto-detects Comet's Chrome version and routes to the correct CSS selectors | -| 📑 | **Tab categorization** | Tracks main, sidecar, agent-browsing, and overlay tabs separately | -| 🚫 | **Zero browser dependencies** | No Puppeteer or Playwright — uses CDP directly via `chrome-remote-interface` | -| 🛠️ | **CLI included** | `asteria detect` to check installation, `asteria snapshot` to capture DOM structure | +If your workflow is "ask, verify, cite, and iterate", this is the server built for it. --- -## Requirements +## Quick Start -- **Node.js** >= 18 -- **[Perplexity Comet](https://comet.perplexity.ai/)** installed and running -- **Windows**, **macOS**, or **Linux** (Linux requires setting `COMET_PATH`) +### 1. Prerequisites ---- +- Node.js >= 18 +- [Perplexity Comet](https://comet.perplexity.ai/) installed -## Installation +Optional pre-flight check: ```bash -npm install -g @onestepat4time/asteria +mcp-comet detect ``` ---- +### 2. Add MCP Comet to MCP -## Quick Start +Use one of these configs. -### 1. Add to your MCP client config +Claude Desktop (`~/.claude/claude_desktop_config.json`): -**Claude Code** (`~/.claude/claude_desktop_config.json`): ```json { "mcpServers": { - "asteria": { + "mcp-comet": { "type": "stdio", - "command": "asteria", + "command": "mcp-comet", "args": ["start"] } } } ``` -**Cursor** (`~/.cursor/mcp.json`) — same format. +Cursor (`~/.cursor/mcp.json`): -### 2. Make sure Comet is running +```json +{ + "mcpServers": { + "mcp-comet": { + "type": "stdio", + "command": "mcp-comet", + "args": ["start"] + } + } +} +``` -Open Perplexity Comet on your machine. Asteria auto-detects the running instance. +### 3. Give your agent a mission -### 3. Use in your agent +Prompt your agent with something like: -``` -> Ask Perplexity what the latest AI research papers are this week -``` +> Use Comet in deep-research mode to analyze the global battery supply chain in 2026. Return a structured summary with all cited sources. -Asteria connects to Comet, sends the query, waits for the full research response, and returns it with cited sources to your assistant. +Your agent can chain `comet_mode`, `comet_ask`, `comet_wait`, and `comet_get_sources` automatically. --- -## Tools - -| Tool | Description | Docs | -|------|-------------|------| -| `comet_connect` | Connect to or launch Perplexity Comet | [Reference](docs/tools.md) | -| `comet_ask` | Send a prompt and start an agentic search | [Reference](docs/tools.md) | -| `comet_poll` | Check current agent status, steps, and response content | [Reference](docs/tools.md) | -| `comet_wait` | Block until the agent finishes responding; use after `comet_ask` times out | [Reference](docs/tools.md) | -| `comet_stop` | Stop the currently running agent | [Reference](docs/tools.md) | -| `comet_screenshot` | Capture a screenshot of the active tab | [Reference](docs/tools.md) | -| `comet_mode` | Get or switch the search mode (standard, deep-research, model-council, etc.) | [Reference](docs/tools.md) | -| `comet_list_tabs` | List all open tabs by category (main, sidecar, agent-browsing, overlay) | [Reference](docs/tools.md) | -| `comet_switch_tab` | Switch focus to a specific tab by ID or title | [Reference](docs/tools.md) | -| `comet_get_sources` | Extract cited sources from the current response | [Reference](docs/tools.md) | -| `comet_list_conversations` | List recent conversation links visible on the page | [Reference](docs/tools.md) | -| `comet_open_conversation` | Navigate to a specific conversation URL | [Reference](docs/tools.md) | -| `comet_get_page_content` | Extract full text from the active page | [Reference](docs/tools.md) | +## Research Modes ---- +Choose the mode that matches the job. + +- `standard`: fast factual lookups. Example: "What is the latest CPI reading for Canada?" +- `deep-research`: multi-source investigations. Example: "Map the 2026 AI chip supply chain and major risks." +- `model-council`: multi-perspective reasoning. Example: "Debate arguments for and against UBI with tradeoffs." +- `create`: drafting and ideation. Example: "Draft a technical explainer on WebAssembly in edge runtimes." +- `learn`: guided teaching. Example: "Teach me B-trees step by step with examples." +- `review`: critical analysis. Example: "Review this API design for security and reliability gaps." +- `computer`: browser-interactive tasks. Example: "Open arXiv and find the newest papers on retrieval augmentation." -## CLI +CLI example: ```bash -asteria start # Start MCP stdio server -asteria detect # Detect Comet installation path and debug port -asteria --version # Print version -asteria --help # Print help +mcp-comet call comet_mode '{"mode":"deep-research"}' +mcp-comet call comet_ask '{"prompt":"Analyze current fusion startups by funding and milestones"}' +mcp-comet call comet_wait +mcp-comet call comet_get_sources ``` --- -## Configuration +## Toolset at a Glance + +### Session + +- `comet_connect`: connects to Comet or launches it. +- `comet_poll`: returns live status and partial progress. +- `comet_wait`: waits for completion and returns the full response. +- `comet_stop`: stops a running task. -All settings can be overridden via environment variables: +### Query -| Variable | Default | Description | -|----------|---------|-------------| -| `ASTERIA_PORT` | `9222` | CDP debug port | -| `COMET_PATH` | auto-detect | Path to Comet executable | -| `ASTERIA_LOG_LEVEL` | `info` | Log level: `debug` / `info` / `warn` / `error` | -| `ASTERIA_TIMEOUT` | `30000` | Comet launch timeout (ms) | -| `ASTERIA_RESPONSE_TIMEOUT` | `180000` | Max wait for response (ms) | -| `ASTERIA_POLL_INTERVAL` | `1000` | Status poll interval (ms) | -| `ASTERIA_SCREENSHOT_FORMAT` | `png` | Screenshot format: `png` / `jpeg` | -| `ASTERIA_MAX_RECONNECT` | `5` | Max reconnection attempts | -| `ASTERIA_RECONNECT_DELAY` | `5000` | Max reconnection backoff delay (ms) | +| Tool | What It Does | +| ------------ | ------------------------------------- | +| `comet_ask` | Sends a prompt to Comet | +| `comet_mode` | Gets or switches active research mode | -See [Configuration](docs/configuration.md) for the full reference. +### Content + +| Tool | What It Does | +| ------------------------ | -------------------------------------------------- | +| `comet_screenshot` | Captures PNG/JPEG screenshots | +| `comet_get_sources` | Extracts references, including collapsed citations | +| `comet_get_page_content` | Extracts page title and readable text | + +### Navigation + +| Tool | What It Does | +| -------------------------- | ---------------------------------- | +| `comet_list_tabs` | Lists tabs by category | +| `comet_switch_tab` | Jumps to a tab by id or title | +| `comet_list_conversations` | Lists sidebar conversations | +| `comet_open_conversation` | Opens a specific conversation | + +Full reference: [docs/tools.md](docs/tools.md) --- -## Guides +## Agent Workflows -| Guide | Description | -|-------|-------------| -| [Tool Reference](docs/tools.md) | All 13 tools with parameters, return values, and examples | -| [Integration Guide](docs/integration.md) | Set up with Claude Code, Cursor, or custom MCP clients | -| [Configuration](docs/configuration.md) | Environment variables and config files | -| [Troubleshooting](docs/troubleshooting.md) | Common issues and error codes | -| [Architecture](docs/architecture.md) | How Asteria works internally | +| Goal | Flow | +| ---- | ---- | +| Deep research with citations | `comet_connect` -> `comet_mode(deep-research)` -> `comet_ask` -> `comet_wait` -> `comet_get_sources` | +| Multi-perspective debate | `comet_mode(model-council)` -> `comet_ask` -> `comet_wait` | +| Visual evidence capture | `comet_screenshot` -> pass image into your vision-capable model | +| Resume old investigations | `comet_list_conversations` -> `comet_open_conversation` -> `comet_get_page_content` | --- -## Architecture +## CLI Power Ops + +Install globally: -```mermaid -graph TD - subgraph MCP["MCP Protocol Layer"] - T1[comet_connect] & T2[comet_ask] & T3[comet_poll] - T4[comet_wait] & T5[comet_stop] & T6[comet_screenshot] - T7[comet_mode] & T8[comet_list_tabs] & T9[comet_switch_tab] - T10[comet_get_sources] & T11[comet_list_conversations] - T12[comet_open_conversation] & T13[comet_get_page_content] - end - - subgraph UI["UI Automation"] - S[Selector Strategies] - I[Prompt Input] - ST[Status Detection] - EX[Content Extraction] - end - - subgraph CDP["CDP Transport"] - BR[Browser Launcher] - CO[WebSocket Connection] - TA[Tab Management] - RC[Auto-Reconnect] - end - - T2 --> I - T3 --> ST - T4 --> ST - T10 & T13 --> EX - T6 & T8 --> TA - I & ST & EX --> S - CO --> BR - TA & RC --> CO - CO --> Comet[Perplexity Comet] +```bash +npm install -g @onestepat4time/mcp-comet +``` + +Use directly: + +```bash +# connectivity and diagnostics +mcp-comet detect +mcp-comet call comet_connect + +# ask + wait pattern +mcp-comet call comet_ask '{"prompt":"What are the top AI safety papers this week?"}' +mcp-comet call comet_wait + +# source extraction +mcp-comet call comet_get_sources +``` + +No install option: + +```bash +npx -y @onestepat4time/mcp-comet ``` --- -## Roadmap +## Architecture + +```text +MCP Tools + -> UI Automation + -> CDP Transport + -> Perplexity Comet +``` + +- Ordered selector strategies tolerate Comet UI changes. +- Automatic version detection selects the correct selector set. +- Auto-reconnect includes health checks with retry backoff. +- Source extraction uses a second pass to expand collapsed citations. -- [ ] **Streaming responses** — stream Comet responses token-by-token instead of polling -- [ ] **MCP Resources** — expose Perplexity pages as MCP resources for direct reading -- [ ] **Multi-Comet sessions** — control multiple Comet instances simultaneously -- [ ] **HTTP/SSE transport** — support N8N and REST clients in addition to stdio -- [ ] **Browser extension** — package as a browser extension for tighter integration +Deep dive: [docs/architecture.md](docs/architecture.md) --- -## Contributing +## Configuration Essentials -Contributions are welcome. Open an issue before submitting large PRs. +Most teams only tune these three: -```bash -git clone https://github.com/OneStepAt4time/asteria.git -cd asteria -npm install -npm run build -npm test +| Variable | Default | Change It When | +| ------------------------ | ------------- | ---------------------------------------- | +| `COMET_RESPONSE_TIMEOUT` | `180000` | Queries are long and timing out | +| `COMET_PATH` | auto-detect | Comet is in a non-standard install path | +| `COMET_LOG_LEVEL` | `info` | You need debug logs | + +Config file (`mcp-comet.config.json`) example: + +```json +{ + "responseTimeout": 300000, + "logLevel": "debug" +} ``` -See [Contributing](docs/contributing.md) for code style, adding Comet versions, and commit conventions. +More options and full env var reference: [docs/configuration.md](docs/configuration.md) --- -## Support the Project +## Compatibility -If Asteria saves you time, consider sponsoring: +| Chrome Version | Selector Set | Status | +| -------------- | ------------ | --------- | +| 145 | v145 | Supported | -

- GitHub Sponsors -   - Ko-fi -

+Unknown versions fall back to the latest known selector set. + +Details and upgrade flow: [docs/comet-compatibility.md](docs/comet-compatibility.md) --- +## Contributing + +PRs are welcome. Before opening one: + +```bash +npm run lint && npm test +``` + +Guide: [docs/contributing.md](docs/contributing.md) + +## Feedback + +- Issues: + ## License -MIT © 2026 [OneStepAt4time](https://github.com/OneStepAt4time) +[MIT](LICENSE) + +Built by [OneStepAt4time](https://github.com/OneStepAt4time) diff --git a/docs/architecture.md b/docs/architecture.md index 313bdcc..a50512f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -Asteria uses a four-layer architecture: +MCP Comet uses a four-layer architecture: ``` MCP Tools (13 tools) @@ -14,7 +14,7 @@ Perplexity Comet Browser (Chromium) ## MCP Layer -Asteria exposes 13 MCP tools grouped by function: +MCP Comet exposes 13 MCP tools grouped by function: **Session** @@ -57,7 +57,7 @@ Selectors are ordered arrays of CSS selectors. Each strategy tries selectors in ### Typeahead Mode Detection -When switching modes via `comet_mode`, Asteria reads the SVG icon `href` from typeahead menu items with the `.bg-subtle` class. Icon IDs map to mode names: +When switching modes via `comet_mode`, MCP Comet reads the SVG icon `href` from typeahead menu items with the `.bg-subtle` class. Icon IDs map to mode names: | Icon ID | Mode | |---------|------| @@ -72,7 +72,7 @@ If icon detection fails, the system falls back to URL-based mode detection. ### Collapsed Citation Expansion -Sources with collapsed citation text (matching the pattern `^\w+\+\d+$`, such as "arXiv+3") do not expose a URL directly. Asteria clicks these elements to reveal the full source URL, then re-extracts sources in a second pass. This two-pass strategy ensures complete source collection. +Sources with collapsed citation text (matching the pattern `^\w+\+\d+$`, such as "arXiv+3") do not expose a URL directly. MCP Comet clicks these elements to reveal the full source URL, then re-extracts sources in a second pass. This two-pass strategy ensures complete source collection. ### Prompt Injection @@ -84,7 +84,7 @@ The prompt text is embedded via `JSON.stringify()` before injection. This preven ### Version Detection -On connect, Asteria queries the `/json/version` endpoint to retrieve the Chrome major version number. This version determines which selector set to use from the registry. Unknown versions fall back to the latest known selector set. +On connect, MCP Comet queries the `/json/version` endpoint to retrieve the Chrome major version number. This version determines which selector set to use from the registry. Unknown versions fall back to the latest known selector set. ### Connection Management @@ -96,7 +96,7 @@ Connection health is verified by evaluating `1+1` via CDP with a 3-second timeou ## Error Handling -Asteria defines 9 error subclasses, all extending the base `AsteriaError`. Each carries a string code, a context object, and an optional cause. +MCP Comet defines 9 error subclasses, all extending the base `CometError`. Each carries a string code, a context object, and an optional cause. | Code | Class | |------|-------| @@ -121,7 +121,7 @@ Errors are formatted as `[CODE] message` when converted to MCP responses via `to 2. Pre-send state capture -- snapshot proseCount and lastProseText 3. Type prompt -- execCommand('insertText') via JSON.stringify 4. Submit -- Enter key via CDP -5. Polling loop -- check status every ASTERIA_POLL_INTERVAL ms +5. Polling loop -- check status every COMET_POLL_INTERVAL ms - Detect new response via proseCount growth - Stall detection: 10 polls without growth -> break 6. Response settle -- 5 x 1s polls to ensure complete text @@ -131,3 +131,4 @@ Errors are formatted as `[CODE] message` when converted to MCP responses via `to ### comet_wait lifecycle `comet_wait` is `comet_ask`'s polling loop without the prompt submission steps (3-4). It polls until the agent finishes or stalls, using the same settle logic (5 x 1s polls) to ensure the response is complete before returning. + diff --git a/docs/assets/banner.svg b/docs/assets/banner.svg index 0448fb0..3f70b87 100644 --- a/docs/assets/banner.svg +++ b/docs/assets/banner.svg @@ -1,67 +1,244 @@ - + + MCP Comet banner + Animated banner for MCP Comet with a dark space backdrop, glowing title, orbital accents, and a moving comet trail. + - - - - - - - - + + + - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ASTERIA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MCP COMET + + + + + MCP COMET - - - MCP SERVER FOR PERPLEXITY COMET + + DEEP RESEARCH SUPERPOWERS FOR AI AGENTS - - - Control Comet's agentic browser from any AI assistant via Chrome DevTools Protocol + + Turn Perplexity Comet into a production-grade MCP research engine - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/comet-compatibility.md b/docs/comet-compatibility.md index d1c4937..b54a42f 100644 --- a/docs/comet-compatibility.md +++ b/docs/comet-compatibility.md @@ -1,6 +1,6 @@ # Comet Compatibility -Asteria interacts with Comet through the Chrome DevTools Protocol, relying on CSS selectors to locate DOM elements such as the prompt input, submit button, and response container. When Comet updates its internal Chrome version, the DOM structure may change, breaking those selectors. Asteria handles this through a versioned selector system. +MCP Comet interacts with Comet through the Chrome DevTools Protocol, relying on CSS selectors to locate DOM elements such as the prompt input, submit button, and response container. When Comet updates its internal Chrome version, the DOM structure may change, breaking those selectors. MCP Comet handles this through a versioned selector system. ## Supported Versions @@ -12,7 +12,7 @@ Unknown or newer versions fall back to the latest known selector set (currently ## How Version Detection Works -When a client calls `comet_connect`, Asteria performs automatic version detection: +When a client calls `comet_connect`, MCP Comet performs automatic version detection: 1. Queries the CDP endpoint at `http://127.0.0.1:{port}/json/version` 2. Extracts the Chrome major version from the `Browser` field in the response (for example, `145` from `Chrome/145.2.7632.4587`) @@ -24,7 +24,7 @@ The detection is implemented in `src/version.ts` (`detectCometVersion`). On fail ## Selector Strategy Pattern -Each selector set is a collection of ordered arrays of CSS selectors. For every DOM operation, Asteria tries selectors in array order and uses the first one that matches an element. +Each selector set is a collection of ordered arrays of CSS selectors. For every DOM operation, MCP Comet tries selectors in array order and uses the first one that matches an element. ### Why ordered arrays matter @@ -66,10 +66,10 @@ When a new Comet release changes the DOM, follow these steps to add support. ### 1. Detect the version -Run Asteria's CLI to see the current Chrome/Comet version: +Run MCP Comet's CLI to see the current Chrome/Comet version: ```bash -asteria detect +mcp-comet detect ``` This queries the CDP endpoint and prints the detected version. @@ -136,9 +136,10 @@ Also update the fallback in `getSelectorsForVersion` if the new version should b - Verify against a real Comet instance: ```bash -asteria call comet_connect +mcp-comet call comet_connect ``` ### 6. Document Update the **Supported Versions** table at the top of this file with the new version entry. + diff --git a/docs/configuration.md b/docs/configuration.md index 59602f4..53c64a2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,39 +7,39 @@ The three most commonly used variables: | Variable | Purpose | |----------|---------| | `COMET_PATH` | Set only if Comet is not auto-detected on your system | -| `ASTERIA_RESPONSE_TIMEOUT` | Increase for long queries (default: 180000 ms / 3 min) | -| `ASTERIA_LOG_LEVEL` | Set to `debug` when troubleshooting issues | +| `COMET_RESPONSE_TIMEOUT` | Increase for long queries (default: 180000 ms / 3 min) | +| `COMET_LOG_LEVEL` | Set to `debug` when troubleshooting issues | ## Environment Variables | Variable | Default | Description | |----------|---------|-------------| -| `ASTERIA_PORT` | 9222 | Chrome DevTools Protocol port (1-65535) | +| `COMET_PORT` | 9222 | Chrome DevTools Protocol port (1-65535) | | `COMET_PATH` | auto-detect | Path to Comet executable | -| `ASTERIA_LOG_LEVEL` | info | Logging level: `debug`, `info`, `warn`, `error` | -| `ASTERIA_TIMEOUT` | 30000 | Comet launch timeout in ms (min 1000) | -| `ASTERIA_RESPONSE_TIMEOUT` | 180000 | Response polling timeout in ms (min 1000) | -| `ASTERIA_SCREENSHOT_FORMAT` | png | Screenshot format: `png` or `jpeg` | -| `ASTERIA_SCREENSHOT_QUALITY` | 80 | JPEG screenshot quality (0-100) | -| `ASTERIA_MAX_RECONNECT` | 5 | Max reconnection attempts (min 0) | -| `ASTERIA_RECONNECT_DELAY` | 5000 | Max reconnection backoff delay in ms | -| `ASTERIA_POLL_INTERVAL` | 1000 | Status poll interval in ms (min 100) | -| `ASTERIA_USER_DATA_DIR` | null | Path to a custom Chrome user data directory. Use this to persist cookies, local storage, and other browser profile data across sessions (for example, `~/.config/asteria/chrome-profile`). When unset, Comet uses a temporary profile each launch. | -| `ASTERIA_WINDOW_WIDTH` | 1440 | Browser window width in pixels at launch. Controls the initial viewport dimensions of the Comet browser window. | -| `ASTERIA_WINDOW_HEIGHT` | 900 | Browser window height in pixels at launch. Controls the initial viewport dimensions of the Comet browser window. | +| `COMET_LOG_LEVEL` | info | Logging level: `debug`, `info`, `warn`, `error` | +| `COMET_TIMEOUT` | 30000 | Comet launch timeout in ms (min 1000) | +| `COMET_RESPONSE_TIMEOUT` | 180000 | Response polling timeout in ms (min 1000) | +| `COMET_SCREENSHOT_FORMAT` | png | Screenshot format: `png` or `jpeg` | +| `COMET_SCREENSHOT_QUALITY` | 80 | JPEG screenshot quality (0-100) | +| `COMET_MAX_RECONNECT` | 5 | Max reconnection attempts (min 0) | +| `COMET_RECONNECT_DELAY` | 5000 | Max reconnection backoff delay in ms | +| `COMET_POLL_INTERVAL` | 1000 | Status poll interval in ms (min 100) | +| `COMET_USER_DATA_DIR` | null | Path to a custom Chrome user data directory. Use this to persist cookies, local storage, and other browser profile data across sessions (for example, `~/.config/mcp-comet/chrome-profile`). When unset, Comet uses a temporary profile each launch. | +| `COMET_WINDOW_WIDTH` | 1440 | Browser window width in pixels at launch. Controls the initial viewport dimensions of the Comet browser window. | +| `COMET_WINDOW_HEIGHT` | 900 | Browser window height in pixels at launch. Controls the initial viewport dimensions of the Comet browser window. | ## Priority 1. **Defaults** -- hardcoded sensible defaults -2. **Config file** -- `asteria.config.json` in current working directory -3. **Environment variables** -- `ASTERIA_*` and `COMET_PATH` +2. **Config file** -- `mcp-comet.config.json` in current working directory +3. **Environment variables** -- `COMET_*` and `COMET_PATH` 4. **Programmatic overrides** -- via `loadConfig(overrides)` Higher priority overrides lower. Invalid values fall back to defaults. ## Config File -Create `asteria.config.json` in your project root. Keys use camelCase (not the uppercase env var names). +Create `mcp-comet.config.json` in your project root. Keys use camelCase (not the uppercase env var names). ```jsonc { @@ -78,4 +78,5 @@ Create `asteria.config.json` in your project root. Keys use camelCase (not the u } ``` -See `asteria.config.example.json` in the repository root for a ready-to-copy template. +See `mcp-comet.config.example.json` in the repository root for a ready-to-copy template. + diff --git a/docs/contributing.md b/docs/contributing.md index d74a164..d690047 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,20 +1,20 @@ -# Contributing to Asteria +# Contributing to MCP Comet -Thank you for your interest in contributing to Asteria. This guide covers everything you need to get started, from setting up your development environment to submitting a pull request. +Thank you for your interest in contributing to MCP Comet. This guide covers everything you need to get started, from setting up your development environment to submitting a pull request. ## Setup Clone the repository and install dependencies: ```bash -git clone https://github.com/OneStepAt4time/asteria.git -cd asteria +git clone https://github.com/OneStepAt4time/mcp-comet.git +cd mcp-comet npm install npm run build npm test ``` -Asteria requires Node.js 18 or later. The build step compiles TypeScript to `dist/`. Running the test suite confirms your environment is configured correctly. +MCP Comet requires Node.js 18 or later. The build step compiles TypeScript to `dist/`. Running the test suite confirms your environment is configured correctly. ## Development Commands @@ -73,7 +73,7 @@ tests/ ## Testing Strategy -Asteria uses Vitest as its test runner. The suite contains 314 tests across 30+ files, organized into two categories. +MCP Comet uses Vitest as its test runner. The suite contains 314 tests across 30+ files, organized into two categories. **Unit tests** (`tests/unit/`) mock the CDP client and test components in isolation. This includes UI automation scripts, configuration validation, and error handling. When writing unit tests for UI scripts, verify the script output format against expected structures. @@ -83,7 +83,7 @@ Asteria uses Vitest as its test runner. The suite contains 314 tests across 30+ ## Code Style -Asteria uses Biome for linting and formatting, configured in `biome.json` at the project root. +MCP Comet uses Biome for linting and formatting, configured in `biome.json` at the project root. - TypeScript strict mode is enabled - Single quotes, trailing commas, no semicolons @@ -96,7 +96,7 @@ Run `npm run lint` before every commit. If Biome reports formatting issues, run When a new version of Comet (or its underlying Chrome version) is released, the CSS selectors used for UI automation may change. Follow these steps to add support: -1. Run `asteria detect` to confirm the Comet/Chrome version number +1. Run `mcp-comet detect` to confirm the Comet/Chrome version number 2. Open Comet with DevTools (Ctrl+Shift+I) and inspect the DOM elements used by the UI scripts 3. Create `src/selectors/v{version}.ts` implementing the `SelectorSet` interface 4. Copy selectors from the latest version file and update as needed @@ -127,3 +127,4 @@ Example: `feat: add support for Comet v147 selectors` - Keep PRs focused -- one feature or fix per PR Before opening a PR, verify locally that `npm run lint` and `npm test` both pass with no failures. + diff --git a/docs/integration.md b/docs/integration.md index caf1f7f..cb51a5a 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -1,14 +1,14 @@ # Integration Guide -This guide explains how to integrate Asteria with MCP clients, use it from the command line, and embed it in Node.js applications. +This guide explains how to integrate MCP Comet with MCP clients, use it from the command line, and embed it in Node.js applications. --- ## 1. Overview -Asteria communicates via the MCP stdio transport. The MCP client spawns `asteria start` as a subprocess and exchanges JSON-RPC 2.0 messages over stdin/stdout. There is no HTTP server and no port to configure for the MCP layer itself -- the stdio pipe is the transport. +MCP Comet communicates via the MCP stdio transport. The MCP client spawns `mcp-comet start` as a subprocess and exchanges JSON-RPC 2.0 messages over stdin/stdout. There is no HTTP server and no port to configure for the MCP layer itself -- the stdio pipe is the transport. -Under the hood, Asteria connects to the Perplexity Comet browser over Chrome DevTools Protocol (CDP) on port 9222. From the MCP client's perspective, all of that is invisible: you invoke tools and receive results. +Under the hood, MCP Comet connects to the Perplexity Comet browser over Chrome DevTools Protocol (CDP) on port 9222. From the MCP client's perspective, all of that is invisible: you invoke tools and receive results. --- @@ -18,40 +18,40 @@ Before configuring any MCP client, verify the following: - **Node.js >= 18** is installed. Run `node --version` to confirm. - **Perplexity Comet** is installed from [https://comet.perplexity.ai/](https://comet.perplexity.ai/). -- **Comet is running**. Asteria auto-detects it on port 9222. If Comet is not running, `comet_connect` will attempt to launch it automatically. -- **Asteria is installed globally**: +- **Comet is running**. MCP Comet auto-detects it on port 9222. If Comet is not running, `comet_connect` will attempt to launch it automatically. +- **MCP Comet is installed globally**: ```bash - npm install -g @onestepat4time/asteria + npm install -g @onestepat4time/mcp-comet ``` **Verify the setup:** ```bash -asteria detect +mcp-comet detect ``` -This should report that Comet is running and the debug port is active. If it reports that Comet was not found, start Comet manually and run `asteria detect` again. +This should report that Comet is running and the debug port is active. If it reports that Comet was not found, start Comet manually and run `mcp-comet detect` again. --- ## 3. Claude Code -Claude Code reads MCP server configuration from `~/.claude/claude_desktop_config.json`. Add Asteria as a stdio server: +Claude Code reads MCP server configuration from `~/.claude/claude_desktop_config.json`. Add MCP Comet as a stdio server: ```json { "mcpServers": { - "asteria": { + "mcp-comet": { "type": "stdio", - "command": "asteria", + "command": "mcp-comet", "args": ["start"] } } } ``` -After updating the configuration file, restart Claude Code. Asteria will appear as an MCP server exposing 13 tools. +After updating the configuration file, restart Claude Code. MCP Comet will appear as an MCP server exposing 13 tools. **Verify:** Ask Claude "What MCP tools do you have available?" The list should include `comet_connect`, `comet_ask`, `comet_wait`, and the other tools documented in [tools.md](tools.md). @@ -68,31 +68,31 @@ Claude: [calls comet_connect, then comet_ask with the prompt] ## 4. Cursor -Cursor reads MCP server configuration from `~/.cursor/mcp.json`. Add Asteria with the same stdio configuration: +Cursor reads MCP server configuration from `~/.cursor/mcp.json`. Add MCP Comet with the same stdio configuration: ```json { "mcpServers": { - "asteria": { + "mcp-comet": { "type": "stdio", - "command": "asteria", + "command": "mcp-comet", "args": ["start"] } } } ``` -After updating the configuration file, restart Cursor. Open **Settings > MCP** to confirm that `asteria` appears in the server list with an active status. +After updating the configuration file, restart Cursor. Open **Settings > MCP** to confirm that `mcp-comet` appears in the server list with an active status. --- ## 5. Other MCP Clients (Generic) -Any MCP-compatible client that supports the stdio transport can use Asteria. The configuration pattern is always the same: +Any MCP-compatible client that supports the stdio transport can use MCP Comet. The configuration pattern is always the same: | Field | Value | |------------|---------------| -| Command | `asteria` | +| Command | `mcp-comet` | | Args | `["start"]` | | Transport | `stdio` | @@ -102,47 +102,47 @@ Consult your client's documentation for where to place MCP server definitions. S ## 6. CLI Usage -Asteria provides a `call` subcommand for invoking tools directly from the terminal, outside of any MCP client. This is useful for debugging, scripting, and manual testing. +MCP Comet provides a `call` subcommand for invoking tools directly from the terminal, outside of any MCP client. This is useful for debugging, scripting, and manual testing. ```bash # Connect to Comet -asteria call comet_connect +mcp-comet call comet_connect # Ask a question -asteria call comet_ask '{"prompt": "What is 2+2?"}' +mcp-comet call comet_ask '{"prompt": "What is 2+2?"}' # Wait for completion after a timeout -asteria call comet_wait +mcp-comet call comet_wait # Check the current status -asteria call comet_poll +mcp-comet call comet_poll # Take a screenshot -asteria call comet_screenshot '{"format": "jpeg"}' +mcp-comet call comet_screenshot '{"format": "jpeg"}' # Switch mode -asteria call comet_mode '{"mode": "deep-research"}' +mcp-comet call comet_mode '{"mode": "deep-research"}' # Get sources from the current response -asteria call comet_get_sources +mcp-comet call comet_get_sources # List open browser tabs -asteria call comet_list_tabs +mcp-comet call comet_list_tabs # Switch to a specific tab -asteria call comet_switch_tab '{"tabId": "ABC123"}' +mcp-comet call comet_switch_tab '{"tabId": "ABC123"}' # Stop the current Comet operation -asteria call comet_stop +mcp-comet call comet_stop # Get page content -asteria call comet_get_page_content '{"maxLength": 5000}' +mcp-comet call comet_get_page_content '{"maxLength": 5000}' # List conversations -asteria call comet_list_conversations +mcp-comet call comet_list_conversations # Open a conversation by URL -asteria call comet_open_conversation '{"url": "https://www.perplexity.ai/search/abc123"}' +mcp-comet call comet_open_conversation '{"url": "https://www.perplexity.ai/search/abc123"}' ``` The `call` command sends a single JSON-RPC request over stdio and prints the response to stdout. Arguments are parsed as JSON; omit them for tools that take no parameters. @@ -151,12 +151,12 @@ The `call` command sends a single JSON-RPC request over stdio and prints the res ## 7. Programmatic Usage (Node.js) -You can spawn Asteria from a Node.js application and communicate via JSON-RPC over stdio. This is useful when you want to embed Perplexity Comet access in your own tooling without relying on an external MCP client. +You can spawn MCP Comet from a Node.js application and communicate via JSON-RPC over stdio. This is useful when you want to embed Perplexity Comet access in your own tooling without relying on an external MCP client. ```javascript import { spawn } from 'node:child_process' -const child = spawn('asteria', ['start'], { +const child = spawn('mcp-comet', ['start'], { stdio: ['pipe', 'pipe', 'pipe'], }) @@ -214,7 +214,7 @@ child.stdout.on('data', (chunk) => { }) child.stderr.on('data', (chunk) => { - // Asteria logs go to stderr + // MCP Comet logs go to stderr process.stderr.write(chunk) }) ``` @@ -223,5 +223,6 @@ Key points: - Each JSON-RPC message must be a single line terminated with `\n`. - After sending `initialize`, you must send a `notifications/initialized` notification before making tool calls. -- Asteria writes diagnostic logs to stderr, so stdout remains clean JSON-RPC traffic. -- The `protocolVersion` must be `2024-11-05`, which is the version Asteria implements. +- MCP Comet writes diagnostic logs to stderr, so stdout remains clean JSON-RPC traffic. +- The `protocolVersion` must be `2024-11-05`, which is the version MCP Comet implements. + diff --git a/docs/tools.md b/docs/tools.md index ed3f9f5..11de2fa 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -1,6 +1,6 @@ # Tool Reference -This document provides a complete reference for all 13 MCP tools exposed by the Asteria server. Each tool entry includes a description, parameter table, response format, CLI example, and implementation notes. +This document provides a complete reference for all 13 MCP tools exposed by the MCP Comet server. Each tool entry includes a description, parameter table, response format, CLI example, and implementation notes. Tools are consumed via the Model Context Protocol (MCP) stdio transport. All tools automatically connect to or launch the Comet browser if no active session exists (see [Connection Lifecycle](#connection-lifecycle)). @@ -64,7 +64,7 @@ Connected to Comet on port {port} (Chrome/{version}), target {targetId} - Closes extra tabs (sidecar, agent-browsing) after connecting. - Detects the Chrome major version and loads version-appropriate DOM selectors. - If the active tab is a sidecar or non-Perplexity page, it navigates to `https://www.perplexity.ai`. -- The port defaults to the value of `ASTERIA_PORT` (default `9222`) when not provided. +- The port defaults to the value of `COMET_PORT` (default `9222`) when not provided. --- @@ -80,7 +80,7 @@ This is the primary interaction tool. It types the prompt into the Comet input f |-----------|---------|----------|----------------------------------|--------------------------------------------------| | `prompt` | string | Yes | | The question or instruction to send | | `newChat` | boolean | No | `false` | Start a fresh chat before sending the prompt | -| `timeout` | number | No | `180000` (`ASTERIA_RESPONSE_TIMEOUT`) | Maximum wait time in ms for the agent response | +| `timeout` | number | No | `180000` (`COMET_RESPONSE_TIMEOUT`) | Maximum wait time in ms for the agent response | ### Response @@ -275,7 +275,7 @@ Captures a screenshot of the active browser tab and returns it as a base64-encod ### Parameters -| Parameter | Type | Required | Default (`ASTERIA_SCREENSHOT_FORMAT`) | Description | +| Parameter | Type | Required | Default (`COMET_SCREENSHOT_FORMAT`) | Description | |-----------|------|----------|---------------------------------------|--------------------------------| | `format` | enum | No | `"png"` | Image format: `"png"` or `"jpeg"` | @@ -308,8 +308,8 @@ The `mimeType` is `"image/png"` for PNG format and `"image/jpeg"` for JPEG forma ### Notes -- The default format can be overridden via the `ASTERIA_SCREENSHOT_FORMAT` environment variable. -- JPEG quality is controlled by `ASTERIA_SCREENSHOT_QUALITY` (default `80`, range 0-100). +- The default format can be overridden via the `COMET_SCREENSHOT_FORMAT` environment variable. +- JPEG quality is controlled by `COMET_SCREENSHOT_QUALITY` (default `80`, range 0-100). - Returns the image as an MCP image content block, not as text. --- @@ -739,7 +739,7 @@ All tools use a consistent error response format: | `AGENT_ERROR` | General agent execution error | | `CONFIG_ERROR` | Configuration validation error | -Non-Asteria errors (e.g., unexpected exceptions) return: +Non-MCP Comet errors (e.g., unexpected exceptions) return: ```json { @@ -775,9 +775,10 @@ Connection behavior is controlled by these configuration values (see [Configurat | Setting | Default | Description | |--------------------------|----------|----------------------------------------------| -| `ASTERIA_PORT` | `9222` | CDP debug port | +| `COMET_PORT` | `9222` | CDP debug port | | `COMET_PATH` | auto | Path to Comet executable | -| `ASTERIA_TIMEOUT` | `30000` | Comet launch timeout in ms | -| `ASTERIA_POLL_INTERVAL` | `1000` | Status poll interval in ms | -| `ASTERIA_MAX_RECONNECT` | `5` | Maximum reconnection attempts | -| `ASTERIA_RECONNECT_DELAY`| `5000` | Maximum reconnection backoff delay in ms | +| `COMET_TIMEOUT` | `30000` | Comet launch timeout in ms | +| `COMET_POLL_INTERVAL` | `1000` | Status poll interval in ms | +| `COMET_MAX_RECONNECT` | `5` | Maximum reconnection attempts | +| `COMET_RECONNECT_DELAY`| `5000` | Maximum reconnection backoff delay in ms | + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a84f875..b65e5a3 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,12 +1,12 @@ # Troubleshooting Guide -This guide covers common issues, error codes, and debugging techniques for Asteria. +This guide covers common issues, error codes, and debugging techniques for MCP Comet. ## 1. Connection Issues ### "Comet not found" / `COMET_NOT_FOUND` -Asteria cannot locate the Comet executable on your system. +MCP Comet cannot locate the Comet executable on your system. **Fix:** Set the `COMET_PATH` environment variable to the full path of the Comet binary. @@ -19,7 +19,7 @@ Asteria cannot locate the Comet executable on your system. **Verify:** ```bash -asteria detect +mcp-comet detect ``` --- @@ -49,12 +49,12 @@ This should return a JSON object with browser version information. Comet is either not running or is using a different debug port than expected. -**Fix:** Start Comet, or set `ASTERIA_PORT` to the correct port number. +**Fix:** Start Comet, or set `COMET_PORT` to the correct port number. **Verify:** ```bash -asteria detect +mcp-comet detect ``` The output should show "active" for the debug port status. @@ -70,10 +70,10 @@ The output should show "active" for the debug port status. **Fix:** Call `comet_wait` to poll until the agent completes, or increase the timeout. ```bash -asteria call comet_wait '{"timeout": 300000}' +mcp-comet call comet_wait '{"timeout": 300000}' ``` -**Alternative:** Increase the default response timeout by setting the `ASTERIA_RESPONSE_TIMEOUT` environment variable (in milliseconds). +**Alternative:** Increase the default response timeout by setting the `COMET_RESPONSE_TIMEOUT` environment variable (in milliseconds). --- @@ -125,7 +125,7 @@ No agent is currently running in Comet. This is expected behavior when no query ## 3. Error Codes Reference -All error subclasses inherit from `AsteriaError` (defined in `src/errors.ts`). +All error subclasses inherit from `MCP CometError` (defined in `src/errors.ts`). | Code | Class | Meaning | |------|-------|---------| @@ -148,16 +148,16 @@ All error subclasses inherit from `AsteriaError` (defined in `src/errors.ts`). } ``` -Non-Asteria errors are wrapped as `Error: ` without a code prefix. +Non-MCP Comet errors are wrapped as `Error: ` without a code prefix. --- ## 4. Debug Mode -Set `ASTERIA_LOG_LEVEL=debug` for verbose logging: +Set `COMET_LOG_LEVEL=debug` for verbose logging: ```bash -ASTERIA_LOG_LEVEL=debug asteria start +COMET_LOG_LEVEL=debug mcp-comet start ``` **Key log messages:** @@ -175,7 +175,7 @@ ASTERIA_LOG_LEVEL=debug asteria start Stderr contains server logs. Redirect it to a file for inspection: ```bash -asteria call comet_ask '{"prompt": "test"}' 2>asteria.log +mcp-comet call comet_ask '{"prompt": "test"}' 2>mcp-comet.log ``` --- @@ -185,7 +185,7 @@ asteria call comet_ask '{"prompt": "test"}' 2>asteria.log Run the detect command for a quick system diagnosis: ```bash -asteria detect +mcp-comet detect ``` This prints: @@ -197,8 +197,9 @@ This prints: **Quick diagnosis flow:** -1. Run `asteria detect`. +1. Run `mcp-comet detect`. 2. If the output says Comet is **not running**, start the Comet application. 3. If the executable shows **NOT FOUND**, set the `COMET_PATH` environment variable. 4. If the debug port is **not reachable**, launch Comet with `--remote-debugging-port=9222`. -5. If all checks are green, try `asteria call comet_connect` to establish a connection. +5. If all checks are green, try `mcp-comet call comet_connect` to establish a connection. + diff --git a/docs/uat-checklist.md b/docs/uat-checklist.md deleted file mode 100644 index f7b34b2..0000000 --- a/docs/uat-checklist.md +++ /dev/null @@ -1,61 +0,0 @@ -# UAT Checklist - -**Date:** 2026-04-10 (Round 3) -**Environment:** macOS, Node 22, Chrome/145.2.7632.4587, Comet via `--remote-debugging-port=9222` - -## Pre-requisites -- [x] Perplexity Comet installed and running with `--remote-debugging-port=9222` -- [x] MCP client configured with asteria server - -## Basic Operations -- [x] `comet_connect` — connects to running Comet, detects Chrome/145, loads selector set -- [x] `comet_ask` — sends a prompt and returns a complete response (stabilization settle added) -- [x] `comet_poll` — returns agent status (idle/working/completed) with response and proseCount -- [x] `comet_stop` — not tested (no running query at test time, but retry logic verified in unit tests) -- [x] `comet_screenshot` — returns a PNG image - -## Tab Management -- [x] `comet_list_tabs` — lists tabs with correct categories (Main, Sidecar, Other) -- [x] `comet_switch_tab` — switches to tab by title substring - -## Content Extraction -- [x] `comet_get_sources` — returns sources using citation element strategy -- [x] `comet_get_page_content` — extracts page text including full response content -- [x] `comet_list_conversations` — returns 20+ conversations including /computer/tasks/ URLs -- [x] `comet_open_conversation` — works via CLI with auto-connect - -## Mode Switching -- [x] `comet_mode` (get) — returns current mode ("standard") -- [x] `comet_mode` (set) — switches mode via icon-based matching (locale-independent). Works from home page / new chat. Uses CDP Input API for keystroke injection. - -## CLI -- [x] `asteria --version` — prints version (v0.1.0) -- [x] `asteria --help` — prints usage with all commands -- [x] `asteria detect` — detects Comet installation path and debug port status - -## Error Recovery -- [x] Auto-reconnect works — server reconnects on stale connections -- [x] Invalid URLs rejected — non-https, non-perplexity.ai, domain suffix attack, malformed URLs all rejected -- [x] SSRF domain suffix bypass fixed — `evilperplexity.ai` correctly rejected -- [x] Auto-connect works — all tools auto-connect when called via CLI - -## Notes - -- Mode switching works from home page or new chat page (where the input is available). On result pages, the input may not be accessible for the typeahead menu. -- All mode matching is locale-independent via SVG icon href (`#pplx-icon-telescope` etc.) -- Response capture includes a 1-second settle poll to ensure complete text -- Italian locale working patterns added to status detection - -## Fixes Applied (Round 2 -> Round 3) - -| Issue | Fix | Result | -|-------|-----|--------| -| Mode switch label mismatch | Icon-based matching (SVG href) + CDP Input API | Mode switches work regardless of locale | -| Response truncation | Added 1s settle poll after idle detection | Full responses captured | -| Locale-dependent status detection | Added Italian working patterns | Status detection works in non-English locales | -| Mode detection from URL | Added /computer/tasks/ pattern | Computer mode detected from URL | - -## Test Counts - -- **Unit + integration tests:** 295 passing (30 files) -- **UAT items:** 17/17 passing diff --git a/docs/uat/uat-plan.md b/docs/uat/uat-plan.md index 9d2d7d3..7765dc1 100644 --- a/docs/uat/uat-plan.md +++ b/docs/uat/uat-plan.md @@ -1,4 +1,4 @@ -# Asteria UAT Test Plan +# MCP Comet UAT Test Plan Version: 0.1.0 | Date: 2026-04-07 @@ -7,7 +7,7 @@ Version: 0.1.0 | Date: 2026-04-07 ## Prerequisites - Perplexity Comet installed and running -- MCP client configured with asteria server +- MCP client configured with mcp-comet server - Node.js >= 18 - Test environment has network access @@ -288,7 +288,7 @@ Full tool-by-tool validation. - **Tool:** `comet_ask` - **Preconditions:** No active connection to Comet (server just started or disconnected) - **Steps:** - 1. Restart Asteria server or ensure no connection + 1. Restart MCP Comet server or ensure no connection 2. Call `comet_ask` with any prompt - **Expected:** Response is an error indicating connection is required, OR server auto-connects and succeeds - **Actual:** ___ @@ -420,15 +420,15 @@ Full tool-by-tool validation. ## Summary -| Category | Total | Pass | Fail | -|----------|-------|------|------| -| Smoke | 4 | | | -| Functional | 14 | | | -| Error Recovery | 4 | | | -| Mode Switching | 4 | | | -| Cross-Session | 3 | | | -| Tab Management Edge Cases | 2 | | | -| **Total** | **31** | | | +| Category | Total | Pass | Fail | +| ------------------------- | ------ | ---- | ---- | +| Smoke | 4 | | | +| Functional | 14 | | | +| Error Recovery | 4 | | | +| Mode Switching | 4 | | | +| Cross-Session | 3 | | | +| Tab Management Edge Cases | 2 | | | +| **Total** | **31** | | | --- @@ -446,5 +446,5 @@ Full tool-by-tool validation. ## Issue Tracking | Issue # | Test ID | Description | Severity | Status | -|---------|---------|-------------|----------|--------| -| | | | | | +| ------- | ------- | ----------- | -------- | ------ | +| | | | | | diff --git a/asteria.config.example.json b/mcp-comet.config.example.json similarity index 100% rename from asteria.config.example.json rename to mcp-comet.config.example.json diff --git a/package-lock.json b/package-lock.json index 200154b..15f63ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@onestepat4time/asteria", - "version": "1.1.1", + "name": "@onestepat4time/mcp-comet", + "version": "1.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@onestepat4time/asteria", - "version": "1.1.1", + "name": "@onestepat4time/mcp-comet", + "version": "1.1.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", @@ -14,8 +14,7 @@ "zod": "^3.25.76" }, "bin": { - "asteria": "dist/cli.js", - "asteria-comet": "dist/cli.js" + "mcp-comet": "dist/cli.js" }, "devDependencies": { "@biomejs/biome": "^2.4.10", diff --git a/package.json b/package.json index a8615f7..de740d8 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,18 @@ { - "name": "@onestepat4time/asteria", - "version": "1.1.3", - "description": "MCP server for Perplexity Comet browser management", + "name": "@onestepat4time/mcp-comet", + "version": "1.1.4", + "description": "The definitive MCP server for Perplexity Comet browser management", "type": "module", "main": "dist/index.js", "bin": { - "asteria": "./dist/cli.js", - "asteria-comet": "./dist/cli.js" + "mcp-comet": "./dist/cli.js" }, "files": [ "dist/", "README.md", "LICENSE", "CHANGELOG.md", - "asteria.config.example.json" + "mcp-comet.config.example.json" ], "keywords": [ "mcp", @@ -27,7 +26,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/OneStepAt4time/asteria.git" + "url": "git+https://github.com/OneStepAt4time/mcp-comet.git" }, "license": "MIT", "scripts": { diff --git a/scripts/run-uat.ts b/scripts/run-uat.ts index 355ee4e..78e67d6 100644 --- a/scripts/run-uat.ts +++ b/scripts/run-uat.ts @@ -1,80 +1,169 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { writeFileSync } from "fs"; +import { writeFileSync } from 'node:fs' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' -const REPORT_FILE = "docs/uat/uat-report.md"; +const REPORT_FILE = 'docs/uat/uat-report.md' -async function logResult(uatId: string, name: string, status: "Pass" | "Fail", details: string) { - const icon = status === "Pass" ? "✅" : "❌"; - console.log(`${icon} ${uatId} - ${name} : ${status}`); - if (status === "Fail") console.log(` Reason: ${details}`); - return `| ${uatId} | ${name} | ${status} | ${details} |\n`; +type ToolCallResult = Awaited> + +async function logResult( + uatId: string, + name: string, + status: 'Pass' | 'Fail', + details: string, +): Promise { + const icon = status === 'Pass' ? '✅' : '❌' + // biome-ignore lint/suspicious/noConsole: UAT script intentionally prints progress + console.log(`${icon} ${uatId} - ${name} : ${status}`) + if (status === 'Fail') { + // biome-ignore lint/suspicious/noConsole: UAT script intentionally prints failure details + console.log(` Reason: ${details}`) + } + return `| ${uatId} | ${name} | ${status} | ${details} |\n` } -async function runUAT() { - let reportMd = `# Asteria UAT Execution Report\n\n| Test ID | Name | Status | Details |\n|---|---|---|---|\n`; - - const transport = new StdioClientTransport({ command: "node", args: ["dist/index.js"] }); - const client = new Client({ name: "uat-client", version: "1.0.0" }, { capabilities: {} }); - await client.connect(transport); - - try { - let res: any; - - // Smoke 1-4 (Already proven mostly, doing quickly) - res = await client.callTool({ name: "comet_connect", arguments: {} }); - reportMd += await logResult("UAT-001", "Connect", "Pass", "Connected to port 9222"); - - res = await client.callTool({ name: "comet_ask", arguments: { prompt: "What is 10+10?", newChat: true } }); - reportMd += await logResult("UAT-002", "Ask Simple Query", res.content[0].text.includes("20") ? "Pass" : "Fail", "Got 20"); - - res = await client.callTool({ name: "comet_poll", arguments: {} }); - reportMd += await logResult("UAT-003", "Poll Agent Status", res.content[0].text.includes("status") ? "Pass" : "Fail", "Valid fields"); - - res = await client.callTool({ name: "comet_screenshot", arguments: { format: "jpeg" } }); - if (res.isError) { - reportMd += await logResult("UAT-010", "Screenshot JPEG", "Fail", "Tool returned an error instead of image"); - } else { - reportMd += await logResult("UAT-010", "Screenshot JPEG", res.content[0].mimeType === "image/jpeg" ? "Pass" : "Fail", res.content[0].mimeType === "image/jpeg" ? "Got JPEG format" : "MIME Type mismatch or not image"); - } - - // Mode Switching - res = await client.callTool({ name: "comet_mode", arguments: {} }); - reportMd += await logResult("UAT-011", "Query Mode", res.content[0].text.includes("Current mode") ? "Pass" : "Fail", res.content[0].text.replace(/\n/g, ' ')); - - // Tab Management - res = await client.callTool({ name: "comet_list_tabs", arguments: {} }); - const tabsOutput = res.content[0].text; - reportMd += await logResult("UAT-012", "List Tabs", tabsOutput.includes("Main") ? "Pass" : "Fail", "Categorized tabs displayed"); - - // Sources & Conversations - res = await client.callTool({ name: "comet_get_sources", arguments: {} }); - reportMd += await logResult("UAT-015", "Get Sources", res.content[0].text.includes("Sources") || res.content[0].text.includes("No sources") ? "Pass" : "Fail", "Sources retrieved"); - - res = await client.callTool({ name: "comet_list_conversations", arguments: {} }); - reportMd += await logResult("UAT-016", "List Conversations", res.content[0].text.includes("Conversations") || res.content[0].text.includes("No conversation") ? "Pass" : "Fail", "Conversations retrieved"); - - res = await client.callTool({ name: "comet_get_page_content", arguments: { maxLength: 500 } }); - reportMd += await logResult("UAT-018", "Get Page Content", res.content[0].text.includes("Title:") ? "Pass" : "Fail", "Content parsed"); - - // Error Recovery: Timeout - res = await client.callTool({ name: "comet_ask", arguments: { prompt: "Write a complete 100 page essay on AI", timeout: 2000 } }); - reportMd += await logResult("UAT-020", "Timeout returns partial", res.content[0].text.includes("still working") || res.content[0].text.includes("Partial response") ? "Pass" : "Fail", "Handled timeout gracefully"); - - // Mode Switch - res = await client.callTool({ name: "comet_mode", arguments: { mode: "learn" } }); - reportMd += await logResult("UAT-024", "Switch to Learn", res.content[0].text.includes("Mode switch") ? "Pass" : "Fail", "Menu interacted"); - - res = await client.callTool({ name: "comet_mode", arguments: { mode: "standard" } }); - reportMd += await logResult("UAT-026", "Switch back to Standard", "Pass", "Restored standard mode"); - - } catch(err) { - console.error("Test execution aborted due to error:", err); - } finally { - await client.close(); - writeFileSync(REPORT_FILE, reportMd, "utf8"); - console.log(`\nReport written to ${REPORT_FILE}`); +async function runUAT(): Promise { + let reportMd = + '# MCP Comet UAT Execution Report\n\n| Test ID | Name | Status | Details |\n|---|---|---|---|\n' + + const transport = new StdioClientTransport({ command: 'node', args: ['dist/index.js'] }) + const client = new Client({ name: 'uat-client', version: '1.0.0' }, { capabilities: {} }) + await client.connect(transport) + + try { + let res: ToolCallResult + + res = await client.callTool({ name: 'comet_connect', arguments: {} }) + reportMd += await logResult('UAT-001', 'Connect', 'Pass', 'Connected to port 9222') + + res = await client.callTool({ + name: 'comet_ask', + arguments: { prompt: 'What is 10+10?', newChat: true }, + }) + reportMd += await logResult( + 'UAT-002', + 'Ask Simple Query', + res.content[0].text.includes('20') ? 'Pass' : 'Fail', + 'Got 20', + ) + + res = await client.callTool({ name: 'comet_poll', arguments: {} }) + reportMd += await logResult( + 'UAT-003', + 'Poll Agent Status', + res.content[0].text.includes('status') ? 'Pass' : 'Fail', + 'Valid fields', + ) + + res = await client.callTool({ name: 'comet_screenshot', arguments: { format: 'jpeg' } }) + if (res.isError) { + reportMd += await logResult( + 'UAT-010', + 'Screenshot JPEG', + 'Fail', + 'Tool returned an error instead of image', + ) + } else { + reportMd += await logResult( + 'UAT-010', + 'Screenshot JPEG', + res.content[0].mimeType === 'image/jpeg' ? 'Pass' : 'Fail', + res.content[0].mimeType === 'image/jpeg' + ? 'Got JPEG format' + : 'MIME Type mismatch or not image', + ) } + + res = await client.callTool({ name: 'comet_mode', arguments: {} }) + reportMd += await logResult( + 'UAT-011', + 'Query Mode', + res.content[0].text.includes('Current mode') ? 'Pass' : 'Fail', + res.content[0].text.replace(/\n/g, ' '), + ) + + res = await client.callTool({ name: 'comet_list_tabs', arguments: {} }) + const tabsOutput = res.content[0].text + reportMd += await logResult( + 'UAT-012', + 'List Tabs', + tabsOutput.includes('Main') ? 'Pass' : 'Fail', + 'Categorized tabs displayed', + ) + + res = await client.callTool({ name: 'comet_get_sources', arguments: {} }) + reportMd += await logResult( + 'UAT-015', + 'Get Sources', + res.content[0].text.includes('Sources') || res.content[0].text.includes('No sources') + ? 'Pass' + : 'Fail', + 'Sources retrieved', + ) + + res = await client.callTool({ name: 'comet_list_conversations', arguments: {} }) + reportMd += await logResult( + 'UAT-016', + 'List Conversations', + res.content[0].text.includes('Conversations') || + res.content[0].text.includes('No conversation') + ? 'Pass' + : 'Fail', + 'Conversations retrieved', + ) + + res = await client.callTool({ + name: 'comet_get_page_content', + arguments: { maxLength: 500 }, + }) + reportMd += await logResult( + 'UAT-018', + 'Get Page Content', + res.content[0].text.includes('Title:') ? 'Pass' : 'Fail', + 'Content parsed', + ) + + res = await client.callTool({ + name: 'comet_ask', + arguments: { prompt: 'Write a complete 100 page essay on AI', timeout: 2000 }, + }) + reportMd += await logResult( + 'UAT-020', + 'Timeout returns partial', + res.content[0].text.includes('still working') || + res.content[0].text.includes('Partial response') + ? 'Pass' + : 'Fail', + 'Handled timeout gracefully', + ) + + res = await client.callTool({ name: 'comet_mode', arguments: { mode: 'learn' } }) + reportMd += await logResult( + 'UAT-024', + 'Switch to Learn', + res.content[0].text.includes('Mode switch') ? 'Pass' : 'Fail', + 'Menu interacted', + ) + + await client.callTool({ name: 'comet_mode', arguments: { mode: 'standard' } }) + reportMd += await logResult( + 'UAT-026', + 'Switch back to Standard', + 'Pass', + 'Restored standard mode', + ) + } catch (err) { + // biome-ignore lint/suspicious/noConsole: UAT script intentionally prints fatal error + console.error('Test execution aborted due to error:', err) + } finally { + await client.close() + writeFileSync(REPORT_FILE, reportMd, 'utf8') + // biome-ignore lint/suspicious/noConsole: UAT script intentionally prints report path + console.log(`\nReport written to ${REPORT_FILE}`) + } } -runUAT().catch(console.error); \ No newline at end of file +runUAT().catch((err) => { + // biome-ignore lint/suspicious/noConsole: UAT script intentionally prints unhandled failure + console.error(err) +}) diff --git a/src/cli.ts b/src/cli.ts index 225415b..183ae58 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,10 +8,10 @@ const { version } = JSON.parse(readFileSync(resolve(__dirname, '../package.json' function printUsage(): void { // biome-ignore lint/suspicious/noConsole: CLI output - console.error(`asteria v${version} — MCP server for Perplexity Comet browser + console.error(`mcp-comet v${version} — MCP server for Perplexity Comet browser USAGE: - asteria [command] + mcp-comet [command] COMMANDS: start Start MCP stdio server (default) @@ -21,28 +21,28 @@ COMMANDS: --help Print this help message TOOL CALL: - asteria call [json_args] - asteria call comet_connect - asteria call comet_ask '{"prompt": "What is 2+2?"}' - asteria call comet_poll - asteria call comet_screenshot '{"format": "jpeg"}' - asteria call comet_list_tabs - asteria call comet_mode '{"mode": "deep-research"}' - asteria call comet_get_sources - asteria call comet_wait - asteria comet_stop`) + mcp-comet call [json_args] + mcp-comet call comet_connect + mcp-comet call comet_ask '{"prompt": "What is 2+2?"}' + mcp-comet call comet_poll + mcp-comet call comet_screenshot '{"format": "jpeg"}' + mcp-comet call comet_list_tabs + mcp-comet call comet_mode '{"mode": "deep-research"}' + mcp-comet call comet_get_sources + mcp-comet call comet_wait + mcp-comet comet_stop`) } function printVersion(): void { // biome-ignore lint/suspicious/noConsole: CLI output - console.error(`asteria v${version}`) + console.error(`mcp-comet v${version}`) } async function runDetect(): Promise { const { getCometPath, isCometProcessRunning } = await import('./cdp/browser.js') // biome-ignore lint/suspicious/noConsole: CLI output - console.error('Asteria Detect — Comet Browser Status\n') + console.error('MCP Comet Detect — Comet Browser Status\n') const running = isCometProcessRunning() // biome-ignore lint/suspicious/noConsole: CLI output @@ -107,7 +107,7 @@ async function runCall(args: string[]): Promise { if (!toolName) { // biome-ignore lint/suspicious/noConsole: CLI output - console.error('Usage: asteria call [json_args]') + console.error('Usage: mcp-comet call [json_args]') // biome-ignore lint/suspicious/noConsole: CLI output console.error(`Available tools: ${TOOLS.join(', ')}`) process.exit(1) @@ -139,7 +139,7 @@ async function runCall(args: string[]): Promise { params: { protocolVersion: '2024-11-05', capabilities: {}, - clientInfo: { name: 'asteria-cli', version: `v${version}` }, + clientInfo: { name: 'mcp-comet-cli', version: `v${version}` }, }, }) @@ -196,7 +196,7 @@ async function runCall(args: string[]): Promise { console.log(content.text) } else if (content.type === 'image' && content.data) { const ext = content.mimeType === 'image/jpeg' ? 'jpg' : 'png' - const filename = `asteria-screenshot-${Date.now()}.${ext}` + const filename = `mcp-comet-screenshot-${Date.now()}.${ext}` writeFileSync(filename, Buffer.from(content.data, 'base64')) // biome-ignore lint/suspicious/noConsole: CLI output console.error(`Screenshot saved: ${filename}`) @@ -213,7 +213,7 @@ async function runCall(args: string[]): Promise { child.stderr.on('data', (chunk: Buffer) => { const msg = chunk.toString().trim() - if (msg) process.stderr.write(`[asteria] ${msg}\n`) + if (msg) process.stderr.write(`[mcp-comet] ${msg}\n`) }) function shutdown(code: number): void { @@ -264,7 +264,7 @@ async function main(): Promise { // biome-ignore lint/suspicious/noConsole: CLI output console.error(`Unknown command: ${command}`) // biome-ignore lint/suspicious/noConsole: CLI output - console.error("Run 'asteria --help' for usage.") + console.error("Run 'mcp-comet --help' for usage.") process.exit(1) } diff --git a/src/config.ts b/src/config.ts index 3c50cfc..9d34b76 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,12 +23,12 @@ function env(name: string): string | undefined { } /** - * Load config from asteria.config.json in cwd if it exists. + * Load config from mcp-comet.config.json in cwd if it exists. * Returns empty object on file-not-found or parse error. */ function loadConfigFile(): Partial { try { - const configPath = resolve(process.cwd(), 'asteria.config.json') + const configPath = resolve(process.cwd(), 'mcp-comet.config.json') const raw = readFileSync(configPath, 'utf-8') const parsed = JSON.parse(raw) as Record // Only pick keys that match CometConfig @@ -46,7 +46,7 @@ function loadConfigFile(): Partial { } // Invalid JSON or other read error — warn and continue with defaults process.stderr.write( - `[asteria:warn] Failed to load asteria.config.json: ${err instanceof Error ? err.message : err}\n`, + `[mcp-comet:warn] Failed to load mcp-comet.config.json: ${err instanceof Error ? err.message : err}\n`, ) return {} } @@ -96,51 +96,51 @@ export function loadConfig(overrides?: Partial): CometConfig { // Only include env vars that are actually set (non-undefined) const envConfig: Partial = {} - const portEnv = env('ASTERIA_PORT') + const portEnv = env('COMET_PORT') if (portEnv !== undefined) envConfig.port = Number(portEnv) || DEFAULTS.port - const timeoutEnv = env('ASTERIA_TIMEOUT') + const timeoutEnv = env('COMET_TIMEOUT') if (timeoutEnv !== undefined) envConfig.timeout = Number(timeoutEnv) || DEFAULTS.timeout const cometPathEnv = env('COMET_PATH') if (cometPathEnv !== undefined) envConfig.cometPath = cometPathEnv - const responseTimeoutEnv = env('ASTERIA_RESPONSE_TIMEOUT') + const responseTimeoutEnv = env('COMET_RESPONSE_TIMEOUT') if (responseTimeoutEnv !== undefined) envConfig.responseTimeout = Number(responseTimeoutEnv) || DEFAULTS.responseTimeout - const logLevelEnv = env('ASTERIA_LOG_LEVEL') + const logLevelEnv = env('COMET_LOG_LEVEL') if (logLevelEnv !== undefined) envConfig.logLevel = logLevelEnv as CometConfig['logLevel'] - const screenshotFormatEnv = env('ASTERIA_SCREENSHOT_FORMAT') + const screenshotFormatEnv = env('COMET_SCREENSHOT_FORMAT') if (screenshotFormatEnv !== undefined) envConfig.screenshotFormat = screenshotFormatEnv as CometConfig['screenshotFormat'] - const screenshotQualityEnv = env('ASTERIA_SCREENSHOT_QUALITY') + const screenshotQualityEnv = env('COMET_SCREENSHOT_QUALITY') if (screenshotQualityEnv !== undefined) envConfig.screenshotQuality = Number(screenshotQualityEnv) || DEFAULTS.screenshotQuality - const windowWidthEnv = env('ASTERIA_WINDOW_WIDTH') + const windowWidthEnv = env('COMET_WINDOW_WIDTH') if (windowWidthEnv !== undefined) envConfig.windowWidth = Number(windowWidthEnv) || DEFAULTS.windowWidth - const windowHeightEnv = env('ASTERIA_WINDOW_HEIGHT') + const windowHeightEnv = env('COMET_WINDOW_HEIGHT') if (windowHeightEnv !== undefined) envConfig.windowHeight = Number(windowHeightEnv) || DEFAULTS.windowHeight - const maxReconnectEnv = env('ASTERIA_MAX_RECONNECT') + const maxReconnectEnv = env('COMET_MAX_RECONNECT') if (maxReconnectEnv !== undefined) envConfig.maxReconnectAttempts = Number(maxReconnectEnv) || DEFAULTS.maxReconnectAttempts - const reconnectDelayEnv = env('ASTERIA_RECONNECT_DELAY') + const reconnectDelayEnv = env('COMET_RECONNECT_DELAY') if (reconnectDelayEnv !== undefined) envConfig.maxReconnectDelay = Number(reconnectDelayEnv) || DEFAULTS.maxReconnectDelay - const pollIntervalEnv = env('ASTERIA_POLL_INTERVAL') + const pollIntervalEnv = env('COMET_POLL_INTERVAL') if (pollIntervalEnv !== undefined) envConfig.pollInterval = Number(pollIntervalEnv) || DEFAULTS.pollInterval - const userDataDirEnv = env('ASTERIA_USER_DATA_DIR') + const userDataDirEnv = env('COMET_USER_DATA_DIR') if (userDataDirEnv !== undefined) envConfig.userDataDir = userDataDirEnv return validatedConfig({ diff --git a/src/errors.ts b/src/errors.ts index 64f15d1..5105557 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,4 @@ -export class AsteriaError extends Error { +export class CometError extends Error { code: string context: Record override cause?: unknown @@ -10,70 +10,70 @@ export class AsteriaError extends Error { cause?: unknown, ) { super(message) - this.name = 'AsteriaError' + this.name = 'CometError' this.code = code this.context = context if (cause) this.cause = cause } } -export class CDPConnectionError extends AsteriaError { +export class CDPConnectionError extends CometError { constructor(message: string, context?: Record, cause?: unknown) { super(message, 'CDP_CONNECTION_FAILED', context, cause) this.name = 'CDPConnectionError' } } -export class CometNotFoundError extends AsteriaError { +export class CometNotFoundError extends CometError { constructor(message: string, context?: Record) { super(message, 'COMET_NOT_FOUND', context) this.name = 'CometNotFoundError' } } -export class CometLaunchError extends AsteriaError { +export class CometLaunchError extends CometError { constructor(message: string, context?: Record, cause?: unknown) { super(message, 'COMET_LAUNCH_FAILED', context, cause) this.name = 'CometLaunchError' } } -export class TabNotFoundError extends AsteriaError { +export class TabNotFoundError extends CometError { constructor(message: string, context?: Record) { super(message, 'TAB_NOT_FOUND', context) this.name = 'TabNotFoundError' } } -export class TimeoutError extends AsteriaError { +export class TimeoutError extends CometError { constructor(message: string, context?: Record) { super(message, 'TIMEOUT', context) this.name = 'TimeoutError' } } -export class EvaluationError extends AsteriaError { +export class EvaluationError extends CometError { constructor(message: string, context?: Record, cause?: unknown) { super(message, 'EVALUATION_FAILED', context, cause) this.name = 'EvaluationError' } } -export class SelectorError extends AsteriaError { +export class SelectorError extends CometError { constructor(message: string, context?: Record) { super(message, 'SELECTOR_NOT_FOUND', context) this.name = 'SelectorError' } } -export class AgentError extends AsteriaError { +export class AgentError extends CometError { constructor(message: string, context?: Record) { super(message, 'AGENT_ERROR', context) this.name = 'AgentError' } } -export class ConfigurationError extends AsteriaError { +export class ConfigurationError extends CometError { constructor(message: string, context?: Record) { super(message, 'CONFIG_ERROR', context) this.name = 'ConfigurationError' @@ -84,7 +84,7 @@ export function toMcpError(err: unknown): { content: Array<{ type: 'text'; text: string }> isError: boolean } { - if (err instanceof AsteriaError) { + if (err instanceof CometError) { return { content: [{ type: 'text', text: `[${err.code}] ${err.message}` }], isError: true, diff --git a/src/logger.ts b/src/logger.ts index 44c10b4..90cf979 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -17,22 +17,22 @@ export function createLogger(level: CometConfig['logLevel']): Logger { debug: rank <= 0 ? // biome-ignore lint/suspicious/noConsole: intentional debug output to stderr - (msg, ...args) => console.error('[asteria:debug]', msg, ...args) + (msg, ...args) => console.error('[mcp-comet:debug]', msg, ...args) : () => {}, info: rank <= 1 ? // biome-ignore lint/suspicious/noConsole: intentional info output to stderr - (msg, ...args) => console.error('[asteria:info]', msg, ...args) + (msg, ...args) => console.error('[mcp-comet:info]', msg, ...args) : () => {}, warn: rank <= 2 ? // biome-ignore lint/suspicious/noConsole: intentional warn output to stderr - (msg, ...args) => console.warn('[asteria:warn]', msg, ...args) + (msg, ...args) => console.warn('[mcp-comet:warn]', msg, ...args) : () => {}, error: rank <= 3 ? // biome-ignore lint/suspicious/noConsole: intentional error output to stderr - (msg, ...args) => console.error('[asteria:error]', msg, ...args) + (msg, ...args) => console.error('[mcp-comet:error]', msg, ...args) : () => {}, } } diff --git a/src/server.ts b/src/server.ts index a43b587..be4cc3a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -292,20 +292,15 @@ function formatTabs(categorized: CategorizedTabs): string { return lines.length > 0 ? lines.join('\n') : 'No tabs found.' } -/** Heuristic: does the status contain a substantial response? */ -function hasSubstantialResponse(status: RawAgentStatus): boolean { - return !!status.response && status.response.length > 50 -} - // --------------------------------------------------------------------------- // Server setup & start // --------------------------------------------------------------------------- export async function startServer(): Promise { - logger.info('Starting Asteria MCP server...') + logger.info('Starting MCP Comet server...') const server = new McpServer({ - name: 'asteria', + name: 'mcp-comet', version: '0.1.0', }) @@ -357,12 +352,10 @@ export async function startServer(): Promise { 'comet_ask', 'Send a prompt to Perplexity Comet and return immediately. Supports newChat to start fresh. Use comet_poll or comet_wait to get the response.', askShape, - async ({ prompt, newChat, timeout }) => { + async ({ prompt, newChat }) => { try { await ensureConnected() const normalizedPrompt = client.normalizePrompt(prompt) - const effectiveTimeout = timeout ?? config.responseTimeout - // Handle newChat or tab management if (newChat) { await client.closeExtraTabs() @@ -380,7 +373,7 @@ export async function startServer(): Promise { // PRE-SEND STATE CAPTURE const preSendRaw = await client.safeEvaluate(buildPreSendStateScript()) - const preSendState = JSON.parse(String(extractValue(preSendRaw))) as { + const _preSendState = JSON.parse(String(extractValue(preSendRaw))) as { proseCount: number lastProseText: string } @@ -398,7 +391,9 @@ export async function startServer(): Promise { const submitResult = await client.safeEvaluate(buildSubmitPromptScript()) logger.debug('Submit result:', extractValue(submitResult)) - return textResult('Prompt submitted successfully. Use comet_poll to track status or comet_wait to block until completion.') + return textResult( + 'Prompt submitted successfully. Use comet_poll to track status or comet_wait to block until completion.', + ) } catch (err) { return toMcpError(err) } @@ -790,7 +785,7 @@ export async function startServer(): Promise { // Connect via stdio const transport = new StdioServerTransport() await server.connect(transport) - logger.info('Asteria MCP server connected via stdio.') + logger.info('MCP Comet server connected via stdio.') // Signal handlers const shutdown = async () => { diff --git a/src/version.ts b/src/version.ts index f3e0555..7996e3b 100644 --- a/src/version.ts +++ b/src/version.ts @@ -13,7 +13,7 @@ export async function detectCometVersion(port: number): Promise { }) if (!resp.ok) { process.stderr.write( - '[asteria:warn] Comet version detection: non-OK response, using default selectors\n', + '[mcp-comet:warn] Comet version detection: non-OK response, using default selectors\n', ) const { getSelectorsForVersion } = await import('./selectors/index.js') return { chromeMajor: 0, browser: 'Unknown', selectors: getSelectorsForVersion(0) } @@ -25,7 +25,9 @@ export async function detectCometVersion(port: number): Promise { const { getSelectorsForVersion } = await import('./selectors/index.js') return { chromeMajor, browser, selectors: getSelectorsForVersion(chromeMajor) } } catch { - process.stderr.write('[asteria:warn] Comet version detection failed, using default selectors\n') + process.stderr.write( + '[mcp-comet:warn] Comet version detection failed, using default selectors\n', + ) const { getSelectorsForVersion } = await import('./selectors/index.js') return { chromeMajor: 0, browser: 'Unknown', selectors: getSelectorsForVersion(0) } } diff --git a/tests/integration/tools/core-tools.test.ts b/tests/integration/tools/core-tools.test.ts index 7d753e6..235b3cd 100644 --- a/tests/integration/tools/core-tools.test.ts +++ b/tests/integration/tools/core-tools.test.ts @@ -139,9 +139,9 @@ describe('Core tool handlers', () => { describe('comet_ask', () => { it('returns immediate submission message without polling', async () => { - let callCount = 0 + let _callCount = 0 mocks.safeEvaluate.mockImplementation(async () => { - callCount++ + _callCount++ return { result: { value: '{"proseCount":0,"lastProseText":""}' } } }) diff --git a/tests/unit/cli-run.test.ts b/tests/unit/cli-run.test.ts index 0b68c68..5eb3e69 100644 --- a/tests/unit/cli-run.test.ts +++ b/tests/unit/cli-run.test.ts @@ -163,7 +163,7 @@ describe('CLI runCall', () => { expect(exitCode).toBe(1) const output = consoleOutput.join('\n') - expect(output).toContain('Usage: asteria call') + expect(output).toContain('Usage: mcp-comet call') }) it('unknown tool shows error and exits', async () => { diff --git a/tests/unit/cli.test.ts b/tests/unit/cli.test.ts index 4e27b8a..76e896f 100644 --- a/tests/unit/cli.test.ts +++ b/tests/unit/cli.test.ts @@ -18,25 +18,25 @@ function runCli(args: string): { stdout: string; stderr: string; status: number describe('CLI', () => { it('--help prints usage', () => { const { stderr } = runCli('--help') - expect(stderr).toContain('asteria') + expect(stderr).toContain('mcp-comet') expect(stderr).toContain('USAGE') expect(stderr).toContain('COMMANDS') }) it('-h prints usage (short form)', () => { const { stderr } = runCli('-h') - expect(stderr).toContain('asteria') + expect(stderr).toContain('mcp-comet') expect(stderr).toContain('USAGE') }) it('--version prints version', () => { const { stderr } = runCli('--version') - expect(stderr).toMatch(/asteria v\d+\.\d+\.\d+/) + expect(stderr).toMatch(/mcp-comet v\d+\.\d+\.\d+/) }) it('-v prints version (short form)', () => { const { stderr } = runCli('-v') - expect(stderr).toMatch(/asteria v\d+\.\d+\.\d+/) + expect(stderr).toMatch(/mcp-comet v\d+\.\d+\.\d+/) }) it('unknown command exits with error', () => { @@ -48,7 +48,7 @@ describe('CLI', () => { it('call without tool name shows usage', () => { const { stderr, status } = runCli('call') expect(status).toBe(1) - expect(stderr).toContain('Usage: asteria call') + expect(stderr).toContain('Usage: mcp-comet call') }) it('call with unknown tool shows error', () => { diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 4a88fda..e50923c 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -2,7 +2,7 @@ import { existsSync, rmSync, writeFileSync } from 'node:fs' import { resolve } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const configPath = resolve(process.cwd(), 'asteria.config.json') +const configPath = resolve(process.cwd(), 'mcp-comet.config.json') describe('loadConfig', () => { beforeEach(() => { @@ -35,10 +35,10 @@ describe('loadConfig', () => { }) it('env vars override defaults', async () => { - vi.stubEnv('ASTERIA_PORT', '9223') - vi.stubEnv('ASTERIA_TIMEOUT', '60000') + vi.stubEnv('COMET_PORT', '9223') + vi.stubEnv('COMET_TIMEOUT', '60000') vi.stubEnv('COMET_PATH', '/custom/comet') - vi.stubEnv('ASTERIA_LOG_LEVEL', 'debug') + vi.stubEnv('COMET_LOG_LEVEL', 'debug') const { loadConfig } = await import('../../src/config.js') const config = loadConfig() expect(config.port).toBe(9223) @@ -48,7 +48,7 @@ describe('loadConfig', () => { }) it('overrides take precedence over env vars', async () => { - vi.stubEnv('ASTERIA_PORT', '9223') + vi.stubEnv('COMET_PORT', '9223') const { loadConfig } = await import('../../src/config.js') const config = loadConfig({ port: 9224, logLevel: 'warn' }) expect(config.port).toBe(9224) @@ -65,7 +65,7 @@ describe('loadConfig', () => { }) it('falls back to default for invalid env var number values', async () => { - vi.stubEnv('ASTERIA_PORT', 'not-a-number') + vi.stubEnv('COMET_PORT', 'not-a-number') const { loadConfig } = await import('../../src/config.js') const config = loadConfig() expect(config.port).toBe(9222) // Falls back to default @@ -78,7 +78,7 @@ describe('loadConfig', () => { }) it('overrides parameter takes precedence over env vars', async () => { - vi.stubEnv('ASTERIA_PORT', '8080') + vi.stubEnv('COMET_PORT', '8080') const { loadConfig } = await import('../../src/config.js') const config = loadConfig({ port: 9999 }) expect(config.port).toBe(9999) // Override wins over env var @@ -142,64 +142,64 @@ describe('loadConfig', () => { }) it('falls back to default for invalid logLevel env var', async () => { - vi.stubEnv('ASTERIA_LOG_LEVEL', 'verbose') + vi.stubEnv('COMET_LOG_LEVEL', 'verbose') const { loadConfig } = await import('../../src/config.js') const cfg = loadConfig() expect(cfg.logLevel).toBe('info') }) it('falls back to default for invalid screenshotFormat env var', async () => { - vi.stubEnv('ASTERIA_SCREENSHOT_FORMAT', 'gif') + vi.stubEnv('COMET_SCREENSHOT_FORMAT', 'gif') const { loadConfig } = await import('../../src/config.js') const cfg = loadConfig() expect(cfg.screenshotFormat).toBe('png') }) }) - describe('env var branches — each ASTERIA_* env var', () => { + describe('env var branches — each COMET_* env var', () => { const envVars = [ - { name: 'ASTERIA_PORT', key: 'port' as const, value: '9999', expected: 9999 }, - { name: 'ASTERIA_TIMEOUT', key: 'timeout' as const, value: '10000', expected: 10000 }, + { name: 'COMET_PORT', key: 'port' as const, value: '9999', expected: 9999 }, + { name: 'COMET_TIMEOUT', key: 'timeout' as const, value: '10000', expected: 10000 }, { - name: 'ASTERIA_RESPONSE_TIMEOUT', + name: 'COMET_RESPONSE_TIMEOUT', key: 'responseTimeout' as const, value: '60000', expected: 60000, }, { - name: 'ASTERIA_SCREENSHOT_FORMAT', + name: 'COMET_SCREENSHOT_FORMAT', key: 'screenshotFormat' as const, value: 'jpeg', expected: 'jpeg', }, { - name: 'ASTERIA_SCREENSHOT_QUALITY', + name: 'COMET_SCREENSHOT_QUALITY', key: 'screenshotQuality' as const, value: '90', expected: 90, }, - { name: 'ASTERIA_WINDOW_WIDTH', key: 'windowWidth' as const, value: '1920', expected: 1920 }, + { name: 'COMET_WINDOW_WIDTH', key: 'windowWidth' as const, value: '1920', expected: 1920 }, { - name: 'ASTERIA_WINDOW_HEIGHT', + name: 'COMET_WINDOW_HEIGHT', key: 'windowHeight' as const, value: '1080', expected: 1080, }, { - name: 'ASTERIA_MAX_RECONNECT', + name: 'COMET_MAX_RECONNECT', key: 'maxReconnectAttempts' as const, value: '10', expected: 10, }, { - name: 'ASTERIA_RECONNECT_DELAY', + name: 'COMET_RECONNECT_DELAY', key: 'maxReconnectDelay' as const, value: '10000', expected: 10000, }, - { name: 'ASTERIA_POLL_INTERVAL', key: 'pollInterval' as const, value: '500', expected: 500 }, + { name: 'COMET_POLL_INTERVAL', key: 'pollInterval' as const, value: '500', expected: 500 }, { - name: 'ASTERIA_USER_DATA_DIR', + name: 'COMET_USER_DATA_DIR', key: 'userDataDir' as const, value: '/tmp/comet-profile', expected: '/tmp/comet-profile', @@ -216,7 +216,7 @@ describe('loadConfig', () => { } it('falls back to default for invalid number env var', async () => { - vi.stubEnv('ASTERIA_PORT', 'not-a-number') + vi.stubEnv('COMET_PORT', 'not-a-number') const { loadConfig } = await import('../../src/config.js') const config = loadConfig() expect(config.port).toBe(9222) diff --git a/tests/unit/errors.test.ts b/tests/unit/errors.test.ts index 91b6533..3779aff 100644 --- a/tests/unit/errors.test.ts +++ b/tests/unit/errors.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' import { AgentError, - AsteriaError, CDPConnectionError, + CometError, CometLaunchError, CometNotFoundError, ConfigurationError, @@ -13,9 +13,9 @@ import { toMcpError, } from '../../src/errors.js' -describe('AsteriaError', () => { +describe('CometError', () => { it('has code, message, context, and cause', () => { - const err = new AsteriaError('test', 'TEST_CODE', { key: 'val' }) + const err = new CometError('test', 'TEST_CODE', { key: 'val' }) expect(err.message).toBe('test') expect(err.code).toBe('TEST_CODE') expect(err.context).toEqual({ key: 'val' }) @@ -27,7 +27,7 @@ describe('Error subclasses', () => { it('CDPConnectionError', () => { const e = new CDPConnectionError('x') expect(e.code).toBe('CDP_CONNECTION_FAILED') - expect(e).toBeInstanceOf(AsteriaError) + expect(e).toBeInstanceOf(CometError) }) it('CometNotFoundError', () => { const e = new CometNotFoundError('x') @@ -65,7 +65,7 @@ describe('Error subclasses', () => { }) describe('toMcpError', () => { - it('converts AsteriaError to MCP format', () => { + it('converts CometError to MCP format', () => { const r = toMcpError(new CDPConnectionError('conn fail')) expect(r.isError).toBe(true) expect(r.content[0].text).toContain('CDP_CONNECTION_FAILED') diff --git a/tests/unit/logger.test.ts b/tests/unit/logger.test.ts index 7b61b46..1c1ac43 100644 --- a/tests/unit/logger.test.ts +++ b/tests/unit/logger.test.ts @@ -21,7 +21,7 @@ describe('Logger', () => { const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) const logger = createLogger('debug') logger.debug('visible') - expect(spy).toHaveBeenCalledWith('[asteria:debug]', 'visible') + expect(spy).toHaveBeenCalledWith('[mcp-comet:debug]', 'visible') spy.mockRestore() }) @@ -30,7 +30,7 @@ describe('Logger', () => { const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) const logger = createLogger('info') logger.info('info msg') - expect(spy).toHaveBeenCalledWith('[asteria:info]', 'info msg') + expect(spy).toHaveBeenCalledWith('[mcp-comet:info]', 'info msg') spy.mockRestore() }) @@ -39,7 +39,7 @@ describe('Logger', () => { const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const logger = createLogger('warn') logger.warn('warning') - expect(spy).toHaveBeenCalledWith('[asteria:warn]', 'warning') + expect(spy).toHaveBeenCalledWith('[mcp-comet:warn]', 'warning') spy.mockRestore() }) @@ -48,7 +48,7 @@ describe('Logger', () => { const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) const logger = createLogger('error') logger.error('err') - expect(spy).toHaveBeenCalledWith('[asteria:error]', 'err') + expect(spy).toHaveBeenCalledWith('[mcp-comet:error]', 'err') spy.mockRestore() }) })