This file tells AI coding agents (Claude Code, Cursor, Codex, Aider, etc.) how to use WebHarness on a user's Mac through the @mentu/webharness-mcp aggregator. It is not a repo-context file like CLAUDE.md — it's a usage contract.
If you are an AI agent reading this: install
@mentu/webharness-mcp, then usewh_discoverto map the user's running apps,wh_callto invoke their capabilities,wh_dofor natural-language intents, andwh_cortexfor ops + job control. Default toconsent: 'auto', recover gracefully onrequires_consent, and never assume an app stays running between sessions.
Every running WebHarness app on the user's Mac exposes a typed capability surface — vault.read, sql.query, perception.observe, mcp.call, and ~17 more. Each capability is tiered (Safe / Caution / Sensitive / Dangerous) and gated by per-app user consent. The @mentu/webharness-mcp server fronts all running apps through a single MCP connection.
Net effect for your agent:
| You need… | Use… |
|---|---|
| To see what's running | wh_discover({scope: 'apps'}) |
| To call a tool you already know the name of | wh_call({tool: 'canary.vault.read', args}) |
| To express intent in plain English | wh_do({intent: 'embed this string'}) |
| To poll a pending consent / job | wh_cortex({op: 'job.wait', args: {id}}) |
| To check runtime health | wh_cortex({op: 'health'}) |
If the user hasn't installed yet, instruct them:
npm install -g @mentu/webharness-mcp
webharness-mcp-init # auto-wires to 8 MCP clientswebharness-mcp-init detects Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Zed, Gemini, and Codex configs and registers the server in each. After that, restart the MCP client so it picks up the new server.
Always start here. Without it you're guessing at app ids.
Schema:
{
query?: string, // substring filter
scope?: 'apps'|'capabilities'|'recipes'|'all', // default 'all'
app?: string, // restrict to one app
mode?: 'code'|'raw'|'method'|'evidence'|'full', // detail level
}Response:
{
apps: AppEntry[], // running WebHarness apps
capabilities: CapabilityEntry[], // capabilities × apps
recipes: RecipeEntry[], // user-installed recipes
hint?: string, // human-friendly suggestion
}Example:
wh_discover({})
// → apps: [{bundleId: 'io.webharness.canary', name: 'Canary', port: 54775, capabilities: [...]}]
// capabilities: [{name: 'vault.read', tier: 'sensitive', apps: ['io.webharness.canary']}, ...]
// recipes: [{name: 'embed_and_log', triggers: ['embed', 'log']}, ...]Agent rules:
- Call
wh_discoveronce per session, cache the result, refresh ifwh_callreturnsapp_unreachable. - Use bundleId (e.g.
io.webharness.canary) — short names (canary) work but only when unambiguous. - If
apps: [], the user has no running WebHarness apps. Tell them to launch one before retrying.
Three forms:
wh_call({
tool: 'canary.vault.read', // app.capability.method or capability.method
args: { key: 'openai' }, // capability-specific
consent: 'auto', // 'auto' | 'prompt' | 'deny'
})tool formats:
app.capability.method— explicit (e.g.canary.vault.read).capability.method— uses the default app or the only running app.
wh_call({
steps: [
{ tool: 'canary.ane.embed', args: {input: 'hello'}, resultKey: 'vec' },
{ tool: 'canary.vector.store', args: {id: 'h1', vector: '$vec'} },
],
sync: true,
})wh_call({ recipe: 'embed_and_log', params: { intent: 'hello' } })Response shapes — handle all four:
// Success
{ ok: true, value: ... }
// Consent required (only when consent: 'prompt')
{ ok: false, jobId: 'job_xxx', status: 'requires_consent',
requires: { bundleId, capability, tier, reason } }
// Typed error
{ ok: false, error: { code: 'consent_denied', reason: '...' } }
{ ok: false, error: { code: 'app_unreachable', bundleId } }
{ ok: false, error: { code: 'unauthorized', bundleId } }
{ ok: false, error: { code: 'timeout', elapsedMs } }
{ ok: false, error: { code: 'tool_not_found', suggestions: [...] } }
{ ok: false, error: { code: 'unknown_app', available: [...] } }
{ ok: false, error: { code: 'ambiguous_tool', candidates: [...] } }
{ ok: false, error: { code: 'capability_not_declared', needed: '...' } }
{ ok: false, error: { code: 'invalid_response', bundleId, reason } }
{ ok: false, error: { code: 'server_error', bundleId, status, body } }| Mode | Behavior on needsUser |
When to use |
|---|---|---|
'auto' (default) |
Block until the user clicks the consent sheet in the app. | Default — works for any MCP client. |
'prompt' |
Return requires_consent envelope immediately with a jobId. |
LLM clients with strict timeouts. Poll wh_cortex({op:'job.wait', args:{id}}). |
'deny' |
Return consent_denied without prompting. |
Dry-runs or when the user has said no. |
const r1 = await wh_call({ tool: 'canary.network.connect', args, consent: 'prompt' })
if (!r1.ok && r1.status === 'requires_consent') {
const r2 = await wh_cortex({ op: 'job.wait', args: { id: r1.jobId, timeoutMs: 30000 } })
// r2.data.status: 'completed' | 'failed' | 'consent_pending' | 'pending'
// r2.data.result on success, r2.data.error on failure
}Use when you have a user goal in prose, not a known tool name.
Schema:
{
intent: string, // ≤ 4096 bytes
app?: string, // restrict to one app
dry?: boolean, // compile only, don't execute
maxLatencyMs?: number, // soft cap on LLM provider latency
}Provider chain (first hit wins):
- Recipe — exact-match
triggerskeywords. - Heuristic — built-in 18-row keyword table (
embed,vault,screenshot, etc.). - Cortex — interface seam, no implementation in v1.
- LLM — Anthropic-compatible compiler. BYO key from the target app's
vault.get('mcp.do.userKey'). Skipped if no vault key.
Response:
{ ok: true,
plan: [{tool, args}, ...],
value: any, // unless dry: true
provider: 'recipe'|'heuristic'|'cortex'|'compiler',
confidence: 0..1,
cached: boolean }
// or:
{ ok: false, error: { code: 'intent_too_long'|'no_match'|'llm_unavailable'|'app_unreachable'|'call_failed', ... } }The call_failed variant carries the underlying wh_call error in error.callError — surface that to the user, not a generic "failed".
Single tool, dispatched on op. Nine ops:
op |
Purpose |
|---|---|
health |
{ok, uptime, jobs: {pending, completed, failed}} |
restart |
Restart a target app (args.bundleId) |
graph |
Snapshot of all running apps and their capabilities |
job.wait |
Block until job reaches completed / failed |
job.cancel |
Cancel a pending job |
perception.warmup |
Warm the perception provider (always returns ready) |
recipe.list |
List installed recipes |
recipe.add |
Install a recipe from JSON |
recipe.delete |
Remove a recipe |
Examples:
wh_cortex({op: 'health'})
// → {op: 'health', data: {ok: true, uptime: 142, jobs: {pending: 1, completed: 7, failed: 0}}}
wh_cortex({op: 'job.wait', args: {id: 'job_abc', timeoutMs: 10000}})
// → {op: 'job.wait', data: {id, status: 'completed', result: ...}}
wh_cortex({op: 'recipe.add', args: {recipe: {name, triggers: [...], steps: [...]}}})A recipe is a deterministic plan stored at ~/.webharness/recipes/<name>.json:
{
"name": "embed_and_log",
"description": "Embed input and log a confirmation",
"triggers": ["embed", "log"],
"steps": [
{ "tool": "canary.ane.embed", "args": {"input": "$intent"}, "resultKey": "embedding" }
]
}When wh_do's intent contains any triggers keyword, the recipe wins over heuristic + LLM. Zero LLM cost, deterministic.
Best practice for agents: when you've successfully compiled a multi-step plan, offer to save it as a recipe via wh_cortex({op: 'recipe.add', ...}). The user gets a future shortcut; you get a faster + free re-run.
| Error code | What it means | Recovery |
|---|---|---|
unknown_app |
No app matches the prefix | Re-run wh_discover; ask the user to launch the app |
ambiguous_tool |
Short-form tool resolved to >1 app | Re-call with app.capability.method form |
tool_not_found |
Bad string format | Use app.cap.method or cap.method |
capability_not_declared |
App lacks this capability in its manifest | Tell user; offer to switch to an app that has it |
app_unreachable |
Port-file present but app crashed/closed | Tell user to restart the app |
consent_denied |
User said no | Stop; don't retry without permission |
timeout |
Probe or call exceeded 5s | Retry once; if it persists, suggest a restart |
unauthorized |
Bearer token rejected | Port-file is stale; user restarts the app |
invalid_response |
Runtime returned malformed JSON | Internal — surface reason to the user |
server_error |
Capability handler threw | Surface body; suggest filing an issue |
| Term | Meaning |
|---|---|
| bundleId | Reverse-DNS app id (e.g. io.webharness.canary). |
| port-file | ~/.webharness/apps/<bundleId>.json — written at launch, deleted at shutdown. Carries port + bearer token + capabilities + PID. |
| tier | safe (no gate) · caution (logged, no prompt) · sensitive (one-time consent) · dangerous (always consent unless pre-approved). |
| capability | A namespaced verb (e.g. vault.read). Declared in webharness.yml. |
| manifest | The declared capabilities + window + MCP config in webharness.yml. |
| recipe | A deterministic plan in ~/.webharness/recipes/<name>.json. |
| job | An async unit of work tracked by wh_cortex. Has a jobId and a status state machine: `pending → consent_pending → completed |
const r = await wh_call({tool: 'canary.vault.read', args: {key: 'openai'}})
if (r.ok) useKey(r.value)
else if (r.error.code === 'consent_denied') stop('user denied')
else if (r.error.code === 'capability_not_declared') stop('app lacks vault.read')
else retry()await wh_cortex({op: 'recipe.add', args: { recipe: {
name: 'note_index',
triggers: ['index note'],
steps: [
{tool: 'canary.ane.embed', args: {input: '$intent'}, resultKey: 'v'},
{tool: 'canary.vector.store', args: {id: '$intent', vector: '$v'}},
],
}}})
// later — zero LLM cost
await wh_do({intent: 'index note: meeting recap'})const r1 = await wh_call({tool: 'canary.network.connect', args: {url}, consent: 'prompt'})
if (r1.status === 'requires_consent') {
notifyUser(`waiting for consent: ${r1.requires.capability}`)
const r2 = await wh_cortex({op: 'job.wait', args: {id: r1.jobId, timeoutMs: 30000}})
if (r2.data.status === 'completed') return r2.data.result
if (r2.data.status === 'failed') return notify(r2.data.error)
if (r2.data.status === 'consent_pending') return notify('user hasn\'t answered yet')
}const d = await wh_discover({scope: 'apps'})
const withVault = d.apps.filter(a => a.capabilities.includes('vault.read'))
if (withVault.length === 0) return notify('no apps with vault.read')
const targetId = await askUser(withVault.map(a => a.bundleId))
await wh_call({tool: `${targetId}.vault.read`, args: {key: 'openai'}})const plan = await wh_do({intent: 'export DB as CSV', dry: true})
// review plan.plan, plan.confidence
const confirmed = await askUser(plan.plan)
if (confirmed) await wh_call({steps: plan.plan, sync: true})- Each app is single-port; concurrent
wh_calls to the same app are serialized by the runtime's request loop. wh_callis not automatically retried ontimeout— your agent decides.- Recipe execution is atomic per step but not transactional across steps. If step 2 fails, step 1's writes are not rolled back. Design recipes idempotent where possible.
wh_cortex job.waitpolls every 200 ms; default timeout 30 s, capped byargs.timeoutMs.
- Per-tool JSON schemas + envelope reference:
packages/mcp/docs/TOOL-SCHEMAS.md - Troubleshooting (port-file, tokens, app_unreachable):
packages/mcp/docs/TROUBLESHOOTING.md - Client setup (Claude Code, Cursor, Zed, etc.):
packages/mcp/docs/MCP-USAGE.md - System architecture:
docs/ARCHITECTURE.md - Capability ground-truth surface:
docs/CAPABILITIES-surface.md - PRD for the MCPHarness itself:
docs/prds/PRD-webharness-mcp.md