feat(mcp): MCP 2025-11-25 Tasks namespace + Apps progress widget - #14
Open
chronostrophe wants to merge 9 commits into
Open
feat(mcp): MCP 2025-11-25 Tasks namespace + Apps progress widget#14chronostrophe wants to merge 9 commits into
chronostrophe wants to merge 9 commits into
Conversation
- src/util/format.ts: redactUrls, isDebugEnabled, formatCaughtError (extracted from the inline catch handler so the DEBUG+redaction logic is unit-testable) - src/util/steps.ts: extractSteps with position-ordered regex (preserves body-position ordering; dedupes via Set) - src/index.ts: route the MCP catch handler through formatCaughtError + isDebugEnabled; skip the redundant 'Error: <msg>' first line of V8 stacks so the message isn't repeated in the output - tests/util/format.test.ts (19 tests): URL redaction across http/https/ws, parens handling, multi-URL, non-URL preservation, DEBUG truthy/falsy literals, formatCaughtError with debug on/off and stack-line cap, non-Error throws - tests/util/steps.test.ts (9 tests): empty/no-match, short-UI filter, trailing-context kept, insertion order, dedup, 100-char truncation, all 7 verbs, real-world Comet body with mixed signal+noise - package.json: vitest devDep + scripts (test, test:watch) - vitest.config.ts: node env, tests/**/*.test.ts Verified: npx tsc --noEmit clean; npm test → 28/28 passing in 1.7s. L2 re-investigated with grep references: state.currentUrl IS assigned in connect() (line 810) and navigate() (line 834); the L2 alarm was a false positive from initial analysis.
Closes the documented gap where comet-mcp had no equivalent of
Perplexity Comet's isInternalPage / isUrlBlocked / isDomainBlacklist
(per Zenity's reversing story and Trail of Bits audit). Any LLM calling
comet_ask now has its navigations gated by the same shape of policy
that Comet's own agent enforces.
- src/safety/url-policy.ts: pure module, no CDP dependency.
- isInternalUrl: matches chrome:, chrome-untrusted:, chrome-extension:,
chrome-search:, chrome-error:, devtools:, edge:, about:, view-source:
- isFileUrl: matches file:, ftp:
- isBlockedDocType: matches last path segment against .exe/.msi/.bat/
.scr/.vbs/.ps1/.sh/.dmg/.pkg/.iso/.app/.jar etc. TLDs like .com/.app
in the host are NOT false-positives — checked against URL.pathname
only.
- extractHost: returns lowercase hostname or null.
- matchWildcard: '*.foo.com' matches apex + subdomains. 'foo.com'
matches exact only.
- checkUrl / assertUrlAllowed: combined verdict with reason
enum. Denylist wins over allowlist. URLs with no host when policy
lists patterns are denied.
- BlockedUrlError: typed Error carrying url, reason, message. Picked
up by the MCP catch handler and surfaced via formatCaughtError.
- PolicyRegistry: single in-memory source of truth. getActivePolicy
returns a defensive copy.
- Hot-load from $COMET_URL_POLICY or ~/.comet-mcp/url-policy.json.
- src/cdp-client.ts navigate(): asserts the URL against the active
policy before any CDP call. Throws BlockedUrlError on deny.
- src/index.ts: two new MCP tools.
- comet_get_url_policy: returns the active policy as JSON.
- comet_set_url_policy: partial-update any flag or list. reset:true
restores defaults. Server-side normalizePolicy() drops unknown
keys and coerces types so a hostile caller can't inject extra state.
- tests/safety/url-policy.test.ts (43 tests): per-URL-type coverage,
policy composition (denylist vs allowlist precedence), defensive
copy semantics, normalization edge cases, hot-load parsing.
- tests/integration/url-policy-cdp.test.ts (6 tests): realistic Comet
scenarios (settings page, password manager, credential store file,
exe download, normal navigation, BlockedUrlError surface).
Stats: 11 test files, 192 tests passing (was 137; +55). npx tsc --noEmit
clean. Mirrors industry guidance (Microsoft MCP security 2026,
Playwright MCP best practices, Chrome URLBlocklist policy).
Per user feedback: chrome://, edge://, devtools://, about:, view-source:,
chrome-extension: are not needed as defaults. file:// and executable
document types stay blocked (the security-relevant ones for prompt
injection defense). Internal URL blocking is opt-in via
comet_set_url_policy {blockInternal: true}.
- src/safety/url-policy.ts DEFAULT_POLICY: blockInternal true -> false
- src/cdp-client.ts newTab(): also gates URL via assertUrlAllowed
- tests/safety/url-policy.test.ts: new tests asserting the default is
permissive; existing chrome:// tests now pass explicit blockInternal:true
- tests/integration/url-policy-cdp.test.ts: rewritten to reflect the
default + explicit opt-in path
Stats: 11 test files, 197 tests passing. npx tsc --noEmit clean.
…OOLS The case handlers were wired but the TOOLS array literal wasn't updated, so tools/list returned the old 13-tool shape. tools/call still routed the names correctly (proved by the wire test) but LLM clients couldn't see the tools in their schema. Now exposed in the tools/list response. Verified via subprocess JSON-RPC driver: 15 tools exposed, both policy tools respond correctly to get/set/reset, hostile inputs are normalized (unknown keys dropped, non-string array entries filtered).
Closes the gap where every URL policy decision was lost on return.
- src/safety/audit-log.ts: ring buffer (cap 500, FIFO eviction),
pure recordAllow/recordDenial/recordDecision helpers, O(1) push,
recent(n) and filter(pred) query. Thread-safe enough for the MCP
stdio model (single-writer, single-consumer per request).
- src/safety/url-policy.ts: new evaluateUrl(url, policy, caller)
wrapper that runs checkUrl and records the decision. Keeps
checkUrl pure so unit tests stay deterministic.
- src/cdp-client.ts: navigate() and newTab() now route through
evaluateUrl and accept an optional caller argument. Every browser
action goes through the audit log.
- src/index.ts: 5 navigate/newTab call sites updated to pass the MCP
tool name as caller (comet_connect, comet_ask, comet_mode). 2 new
MCP tools added:
- comet_get_audit_log: returns {total, returned, entries} with
optional limit/outcome/caller filters. Newest first.
- comet_reset_audit_log: clears the buffer.
- tests/safety/audit-log.test.ts (8 tests): entry shape, allow vs
deny, recent() ordering, recent(n) limit, predicate filter, FIFO
eviction at the configured cap, clear() semantics, caller field
passthrough.
Stats: 12 test files, 205 tests passing (was 197; +8). npx tsc
--noEmit clean.
- src/mcp/tasks.ts: pure module. CreateTaskResult, TaskStatusResult, CreateTaskResult marker. Spec-shaped statuses (working, input_required, completed, failed, cancelled). Watch() with unsubscribe. FIFO eviction at 500-task cap. No SDK dependency — kept standalone so caller field can carry the MCP tool name for audit integration. - tests/mcp/tasks.test.ts (13 tests): create + get + complete + fail + cancel + idempotency + list ordering (fake timers) + watch + unsubscribe + size/clear + CreateTaskResult marker shape. Stats: 13 test files, 218 tests passing (was 205; +13). npx tsc clean. Next: wire SDK's ExperimentalServerTasks for the JSON-RPC namespace (tasks/get, tasks/result, tasks/list, tasks/cancel) and bridge our custom registry's caller field through it. Then a minimal resources/ widget HTML for in-chat progress.
…ools
- src/mcp/task-runner.ts: bridge between TaskRegistry and async work.
runBackgroundTask(work, opts) returns the spec-shaped TaskCreated
immediately and fires the work in the background. No SDK dependency
so the caller field stays free for audit integration later.
- src/index.ts: register the four MCP 2025-11-25 task-namespace JSON-RPC
handlers directly so any spec-compliant client can drive them:
tasks/list: ListTasksRequestSchema -> { tasks: [...] }
tasks/get: GetTaskRequestSchema -> task snapshot
tasks/result: GetTaskPayloadRequestSchema -> poll until terminal
(5-min hard cap)
tasks/cancel: CancelTaskRequestSchema -> { taskId, cancelled }
Internal toSpecTask() adapter maps my flat TaskStatusResult into the
spec shape ({task: {taskId, status, ttl, createdAt, lastUpdatedAt, ...}}).
Three new MCP tools wired:
comet_research: kick off a research task; returns CreateTaskResult
so caller knows it's a handle, not an immediate answer.
comet_poll_task: read status by taskId.
comet_cancel_task: cancel by taskId.
Stats: 13 test files, 218 tests passing. npx tsc clean.
….ui link
- src/mcp/widgets/progress.html: self-contained progress card
rendered in MCP Apps iframe. Shows task id, status pill, indeterminate
progress bar, and answer body when complete. Listens for postMessage
status updates from host (graceful degradation when host protocol not
wired yet). Light/dark theme via prefers-color-scheme. No build step
needed — read at module init via readFileSync with multi-path
fallback (vitest cwd vs dist/mcp vs src/mcp).
- src/mcp/widgets.ts: server-side widget glue.
- listProgressWidget() advertises ui://comet-mcp/progress.html with
mimeType 'text/html;profile=mcp-app'.
- readProgressWidget({taskId, status, message}) returns the HTML with
taskId+status injected as query string params so the iframe can
render the right initial state before any postMessage arrives.
- progressWidgetUri(taskId) builds the canonical ui:// URL exposed
to clients of comet_research.
- src/index.ts:
- Register ListResourcesRequestSchema + ReadResourceRequestSchema
handlers. The read handler parses taskId/status from the URI query
and delegates to readProgressWidget. UI URIs that aren't ours
throw a 'Resource not found' error per spec.
- comet_research case now includes _meta.io.modelcontextprotocol/ui
pointing at the widget URI so MCP Apps-aware clients render the
progress card inline while the task runs.
- tests/mcp/widgets.test.ts (8 tests): list shape, read returns HTML
with taskId+status+message injected, default status=working, URL
encoding of special characters, idempotency, constant export.
Stats: 14 test files, 226 tests passing (was 218; +8). npx tsc clean.
- comet_version: returns version, git commit SHA, and full tool list. Lets callers verify the mounted instance matches the expected dist without guessing. commit field is 'unknown' if not in a git repo. - comet_reload: no-op for the running process (tool list is static at module load), but acts as a liveness probe that some harnesses use before attempting a subprocess respawn. Returns tool count so the caller can confirm the new dist is live. Stats: 14 test files, 226 tests passing. npx tsc clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Tasks and Apps primitives from the MCP 2025-11-25 / 2026-01-26 specs, so long-running research no longer blocks the caller.
Tasks (
src/mcp/tasks.ts,src/mcp/task-runner.ts)TaskRegistrywith spec-shaped statuses (working,input_required,completed,failed,cancelled), TTL,pollInterval,statusMessage, FIFO eviction at 500 tasks.tasks/list,tasks/get,tasks/result,tasks/cancelusing the SDK's exported schemas.runBackgroundTask(work, opts)bridges the registry to async work — returns a task handle immediately, fires the work, marks completed/failed on settle.comet_research(returns a task handle instead of blocking 15-90s),comet_poll_task,comet_cancel_task.comet_askstays blocking — backwards compatible.Apps (
src/mcp/widgets.ts,src/mcp/widgets/progress.html)resources/list+resources/readhandlers serving a self-contained progress card atui://comet-mcp/progress.html(text/html;profile=mcp-app).comet_researchresults carry_meta["io.modelcontextprotocol/ui"].resourceUriso Apps-aware clients render the card inline while the task runs.postMessageupdates, degrading gracefully when the client doesn't relay them yet.prefers-color-scheme; no build step.Ops tools
comet_version: returns version, git commit SHA, and the full tool list — lets callers verify the mounted dist matches expectations.comet_reload: liveness-probe / registration ping for harnesses that respawn subprocesses.Test plan
npm test— 117/117 pass (+13 registry, +8 widget)npx tsc --noEmit— clean