@hover-dev/mcp 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/mcp.ts
4
+ import { basename } from "path";
4
5
  import { chromium } from "playwright-core";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import {
@@ -16,7 +17,8 @@ import {
16
17
  extractPageObjects,
17
18
  buildOptimizeBrief,
18
19
  saveOptimizedCandidate,
19
- lintWiki
20
+ lintWiki,
21
+ appendWikiLog
20
22
  } from "@hover-dev/core/engine";
21
23
 
22
24
  // src/mcp/controller.ts
@@ -555,6 +557,17 @@ function createHoverMcpServer(c) {
555
557
  messages: [{ role: "user", content: { type: "text", text: lintPrompt() } }]
556
558
  })
557
559
  );
560
+ server2.registerPrompt(
561
+ "ask",
562
+ {
563
+ title: "Hover \u2014 ask the test wiki",
564
+ description: "Answer a question about this app from its .hover/ wiki (business map + remembered rules + specs + run log), with citations. Read-only; can file a confirmed new rule back.",
565
+ argsSchema: { question: z.string().describe('The question to answer, e.g. "what happens when a guest tries to check out?"') }
566
+ },
567
+ ({ question }) => ({
568
+ messages: [{ role: "user", content: { type: "text", text: askPrompt(question) } }]
569
+ })
570
+ );
558
571
  server2.registerPrompt(
559
572
  "heal",
560
573
  {
@@ -568,6 +581,17 @@ function createHoverMcpServer(c) {
568
581
  );
569
582
  return server2;
570
583
  }
584
+ function askPrompt(question) {
585
+ return `Answer a question about this app using its **test wiki** (\`.hover/\`) as the source of truth. The wiki = the business map (\`.hover/hover-map.md\`), the remembered business rules (\`.hover/memory/\` \u2014 via \`recall_business_knowledge\` / \`recall_fact\`), the crystallized specs under \`__vibe_tests__/\`, and the run history (\`.hover/log.md\`).
586
+
587
+ Question: ${question}
588
+
589
+ 1. **Gather (read-only \u2014 don't drive the browser).** Call \`recall_business_knowledge\` for the rule index and pull specifics with \`recall_fact\`. Read \`.hover/hover-map.md\` for the business lines, coverage, and relationships. Skim the relevant \`*.spec.ts\` and \`.hover/log.md\` with your own file tools.
590
+ 2. **Answer directly, with CITATIONS.** Ground every claim in what you read \u2014 name the rule, the business line, or the spec file it came from. If the wiki genuinely doesn't cover it, say so plainly instead of guessing (and suggest running \`/mcp__hover__test_app\` to cover that area).
591
+ 3. **Optionally file it back.** If answering established a durable business RULE the wiki was missing and the user confirms it, persist it with \`record_fact\` (RULES only \u2014 never secrets / credentials / PII) so the next run doesn't re-derive it.
592
+
593
+ Stay on what the wiki + code actually say; don't invent behavior.`;
594
+ }
571
595
  function lintPrompt() {
572
596
  return `Lint this app's **test wiki** (\`.hover/\`) using the Hover MCP tools, then fix what you find. The wiki = the business map (\`.hover/hover-map.md\`), the business-rule memory (\`.hover/memory/*.md\`), and the crystallized specs it points at.
573
597
 
@@ -603,15 +627,21 @@ Drive the browser ONLY through these tools \u2014 they actuate via grounded sele
603
627
  (role+name \u2192 testId \u2192 text), so every spec you save replays EXACTLY what you did
604
628
  (record==replay). Never write spec files yourself; only \`crystallize_spec\` does.
605
629
 
606
- Tools: \`recall_business_knowledge\` \xB7 \`browser_navigate\` \xB7 \`browser_snapshot\` (ARIA
607
- tree \u2014 read before acting) \xB7 \`click_control\` / \`fill_control\` / \`select_control\` /
608
- \`check_control\` (grounded target from the snapshot) \xB7 \`assert_visible\` \xB7
609
- \`record_fact\` \xB7 \`crystallize_spec(name, description?)\`. API layer:
610
- \`capture_requests\` \xB7 \`replay_request\` \xB7 \`crystallize_api_spec\`. Suite:
611
- \`detect_shared_flows\` \xB7 \`extract_page_objects\` \xB7 \`replay_spec\`.
630
+ Tools: \`recall_business_knowledge\` / \`recall_fact\` \xB7 \`browser_navigate\` \xB7
631
+ \`browser_snapshot\` (ARIA tree \u2014 read before acting) \xB7 \`click_control\` /
632
+ \`fill_control\` / \`select_control\` / \`check_control\` (grounded target from the
633
+ snapshot) \xB7 \`assert_visible\` \xB7 \`record_fact\` \xB7 \`crystallize_spec(name, description?)\`.
634
+ API layer: \`capture_requests\` \xB7 \`replay_request\` \xB7 \`crystallize_api_spec\`. Suite:
635
+ \`detect_shared_flows\` \xB7 \`extract_page_objects\` \xB7 \`replay_spec\` \xB7 \`lint_map\`.
612
636
 
613
637
  Target: the app at HOVER_TARGET (set in the server's env). Scope: ${target}.
614
638
 
639
+ ## First: are you bootstrapping or extending? (load only what this run needs)
640
+ Check whether \`.hover/hover-map.md\` already exists.
641
+ - **It exists \u2192 you're EXTENDING.** Read it + call \`recall_business_knowledge\`, then go straight to Phase 2 and cover the uncovered \`[ ]\` lines. Skip the Phase-1 code-mapping \u2014 the map already IS the plan. Only re-map if the user says the app changed.
642
+ - **It's absent \u2192 you're BOOTSTRAPPING.** Do the full Phase 1 below to build the map first.
643
+ This keeps a returning run cheap: you don't re-derive a map you already have.
644
+
615
645
  ## Ground rules (they protect record==replay AND the user's real app)
616
646
  - **Grounded targets only.** Pass role+name EXACTLY as they appear in the LATEST \`browser_snapshot\`. If a locate fails, re-snapshot and read the real target \u2014 never guess, invent, or reuse a stale name.
617
647
  - **It's the user's REAL app.** Avoid irreversible / destructive actions \u2014 real payments, deleting data you didn't create, sending real emails or SMS \u2014 unless the user confirms this is a safe test environment. When unsure, ASK first.
@@ -621,7 +651,7 @@ Target: the app at HOVER_TARGET (set in the server's env). Scope: ${target}.
621
651
  Work in PHASES \u2014 this is what lets it scale from a tiny app to a large one.
622
652
 
623
653
  ## Phase 1 \u2014 Map the business lines (read the CODE, don't click around)
624
- - FIRST call \`recall_business_knowledge\` \u2014 rules earlier runs learned (and read \`.hover/hover-map.md\` if it exists, the running map; CONTINUE it, don't start over). Treat both as ground truth; don't re-ask what they already answer.
654
+ - FIRST call \`recall_business_knowledge\` \u2014 rules earlier runs learned (and read \`.hover/hover-map.md\` if it exists, the running map; CONTINUE it, don't start over). Treat both as ground truth; don't re-ask what they already answer. For an app with many remembered rules this returns an INDEX (one line per rule); when a rule is relevant to what you're about to test, pull its full text with \`recall_fact("<name>")\`.
625
655
  - Use YOUR OWN file tools (read / grep / glob) to find the app's ROUTES + navigation: the router config, route/page files, the nav components. Enumerate the user-facing BUSINESS LINES (a coherent task a user performs), each with its entry route, grouped by area. Reading code is cheaper + more complete than clicking around, and finds areas behind auth / nav you'd otherwise miss.
626
656
  - Write/update \`.hover/hover-map.md\` as a checklist (4-space indent = a code block):
627
657
 
@@ -629,7 +659,11 @@ Work in PHASES \u2014 this is what lets it scale from a tiny app to a large one.
629
659
  ## Auth
630
660
  - [ ] Log in \u2014 /login
631
661
  - [x] Checkout \u2014 /checkout \u2014 checkout.spec.ts
662
+ ## Relationships
663
+ - Checkout depends-on Log in
664
+ - Cart shares-state Checkout
632
665
 
666
+ - Optionally add a \`## Relationships\` block recording inter-line edges you notice \u2014 \`<line> depends-on <line>\`, \`<line> shares-state <line>\`, or \`<line> navigates-to <line>\` (names must match lines above). These become graph edges in the cockpit's Business Map; they also tell a later run what a flow depends on. Record only real edges; skip the block if none stand out.
633
667
  - Don't test yet. For a large app, this map IS the plan.
634
668
 
635
669
  ## Phase 2 \u2014 Pick the scope
@@ -646,6 +680,7 @@ As you drive each flow, Hover passively captures the app's xhr/fetch traffic. Af
646
680
 
647
681
  ## Phase 4 \u2014 Update coverage
648
682
  - Mark each covered line \`[x]\` in \`.hover/hover-map.md\` with its spec filename. Report covered vs still-open. A LARGE app doesn't have to finish in one go \u2014 covering a batch + updating the map is a complete, resumable unit; re-invoke to continue the uncovered lines.
683
+ - Then call \`lint_map\` to catch wiki drift you may have introduced (a covered line whose spec now fails, a stale spec reference, a spec no line maps) and fix or report it. \`/mcp__hover__lint\` runs the deeper check any time.
649
684
 
650
685
  ## Phase 5 \u2014 Lift shared flows into Page Objects (ASK first)
651
686
  Once specs are crystallized, call \`detect_shared_flows\`. If it reports a NON-login flow repeated across specs (login is already handled by the auth setup), tell the user which specs share it and ASK whether to lift it into a shared Page Object (so a UI change to that flow is a one-place fix). On yes \u2192 \`extract_page_objects\` (generates \`pages/*\` + \`fixtures.ts\` and folds the specs to \`await xPage.x()\`). If nothing is shared, skip silently \u2014 most small suites have nothing to lift; don't force it.
@@ -684,10 +719,12 @@ var controller = new HoverMcpController({
684
719
  getPage,
685
720
  crystallize: async (name, description, steps, redactions) => {
686
721
  const res = await writeSpec({ devRoot: DEV_ROOT, name, description, steps, redactions, startUrl: TARGET, overwrite: true });
722
+ await appendWikiLog(DEV_ROOT, "crystallize", `${basename(res.path)} \u2014 ${name}`);
687
723
  return { path: res.path };
688
724
  },
689
725
  crystallizeApi: async (name, description, checks) => {
690
726
  const res = await writeApiSpec({ devRoot: DEV_ROOT, name, description, checks, startUrl: TARGET, overwrite: true });
727
+ await appendWikiLog(DEV_ROOT, "api", `${basename(res.path)} \u2014 ${name}`);
691
728
  return { path: res.path };
692
729
  },
693
730
  recordFact: (title, rule, type) => writeFact(DEV_ROOT, { name: title, description: title, type, body: rule }),
@@ -701,7 +738,13 @@ var controller = new HoverMcpController({
701
738
  return sc ? { steps: sc.steps, startUrl: TARGET } : null;
702
739
  },
703
740
  detectSharedFlows: () => detectExtractableFlows(DEV_ROOT),
704
- extractPageObjects: () => extractPageObjects(DEV_ROOT),
741
+ extractPageObjects: async () => {
742
+ const res = await extractPageObjects(DEV_ROOT);
743
+ if (res.pages.length) {
744
+ await appendWikiLog(DEV_ROOT, "extract", `${res.pages.length} page object(s), folded ${res.folded.length} spec(s)`);
745
+ }
746
+ return res;
747
+ },
705
748
  optimizeBrief: async (slug) => {
706
749
  try {
707
750
  const { prompt } = await buildOptimizeBrief(DEV_ROOT, slug);
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp.ts","../src/mcp/controller.ts","../src/mcp/server.ts"],"sourcesContent":["import { chromium, type Browser, type Page } from 'playwright-core';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n launchDebugChrome,\n writeSpec,\n writeApiSpec,\n writeFact,\n recallMemory,\n readFact,\n formatFact,\n readSidecar,\n detectExtractableFlows,\n extractPageObjects,\n buildOptimizeBrief,\n saveOptimizedCandidate,\n lintWiki,\n type SkillStep,\n type ApiCheck,\n type Redaction,\n} from '@hover-dev/core/engine';\nimport { HoverMcpController } from './mcp/controller.js';\nimport { createHoverMcpServer } from './mcp/server.js';\n\n/*\n * `hover-mcp` — the MCP-first surface. Add it to your OWN agent (Claude Code,\n * Cursor, …); the agent drives the app through Hover's grounded tools and calls\n * crystallize_spec to save a plain Playwright spec. Hover spawns no agent here —\n * the calling agent IS the intelligence; Hover guarantees record==replay at the\n * output. Config via env: HOVER_TARGET, HOVER_CDP_PORT, HOVER_PROJECT_ROOT.\n *\n * NOTE: stdio is the MCP transport — never write to stdout from this process.\n */\n\nconst TARGET = process.env.HOVER_TARGET || 'http://localhost:5173';\nconst PORT = Number(process.env.HOVER_CDP_PORT || 9222);\nconst DEV_ROOT = process.env.HOVER_PROJECT_ROOT || process.cwd();\nconst CDP_URL = `http://localhost:${PORT}`;\n\nconst originOf = (u: string): string | null => {\n try {\n return new URL(u).origin;\n } catch {\n return null;\n }\n};\n\nlet browser: Browser | null = null;\n\n/** Launch/connect the debug Chrome lazily and return the page on the app. */\nasync function getPage(): Promise<Page> {\n if (!browser || !browser.isConnected()) {\n await launchDebugChrome({ port: PORT, url: TARGET });\n browser = await chromium.connectOverCDP(CDP_URL, { timeout: 8000 });\n }\n const pages = browser.contexts().flatMap((ctx) => ctx.pages());\n const want = originOf(TARGET);\n const match = want ? pages.find((p) => originOf(p.url()) === want) : undefined;\n if (match) return match;\n if (pages.length) return pages[pages.length - 1];\n const ctx = browser.contexts()[0] ?? (await browser.newContext());\n return ctx.newPage();\n}\n\nconst controller = new HoverMcpController({\n getPage,\n crystallize: async (name: string, description: string | undefined, steps: SkillStep[], redactions: Redaction[]) => {\n const res = await writeSpec({ devRoot: DEV_ROOT, name, description, steps, redactions, startUrl: TARGET, overwrite: true });\n return { path: res.path };\n },\n crystallizeApi: async (name: string, description: string | undefined, checks: ApiCheck[]) => {\n const res = await writeApiSpec({ devRoot: DEV_ROOT, name, description, checks, startUrl: TARGET, overwrite: true });\n return { path: res.path };\n },\n recordFact: (title, rule, type) =>\n writeFact(DEV_ROOT, { name: title, description: title, type, body: rule }),\n recall: () => recallMemory(DEV_ROOT),\n recallFact: async (name: string) => {\n const fact = await readFact(DEV_ROOT, name);\n return fact ? formatFact(fact) : null;\n },\n readSpecSteps: async (slug: string) => {\n const sc = await readSidecar(DEV_ROOT, slug);\n return sc ? { steps: sc.steps, startUrl: TARGET } : null;\n },\n detectSharedFlows: () => detectExtractableFlows(DEV_ROOT),\n extractPageObjects: () => extractPageObjects(DEV_ROOT),\n optimizeBrief: async (slug: string) => {\n try {\n const { prompt } = await buildOptimizeBrief(DEV_ROOT, slug);\n return { prompt };\n } catch (e) {\n return { error: e instanceof Error ? e.message.split('\\n')[0] : String(e) };\n }\n },\n saveOptimized: (slug: string, code: string) => saveOptimizedCandidate(DEV_ROOT, slug, code),\n lintWiki: () => lintWiki(DEV_ROOT),\n});\n\nconst server = createHoverMcpServer(controller);\nawait server.connect(new StdioServerTransport());\n","import { request as pwRequest, type Page } from 'playwright-core';\nimport {\n groundedLocate,\n replayOnPage,\n type GroundedTarget,\n type SkillStep,\n type ApiCheck,\n type ReplayStep,\n type Redaction,\n type SharedFlow,\n type ExtractResult,\n type LintResult,\n} from '@hover-dev/core/engine';\n\n/*\n * The hover-mcp engine, decoupled from the MCP wire layer so it's testable with\n * a mock Page. The user's OWN agent (Claude Code / Cursor) calls these via MCP:\n * it reads the page (snapshot), actuates with GROUNDED targets (role+name →\n * testId → text), and Hover buffers each successful actuation as a SkillStep.\n * `crystallize` turns the buffer into a plain Playwright spec — record==replay,\n * because the buffered selectors ARE the ones that drove the page.\n *\n * API layer: while the agent drives the UI, Hover passively buffers the app's\n * xhr/fetch traffic off the SAME CDP connection (no MITM proxy). The agent reads\n * it with `capture_requests`, verifies a contract/authz check with\n * `replay_request`, and crystallizes the worthwhile ones into a\n * `*.api-test.spec.ts`. record == replay holds for the API layer too.\n */\n\nexport type FactType = 'business-rule' | 'expected-behavior' | 'validation' | 'access-policy';\n\n/** A passively-observed xhr/fetch call (metadata + a light body shape). */\nexport interface CapturedRequest {\n method: string;\n url: string;\n status: number;\n contentType?: string;\n /** Request post data, truncated. */\n requestBody?: string;\n /** Top-level keys of a JSON response (a light shape hint). */\n responseKeys?: string[];\n}\n\nexport interface McpDeps {\n /** Resolve the live page on the app under test (launch/connect lazily). */\n getPage: () => Promise<Page>;\n /** Write the buffered steps to a UI spec; returns the written path. `redactions`\n * parameterize captured credentials into `process.env.<envVar>` refs (and let\n * auth-fixture detect the login prefix). */\n crystallize: (\n name: string,\n description: string | undefined,\n steps: SkillStep[],\n redactions: Redaction[],\n ) => Promise<{ path: string }>;\n /** Write selected API checks to a `*.api-test.spec.ts`; returns the path. */\n crystallizeApi: (name: string, description: string | undefined, checks: ApiCheck[]) => Promise<{ path: string }>;\n /** Persist a learned business rule to .hover/memory/ (rules only — no secrets). */\n recordFact?: (title: string, rule: string, type: FactType) => Promise<{ path: string } | { error: string }>;\n /** Recall known business knowledge from .hover/memory/ ('' if none). Progressive:\n * full bodies when the set is small, the index alone when it's large. */\n recall?: () => Promise<string>;\n /** Read ONE remembered rule's full text by name/slug (behind recall_fact), or\n * null if nothing matches — the on-demand tier of progressive recall. */\n recallFact?: (name: string) => Promise<string | null>;\n /** Read a saved spec's recorded grounded steps (its `.hover/sidecars/<slug>.json`)\n * so self-heal can replay them against the live app. */\n readSpecSteps?: (slug: string) => Promise<{ steps: SkillStep[]; startUrl?: string } | null>;\n /** Detect NON-login flows shared across saved specs (for the extract offer). */\n detectSharedFlows?: () => Promise<SharedFlow[]>;\n /** Lift shared flows into Page Objects + fold the specs that use them. */\n extractPageObjects?: () => Promise<ExtractResult>;\n /** Build the optimize (F7) brief for a spec — the improvement rules + the spec\n * + its observed session + reusable Page Objects — for the user's OWN agent to\n * work from. `{ error }` when the spec doesn't exist. No model runs. */\n optimizeBrief?: (slug: string) => Promise<{ prompt: string } | { error: string }>;\n /** File an agent-improved spec as a REVIEW candidate: validate it against the\n * deterministic guardrails, soft-batch, and write `.hover/cache/optimized/\n * <slug>.spec.ts.draft` (never the original). Throws if it fails validation. */\n saveOptimized?: (slug: string, code: string) => Promise<{ candidatePath: string }>;\n /** Deterministic health check over `.hover/`: map vs spec files vs run ledger. */\n lintWiki?: () => Promise<LintResult>;\n}\n\nfunction describe(g: GroundedTarget): string {\n if (g.role && g.name) return `${g.role} \"${g.name}\"`;\n if (g.testId) return `testId \"${g.testId}\"`;\n if (g.text) return `text \"${g.text}\"`;\n return '(no target)';\n}\n\nconst MAX_CAPTURED = 200; // ring-buffer cap so a long run can't grow unbounded\n\nexport class HoverMcpController {\n /** The grounded-action buffer — sliced by `crystallize`. */\n readonly steps: SkillStep[] = [];\n /** Credentials typed into password fields — parameterized to process.env in\n * the crystallized spec (never written literally) + used to detect the login\n * prefix for auth-as-fixture. */\n private readonly redactions: Redaction[] = [];\n /** Passively-observed xhr/fetch traffic — read by `capture_requests`. */\n private readonly captured: CapturedRequest[] = [];\n /** Pages we've already attached a network listener to (avoid duplicates). */\n private readonly listening = new WeakSet<Page>();\n\n constructor(private readonly deps: McpDeps) {}\n\n private push(tool: string, input: unknown): void {\n this.steps.push({ kind: 'step', tool, input });\n }\n\n /** getPage + ensure the passive network listener is attached to it. */\n private async livePage(): Promise<Page> {\n const page = await this.deps.getPage();\n if (!this.listening.has(page)) {\n this.listening.add(page);\n page.on('response', (response) => {\n void this.onResponse(response).catch(() => {});\n });\n }\n return page;\n }\n\n private async onResponse(response: import('playwright-core').Response): Promise<void> {\n const req = response.request();\n const rt = req.resourceType();\n if (rt !== 'xhr' && rt !== 'fetch') return; // API calls only — skip docs/assets\n const contentType = (response.headers()['content-type'] || '').split(';')[0] || undefined;\n let responseKeys: string[] | undefined;\n if (contentType === 'application/json') {\n try {\n const body = await response.json();\n if (body && typeof body === 'object' && !Array.isArray(body)) {\n responseKeys = Object.keys(body as Record<string, unknown>).slice(0, 24);\n }\n } catch {\n /* body unavailable / not JSON — metadata only */\n }\n }\n const post = req.postData();\n this.captured.push({\n method: req.method(),\n url: req.url(),\n status: response.status(),\n contentType,\n requestBody: post ? post.slice(0, 2000) : undefined,\n responseKeys,\n });\n if (this.captured.length > MAX_CAPTURED) this.captured.splice(0, this.captured.length - MAX_CAPTURED);\n }\n\n async navigate(url: string): Promise<string> {\n const page = await this.livePage();\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });\n this.push('browser_navigate', { url });\n return `navigated to ${url}`;\n }\n\n /** ARIA snapshot of the page — the agent reads role+name from here. */\n async snapshot(): Promise<string> {\n const page = await this.livePage();\n return await page.locator('body').ariaSnapshot();\n }\n\n private async resolve(g: GroundedTarget) {\n const page = await this.livePage();\n const loc = groundedLocate(page, g);\n if (!loc) throw new Error(`pass role+name (preferred), or testId, or text — taken from the snapshot`);\n return loc;\n }\n\n async click(g: GroundedTarget): Promise<string> {\n const loc = await this.resolve(g);\n await loc.click({ timeout: 8000 });\n this.push('click_control', g);\n return `✓ clicked ${describe(g)}`;\n }\n\n async fill(g: GroundedTarget, value: string): Promise<string> {\n const loc = await this.resolve(g);\n await loc.fill(value, { timeout: 8000 });\n this.push('fill_control', { ...g, value });\n // A value typed into a password field is a secret: parameterize it to\n // process.env so it never lands literally in the spec/sidecar, and so\n // auth-as-fixture can detect this fill as part of the login prefix.\n if (value) {\n let isPassword = false;\n try {\n isPassword = (await loc.getAttribute('type')) === 'password';\n } catch {\n /* locator has no getAttribute (test mock) or field gone — treat as non-secret */\n }\n if (isPassword && !this.redactions.some((r) => r.value === value)) {\n this.redactions.push({ value, envVar: 'HOVER_PASSWORD' });\n }\n }\n return `✓ filled ${describe(g)}`;\n }\n\n async select(g: GroundedTarget, value: string): Promise<string> {\n const loc = await this.resolve(g);\n await loc.selectOption(value, { timeout: 8000 });\n this.push('select_control', { ...g, value });\n return `✓ selected ${value} in ${describe(g)}`;\n }\n\n async check(g: GroundedTarget, checked: boolean): Promise<string> {\n const loc = await this.resolve(g);\n if (checked) await loc.check({ timeout: 8000 });\n else await loc.uncheck({ timeout: 8000 });\n this.push('check_control', { ...g, checked });\n return `✓ ${checked ? 'checked' : 'unchecked'} ${describe(g)}`;\n }\n\n async assertVisible(g: GroundedTarget): Promise<string> {\n const loc = await this.resolve(g);\n const visible = await loc.first().isVisible();\n if (!visible) throw new Error(`${describe(g)} is not visible`);\n this.push('assert_visible', { ...g, matcher: 'visible' });\n return `✓ ${describe(g)} is visible`;\n }\n\n /** Return the passively-observed xhr/fetch traffic (optionally filtered) as\n * JSON for the agent to reason over when deciding API checks. */\n captureRequests(filter?: { urlContains?: string; method?: string }): string {\n let rows = this.captured;\n if (filter?.urlContains) rows = rows.filter((r) => r.url.includes(filter.urlContains!));\n if (filter?.method) rows = rows.filter((r) => r.method.toUpperCase() === filter.method!.toUpperCase());\n if (rows.length === 0) {\n return 'No xhr/fetch traffic captured yet. Drive the app (navigate / click) so its API calls fire, then call this again.';\n }\n return JSON.stringify(rows.slice(-60), null, 2);\n }\n\n /** Send a (possibly mutated) request and return the response summary — the\n * API analogue of replaying a grounded step, so an authz/contract check is\n * VERIFIED against the live app before it's crystallized (no confabulation).\n * `authenticated` (default true) replays with the browser session's cookies;\n * false uses a fresh context (no session) for \"requires auth\" checks. */\n async replayRequest(opts: {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: unknown;\n authenticated?: boolean;\n }): Promise<string> {\n const page = await this.livePage();\n const authed = opts.authenticated !== false;\n const ctx = authed ? page.context().request : await pwRequest.newContext();\n try {\n const m = opts.method.toUpperCase();\n const reqOpts: { headers?: Record<string, string>; data?: unknown } = {};\n if (opts.headers) reqOpts.headers = opts.headers;\n if (opts.body !== undefined && m !== 'GET' && m !== 'HEAD') reqOpts.data = opts.body;\n const res = await ctx.fetch(opts.url, { method: m, ...reqOpts });\n const contentType = (res.headers()['content-type'] || '').split(';')[0] || '';\n let preview = '';\n try {\n preview = contentType === 'application/json' ? JSON.stringify(await res.json()).slice(0, 800) : (await res.text()).slice(0, 400);\n } catch {\n /* no body */\n }\n return JSON.stringify({ status: res.status(), ok: res.ok(), contentType, body: preview, authenticated: authed }, null, 2);\n } finally {\n if (!authed) await ctx.dispose();\n }\n }\n\n /** Recall what earlier runs learned about this app's business rules. Progressive:\n * a large memory comes back as an INDEX; use recallFact to pull one rule's body. */\n async recall(): Promise<string> {\n if (!this.deps.recall) return 'No business memory available.';\n const known = await this.deps.recall();\n return known || 'No business knowledge recorded yet for this app.';\n }\n\n /** Read one remembered rule's full text by name (the on-demand tier — used when\n * recall returned only the index and the agent needs a specific rule's body). */\n async recallFact(name: string): Promise<string> {\n if (!this.deps.recallFact) return 'Rule lookup unavailable in this server.';\n const body = await this.deps.recallFact(name);\n return body ?? `No remembered rule matches \"${name}\". Call recall_business_knowledge to see the index of known rules.`;\n }\n\n /** Persist a confirmed business RULE so future runs don't re-ask it. Rules\n * only — never secrets / credentials / PII. */\n async recordFact(title: string, rule: string, type: FactType = 'business-rule'): Promise<string> {\n if (!this.deps.recordFact) return 'Memory channel unavailable; continuing.';\n const res = await this.deps.recordFact(title, rule, type);\n return 'error' in res ? `✗ could not save fact: ${res.error}` : `✓ remembered: ${title}`;\n }\n\n /** Crystallize the buffered UI flow → a plain Playwright spec, then clear the\n * buffer for the next flow. */\n async crystallize(name: string, description?: string): Promise<string> {\n if (this.steps.length === 0) return 'Nothing to crystallize yet — actuate some controls first.';\n const flow = [...this.steps];\n const { path } = await this.deps.crystallize(name, description, flow, [...this.redactions]);\n this.steps.length = 0;\n return `✓ wrote ${path} (${flow.length} step${flow.length === 1 ? '' : 's'})`;\n }\n\n /** Crystallize agent-selected API checks → a `*.api-test.spec.ts`. The agent\n * decides WHICH calls are worth locking (a real contract / authz boundary),\n * not every captured request. */\n async crystallizeApiSpec(name: string, description: string | undefined, checks: ApiCheck[]): Promise<string> {\n if (!checks?.length) return 'No checks provided — pass the API checks you verified worth locking.';\n const { path } = await this.deps.crystallizeApi(name, description, checks);\n return `✓ wrote ${path} (${checks.length} check${checks.length === 1 ? '' : 's'})`;\n }\n\n /** Self-heal detection: replay a saved spec's RECORDED grounded steps against\n * the live app and report the first step that no longer locates — the drift\n * point the agent re-grounds. No `playwright test`, no install; the same\n * grounded replay as creation-verification, seeded from the spec's sidecar. */\n async replaySpec(slug: string): Promise<string> {\n if (!this.deps.readSpecSteps) return 'Spec replay unavailable in this server.';\n const sc = await this.deps.readSpecSteps(slug);\n if (!sc) {\n return `No sidecar for \"${slug}\" — only Hover-crystallized specs can be replayed (looked for .hover/sidecars/${slug}.json).`;\n }\n const page = await this.livePage();\n const devUrl = sc.startUrl ?? page.url();\n const res = await replayOnPage(page, devUrl, sc.steps as ReplayStep[]);\n if (res.ok) {\n return `✓ \"${slug}\" still replays clean — ${res.ran}/${res.total} grounded steps located. No drift to heal.`;\n }\n const f = res.failures[0];\n return JSON.stringify(\n {\n spec: slug,\n drifted: true,\n ranBeforeBreak: res.ran,\n total: res.total,\n brokeAtStep: f.index,\n tool: f.tool,\n lookingFor: f.target,\n error: f.error,\n next: 'Re-snapshot at this point, re-locate the control by the intent in \"lookingFor\" (its label/role may have changed), re-drive from here, then crystallize_spec with the SAME name to overwrite the healed spec.',\n },\n null,\n 2,\n );\n }\n\n /** LLM-Wiki P1 Lint: run the deterministic `.hover/` health check (map vs\n * specs vs runs) and return it as a readable report the agent acts on (heal a\n * regressed line, map an orphan spec, drop a dead ref). The LLM-judged checks\n * (contradictory rules, code routes missing from the map) are driven on top of\n * this by the /mcp__hover__lint prompt. */\n async lintWiki(): Promise<string> {\n if (!this.deps.lintWiki) return 'Wiki lint is unavailable in this server.';\n const res = await this.deps.lintWiki();\n if (!res.hasMap) {\n return 'No .hover/hover-map.md yet — nothing to lint. Run /mcp__hover__test_app first to map the app and crystallize specs.';\n }\n const { areas, lines, covered, specs } = res.summary;\n const head = `Wiki lint — ${covered}/${lines} lines covered across ${areas} area${areas === 1 ? '' : 's'}, ${specs} spec file${specs === 1 ? '' : 's'}.`;\n if (res.findings.length === 0) {\n return `${head}\\n✓ No drift: every mapped spec exists, no covered line is failing, no spec is unmapped.`;\n }\n const icon = { error: '✗', warn: '⚠', info: '·' } as const;\n const body = res.findings\n .map((f) => `${icon[f.severity]} [${f.kind}] ${f.message}${f.fix ? `\\n → ${f.fix}` : ''}`)\n .join('\\n');\n return `${head}\\n${res.findings.length} finding${res.findings.length === 1 ? '' : 's'}:\\n${body}`;\n }\n\n /** Report NON-login flows repeated across the saved specs — so the agent can\n * ASK the user whether to lift them into a shared Page Object. Read-only. */\n async detectSharedFlows(): Promise<string> {\n if (!this.deps.detectSharedFlows) return 'Shared-flow detection unavailable in this server.';\n const flows = await this.deps.detectSharedFlows();\n if (!flows.length) {\n return 'No extractable shared flows — no set of specs shares a non-login entry flow (login is already handled by the auth setup). Nothing to lift.';\n }\n return JSON.stringify(\n flows.map((f) => ({ sharedBy: f.specs, steps: f.prose })),\n null,\n 2,\n );\n }\n\n /** Optimize (F7), MCP-first: return the improvement brief for the user's own\n * agent to work from (the agent IS the model — Hover picks none). Never\n * throws; a missing spec / unavailable channel comes back as a plain message\n * the agent reads (this feeds a PROMPT, which isn't wrapped in the ✗ guard). */\n async optimizeBrief(slug: string): Promise<string> {\n if (!this.deps.optimizeBrief) return `Optimize is unavailable in this server (heal/optimize need spec sidecars).`;\n try {\n const res = await this.deps.optimizeBrief(slug);\n if ('error' in res) {\n return `Can't optimize \"${slug}\": ${res.error}. Optimize only works on a Hover-crystallized spec — list __vibe_tests__/*.spec.ts and pass one by slug.`;\n }\n return res.prompt;\n } catch (e) {\n return `Can't build the optimize brief for \"${slug}\": ${e instanceof Error ? e.message.split('\\n')[0] : String(e)}`;\n }\n }\n\n /** File an agent-improved spec as a review candidate. Validation failures throw\n * (the tool guard turns them into a ✗ the agent can fix and retry against). */\n async saveOptimized(slug: string, code: string): Promise<string> {\n if (!this.deps.saveOptimized) return 'Optimize is unavailable in this server.';\n const { candidatePath } = await this.deps.saveOptimized(slug, code);\n return `✓ filed optimized candidate at ${candidatePath} (your spec is untouched). Tell the user to diff it against __vibe_tests__/${slug}.spec.ts and, to keep it, replace the spec with the candidate.`;\n }\n\n /** Lift the detected shared flows into `pages/*` + `fixtures.ts` and fold the\n * specs that use them. Call ONLY after the user approves (detectSharedFlows\n * → ask → this). Deterministic; login flows are excluded (auth-fixture owns). */\n async extractPageObjects(): Promise<string> {\n if (!this.deps.extractPageObjects) return 'Page Object extraction unavailable in this server.';\n const res = await this.deps.extractPageObjects();\n if (!res.pages.length) return 'Nothing to extract — no shared non-login flow found.';\n return `✓ lifted ${res.pages.length} Page Object${res.pages.length === 1 ? '' : 's'} (${res.pages\n .map((p) => p.className)\n .join(', ')}) into __vibe_tests__/pages + fixtures.ts; folded ${res.folded.length} spec${\n res.folded.length === 1 ? '' : 's'\n } (${res.folded.join(', ')}) to consume them.`;\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { HoverMcpController } from './controller.js';\n\n/* Wrap the controller as an MCP server. Tool names + the GROUNDED target shape\n * mirror Hover's control MCP so an agent that knows one knows the other. Every\n * handler returns text (✓/✗) and never throws — a failed locate/action becomes\n * a ✗ message the calling agent can react to, not an MCP transport error. */\n\nconst md = (text: string) => ({ content: [{ type: 'text' as const, text }] });\nconst errLine = (e: unknown) => (e instanceof Error ? e.message.split('\\n')[0] : String(e));\n\nconst GROUND = {\n role: z.string().optional().describe(\"ARIA role from the snapshot, e.g. 'button', 'textbox', 'link'. Pair with `name`.\"),\n name: z.string().optional().describe('Accessible name from the snapshot, exactly as shown. Pair with `role`.'),\n testId: z.string().optional().describe('A data-testid, if the element has one and no clean role+name.'),\n text: z.string().optional().describe('Real visible text on the element — last resort.'),\n within: z\n .object({ role: z.string(), name: z.string() })\n .optional()\n .describe('Scope the search to a container first (role+name) when a label repeats across groups.'),\n};\n\n/** One asserted API call for crystallize_api_spec — mirrors core's ApiCheck. */\nconst API_CHECK = z.object({\n title: z.string().describe('Short test name, e.g. \"GET /api/cart returns the cart\".'),\n method: z.string(),\n url: z.string().describe('Full URL or same-origin path (same-origin is relativized in the spec).'),\n requestBody: z.any().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n expectStatus: z.number().optional().describe('Expected status — verified with replay_request.'),\n expectBodyKeys: z.array(z.string()).optional().describe('Top-level response keys to assert present.'),\n note: z.string().optional().describe('Emitted as a leading comment, e.g. \"authz: no session → 401\".'),\n});\n\nexport function createHoverMcpServer(c: HoverMcpController): McpServer {\n const server = new McpServer({ name: 'hover', version: '0.1.0' });\n const guard = (fn: () => Promise<string>) => fn().then(md, (e) => md(`✗ ${errLine(e)}`));\n\n server.registerTool(\n 'browser_navigate',\n { description: 'Open a URL in the app under test (the debug Chrome).', inputSchema: { url: z.string() } },\n ({ url }) => guard(() => c.navigate(url)),\n );\n\n server.registerTool(\n 'browser_snapshot',\n {\n description:\n 'Read the current page as an ARIA snapshot (role + accessible-name tree). Call this BEFORE actuating to get the exact role+name to pass to the *_control tools.',\n inputSchema: {},\n },\n () => guard(() => c.snapshot()),\n );\n\n server.registerTool(\n 'click_control',\n {\n description:\n 'Click a control by a GROUNDED target read off the snapshot (role+name preferred → testId → text). Grounded so the saved spec replays exactly what you did.',\n inputSchema: GROUND,\n },\n (g) => guard(() => c.click(g)),\n );\n\n server.registerTool(\n 'fill_control',\n { description: 'Type a value into a textbox/field by a grounded target.', inputSchema: { ...GROUND, value: z.string() } },\n ({ value, ...g }) => guard(() => c.fill(g, value)),\n );\n\n server.registerTool(\n 'select_control',\n { description: 'Choose an option in a <select> by a grounded target.', inputSchema: { ...GROUND, value: z.string() } },\n ({ value, ...g }) => guard(() => c.select(g, value)),\n );\n\n server.registerTool(\n 'check_control',\n {\n description: 'Check or uncheck a checkbox/radio by a grounded target (handles sr-only / hidden inputs).',\n inputSchema: { ...GROUND, checked: z.boolean().optional().describe('true (default) = check; false = uncheck.') },\n },\n ({ checked, ...g }) => guard(() => c.check(g, checked !== false)),\n );\n\n server.registerTool(\n 'assert_visible',\n {\n description: 'Assert a control/text is visible now — captures an expect(...).toBeVisible() into the saved spec.',\n inputSchema: GROUND,\n },\n (g) => guard(() => c.assertVisible(g)),\n );\n\n server.registerTool(\n 'recall_business_knowledge',\n {\n description:\n 'Recall what earlier Hover runs learned about this app (business rules, expected behaviors, access policies). Call this at the START so you do not re-ask settled questions. Treat what it returns as ground truth. For an app with a lot of remembered rules this returns an INDEX (one line per rule); when a rule is relevant to what you are testing, read its full text with recall_fact.',\n inputSchema: {},\n },\n () => guard(() => c.recall()),\n );\n\n server.registerTool(\n 'recall_fact',\n {\n description:\n \"Read ONE remembered business rule's full text by name (the name shown in the recall_business_knowledge index). Use it when recall returned only the index and you need a specific rule's detail before deciding how to test.\",\n inputSchema: {\n name: z.string().describe('The rule name from the recall index (a slug like \"guests-cannot-checkout\").'),\n },\n },\n ({ name }) => guard(() => c.recallFact(name)),\n );\n\n server.registerTool(\n 'record_fact',\n {\n description:\n \"Remember a durable BUSINESS RULE about this app so neither you nor a future run re-asks it — e.g. an expected behavior, a validation rule, an access policy, or the answer to a 'bug or by-design?' the user just confirmed. State it as a clean self-contained rule. RULES ONLY — never store secrets, passwords, tokens, or personal data.\",\n inputSchema: {\n title: z.string().describe('Short title for the rule (becomes its memory filename + index entry).'),\n rule: z.string().describe('The rule itself, stated clearly and self-contained (no secrets/PII).'),\n type: z\n .enum(['business-rule', 'expected-behavior', 'validation', 'access-policy'])\n .optional()\n .describe('What kind of knowledge this is. Defaults to business-rule.'),\n },\n },\n ({ title, rule, type }) => guard(() => c.recordFact(title, rule, type ?? 'business-rule')),\n );\n\n server.registerTool(\n 'crystallize_spec',\n {\n description:\n 'Save the flow you JUST performed (the grounded click/fill/select/check/assert actions since the last crystallize) as a plain Playwright .spec.ts in __vibe_tests__/. Call it the moment you finish a coherent end-to-end flow. Name it in English, imperative (e.g. \"Log in\").',\n inputSchema: {\n name: z.string().describe('Short imperative flow name in English — becomes the spec filename + test name.'),\n description: z.string().optional().describe('One line on what this flow verifies.'),\n },\n },\n ({ name, description }) => guard(() => c.crystallize(name, description)),\n );\n\n // ── API layer ──────────────────────────────────────────────────────────────\n // While you drive the UI, Hover passively buffers the app's xhr/fetch traffic\n // off the same CDP connection (no MITM). Read it, verify a check, crystallize.\n\n server.registerTool(\n 'capture_requests',\n {\n description:\n \"Return the app's xhr/fetch API calls observed while you drove the UI (method, url, status, content-type, request body, response shape). Call it after exercising a flow to see which endpoints it hit. Optionally filter.\",\n inputSchema: {\n urlContains: z.string().optional().describe('Only calls whose URL contains this substring.'),\n method: z.string().optional().describe('Only calls with this HTTP method.'),\n },\n },\n ({ urlContains, method }) => guard(() => Promise.resolve(c.captureRequests({ urlContains, method }))),\n );\n\n server.registerTool(\n 'replay_request',\n {\n description:\n 'Send a (possibly mutated) request and return the response, to VERIFY an API check before crystallizing it (no confabulated status codes). For an authz check, set authenticated:false (fresh context, no session) and expect 401/403; or drop/alter headers / swap an id for IDOR.',\n inputSchema: {\n method: z.string().describe('HTTP method, e.g. GET / POST.'),\n url: z.string().describe('Full URL or same-origin path.'),\n headers: z.record(z.string(), z.string()).optional().describe('Headers to send (omit/blank the auth header for a \"requires auth\" check).'),\n body: z.any().optional().describe('Request body for POST/PUT/PATCH.'),\n authenticated: z\n .boolean()\n .optional()\n .describe('true (default) = replay with the browser session; false = fresh context with NO session (for \"requires auth\" checks).'),\n },\n },\n ({ method, url, headers, body, authenticated }) =>\n guard(() => c.replayRequest({ method, url, headers, body, authenticated })),\n );\n\n server.registerTool(\n 'crystallize_api_spec',\n {\n description:\n 'Save selected API checks as a plain Playwright `*.api-test.spec.ts` (uses the `request` fixture). Use it ALONGSIDE crystallize_spec when a flow exercised a worthwhile API surface — a real contract, a data mutation, or an authz boundary. Be SELECTIVE: lock checks that matter, not every captured call. Verify each with replay_request first.',\n inputSchema: {\n name: z.string().describe('Short imperative English name — becomes the <name>.api-test.spec.ts filename.'),\n description: z.string().optional().describe('One line on what this API spec verifies.'),\n checks: z.array(API_CHECK).describe('The API checks to lock — each a request + its expected status / shape / authz outcome.'),\n },\n },\n ({ name, description, checks }) => guard(() => c.crystallizeApiSpec(name, description, checks)),\n );\n\n // ── Self-heal ────────────────────────────────────────────────────────────\n server.registerTool(\n 'replay_spec',\n {\n description:\n \"Detect drift in a saved spec: replay its RECORDED grounded steps against the LIVE app and report the first step that no longer locates (its index + what it was looking for). No `playwright test` needed. Use this to find what to heal, then re-ground that step and re-crystallize.\",\n inputSchema: {\n slug: z.string().describe('The spec slug = its filename without .spec.ts (e.g. \"login\" for login.spec.ts).'),\n },\n },\n ({ slug }) => guard(() => c.replaySpec(slug)),\n );\n\n // ── Page Object extraction (detect → ask → extract) ──────────────────────\n server.registerTool(\n 'detect_shared_flows',\n {\n description:\n 'After crystallizing the suite, report NON-login flows repeated across specs (login is already handled by the auth setup). Use it to decide whether to OFFER lifting a shared flow into a Page Object — then ASK the user before extracting.',\n inputSchema: {},\n },\n () => guard(() => c.detectSharedFlows()),\n );\n\n server.registerTool(\n 'extract_page_objects',\n {\n description:\n 'Lift the shared flows into __vibe_tests__/pages/* + fixtures.ts and fold the specs that use them (they call `await xPage.x()` from ./fixtures). Deterministic. Call ONLY after the user approves the offer from detect_shared_flows.',\n inputSchema: {},\n },\n () => guard(() => c.extractPageObjects()),\n );\n\n // ── Wiki lint (LLM-Wiki P1) ──────────────────────────────────────────────\n server.registerTool(\n 'lint_map',\n {\n description:\n \"Health-check the app's test wiki (.hover/): cross-check the business map against the real spec files and the run ledger. Reports deleted specs (a line points at a missing *.spec.ts), regressed coverage (a covered line's spec last ran fail/flaky → heal it), and orphan specs (a spec no line maps). Deterministic; no LLM. Run it to find drift, then fix each finding (heal / re-map / drop the stale ref).\",\n inputSchema: {},\n },\n () => guard(() => c.lintWiki()),\n );\n\n // ── Optimize (F7) ────────────────────────────────────────────────────────\n // The IMPROVEMENT is the agent's (the /mcp__hover__optimize prompt gives it the\n // brief); this tool is Hover's guardrail + write path — it validates the agent's\n // result and files it as a review candidate, never touching the spec.\n server.registerTool(\n 'save_optimized_spec',\n {\n description:\n \"File an improved spec (produced from the /optimize brief) as a REVIEW CANDIDATE. Hover validates it (semantic selectors, no waitForTimeout/XPath, parses), soft-batches trailing assertions, and writes .hover/cache/optimized/<slug>.spec.ts.draft — it does NOT overwrite your spec. On a ✗ (rejected check), fix it and call again. This is the only way to file an optimization; don't write the .draft yourself.\",\n inputSchema: {\n slug: z.string().describe('The spec slug being optimized (its filename without .spec.ts).'),\n code: z.string().describe('The COMPLETE improved .ts file contents.'),\n },\n },\n ({ slug, code }) => guard(() => c.saveOptimized(slug, code)),\n );\n\n // The workflow ships WITH the server as an MCP prompt — Claude Code surfaces\n // it as `/mcp__hover__test_app`, so adding the server brings both the tools\n // AND the command. No project scaffolding needed.\n server.registerPrompt(\n 'test_app',\n {\n title: 'Hover — map & crystallize a test suite',\n description: 'Map this app\\'s business lines and crystallize a Playwright suite (incremental, scales to large apps).',\n argsSchema: { scope: z.string().optional().describe('An area/flow to focus on. Omit to cover the whole app.') },\n },\n ({ scope }) => ({\n messages: [{ role: 'user', content: { type: 'text', text: workflowPrompt(scope) } }],\n }),\n );\n\n // Optimize workflow — surfaced as `/mcp__hover__optimize`. Hover builds the\n // brief (spec + observed session + Page Objects); the agent does the thinking.\n server.registerPrompt(\n 'optimize',\n {\n title: 'Hover — enrich a spec with observed assertions',\n description: 'Improve a crystallized spec: add assertions for what the session observed, de-literalize volatile values, reuse Page Objects. Files a review candidate; never overwrites the spec.',\n argsSchema: { spec: z.string().describe('The spec slug to optimize (e.g. \"checkout\" for checkout.spec.ts).') },\n },\n async ({ spec }) => ({\n messages: [{ role: 'user' as const, content: { type: 'text' as const, text: await c.optimizeBrief(spec) } }],\n }),\n );\n\n // Wiki-lint workflow — surfaced as `/mcp__hover__lint`. The tool does the\n // mechanical checks; the prompt drives the LLM-judged half on top.\n server.registerPrompt(\n 'lint',\n {\n title: 'Hover — lint the test wiki',\n description: \"Health-check .hover/: deterministic drift (dead spec refs, regressed coverage, unmapped specs) plus LLM-judged checks (contradictory rules, code routes missing from the map), then offer fixes.\",\n argsSchema: {},\n },\n () => ({\n messages: [{ role: 'user' as const, content: { type: 'text' as const, text: lintPrompt() } }],\n }),\n );\n\n // Self-heal workflow — surfaced as `/mcp__hover__heal`.\n server.registerPrompt(\n 'heal',\n {\n title: 'Hover — heal a drifted spec',\n description: \"Replay a saved spec against the live app; where the UI drifted, re-ground the broken step and re-crystallize. Pass a spec to heal one, or omit to check all.\",\n argsSchema: { spec: z.string().optional().describe('A spec slug to heal (e.g. \"login\"). Omit to check every spec.') },\n },\n ({ spec }) => ({\n messages: [{ role: 'user', content: { type: 'text', text: healPrompt(spec) } }],\n }),\n );\n\n return server;\n}\n\n/** Wiki-lint workflow body. `lint_map` does the mechanical checks; the agent\n * layers the LLM-judged checks (contradictions, unmapped code routes) and fixes. */\nfunction lintPrompt(): string {\n return `Lint this app's **test wiki** (\\`.hover/\\`) using the Hover MCP tools, then fix what you find. The wiki = the business map (\\`.hover/hover-map.md\\`), the business-rule memory (\\`.hover/memory/*.md\\`), and the crystallized specs it points at.\n\n1. **Deterministic drift** — call \\`lint_map\\`. It cross-checks the map against the real spec files and the run ledger and reports:\n - **deleted-spec** — a line points at a \\`*.spec.ts\\` that's gone. Re-crystallize the flow, or drop the stale reference from the map.\n - **regressed-coverage** — a covered line whose spec last ran fail/flaky. Heal it with \\`/mcp__hover__heal <slug>\\` (or say it's a real app bug to fix).\n - **orphan-spec** — a spec no line maps. Add its business line to the map (\\`[x]\\` with the spec).\n2. **Contradictory rules (LLM-judged)** — read \\`.hover/memory/*.md\\`. If two facts conflict (e.g. one says guests can check out, another says they can't), surface the pair and ASK the user which holds; correct the wrong one with \\`record_fact\\` (or delete it).\n3. **Unmapped code routes (LLM-judged)** — with your own file tools, grep the app's router for user-facing routes. Any real route absent from \\`.hover/hover-map.md\\` is a coverage gap — add it as an uncovered \\`[ ]\\` line under the right area so the map stays honest.\n4. **Report + fix** — summarize the findings, apply the safe fixes (re-map, add gaps, correct rules), and for anything destructive (dropping a spec, deleting a rule) ASK first. Update \\`.hover/hover-map.md\\` as you go.\n\nStay on the app under test. Don't invent business lines that don't exist in the code.`;\n}\n\n/** Self-heal workflow body. The agent uses `replay_spec` to find the drift,\n * re-grounds the broken step by its recorded intent, and re-crystallizes. */\nfunction healPrompt(spec?: string): string {\n const scope = spec?.trim()\n ? `the spec \\`${spec.trim()}\\``\n : 'every spec under `__vibe_tests__/` (list them first, then heal each that drifted)';\n return `Heal ${scope} for this app using the **Hover MCP tools** — repair specs whose UI drifted, without rewriting them by hand.\n\nA spec \"drifted\" when the app changed so a recorded step no longer locates its control (a renamed button, a moved field). Healing = re-grounding ONLY the broken step against the current UI, keeping everything else, so record==replay still holds.\n\nWork ONE spec at a time:\n\n1. **Detect** — \\`replay_spec(\"<slug>\")\\`. It replays the spec's recorded grounded steps against the live app and reports the first step that fails to locate: its index, the tool, and what it was \\`lookingFor\\` (role+name/text). If it replays clean, that spec is fine — move on.\n2. **Re-ground the broken step** — \\`browser_navigate\\` to the spec's route, \\`browser_snapshot\\`, and find the control that NOW serves the intent in \\`lookingFor\\` (e.g. the submit button whose label changed \"Sign in\" → \"Log in\"). Re-drive from the break with the grounded \\`*_control\\` tools. Change ONLY what drifted — don't redesign the flow.\n3. **Re-crystallize** — when the flow runs green again, \\`crystallize_spec\\` with the SAME name as the broken spec to overwrite it with the healed version.\n4. **Report** — say which step drifted, what changed (old target → new target), and that it's re-crystallized. The user reviews the old-vs-new diff in the cockpit before keeping it.\n\nRules: heal by the recorded INTENT (re-locate the same control), never invent a new flow or new assertions. If a step is gone because the FEATURE was removed (not just renamed), don't guess — say so and ASK whether to drop that step or the whole spec. Stay on the app under test.`;\n}\n\n/** The phased, scale-aware workflow, delivered as the prompt body. Mirrors the\n * hover-mcp tool surface; the agent's own file tools do the code-reading. */\nfunction workflowPrompt(scope?: string): string {\n const target = scope?.trim() ? scope.trim() : 'the whole app';\n return `Build (or extend) a Playwright test suite for this web app using the **Hover MCP tools**.\nDrive the browser ONLY through these tools — they actuate via grounded selectors\n(role+name → testId → text), so every spec you save replays EXACTLY what you did\n(record==replay). Never write spec files yourself; only \\`crystallize_spec\\` does.\n\nTools: \\`recall_business_knowledge\\` · \\`browser_navigate\\` · \\`browser_snapshot\\` (ARIA\ntree — read before acting) · \\`click_control\\` / \\`fill_control\\` / \\`select_control\\` /\n\\`check_control\\` (grounded target from the snapshot) · \\`assert_visible\\` ·\n\\`record_fact\\` · \\`crystallize_spec(name, description?)\\`. API layer:\n\\`capture_requests\\` · \\`replay_request\\` · \\`crystallize_api_spec\\`. Suite:\n\\`detect_shared_flows\\` · \\`extract_page_objects\\` · \\`replay_spec\\`.\n\nTarget: the app at HOVER_TARGET (set in the server's env). Scope: ${target}.\n\n## Ground rules (they protect record==replay AND the user's real app)\n- **Grounded targets only.** Pass role+name EXACTLY as they appear in the LATEST \\`browser_snapshot\\`. If a locate fails, re-snapshot and read the real target — never guess, invent, or reuse a stale name.\n- **It's the user's REAL app.** Avoid irreversible / destructive actions — real payments, deleting data you didn't create, sending real emails or SMS — unless the user confirms this is a safe test environment. When unsure, ASK first.\n- **Assert stable outcomes.** Assert on semantic, durable signals (a success message, a heading, a new row's label) — NEVER volatile instance data (timestamps, generated ids, \"today\", a one-off order number), which makes the saved spec flaky on replay.\n- **Log in first.** If the app needs auth, do that before anything else — ask for credentials if you don't have them — then crystallize it as its own \"Log in\" spec and stay logged in for the rest of the run.\n\nWork in PHASES — this is what lets it scale from a tiny app to a large one.\n\n## Phase 1 — Map the business lines (read the CODE, don't click around)\n- FIRST call \\`recall_business_knowledge\\` — rules earlier runs learned (and read \\`.hover/hover-map.md\\` if it exists, the running map; CONTINUE it, don't start over). Treat both as ground truth; don't re-ask what they already answer.\n- Use YOUR OWN file tools (read / grep / glob) to find the app's ROUTES + navigation: the router config, route/page files, the nav components. Enumerate the user-facing BUSINESS LINES (a coherent task a user performs), each with its entry route, grouped by area. Reading code is cheaper + more complete than clicking around, and finds areas behind auth / nav you'd otherwise miss.\n- Write/update \\`.hover/hover-map.md\\` as a checklist (4-space indent = a code block):\n\n # Business map — <app>\n ## Auth\n - [ ] Log in — /login\n - [x] Checkout — /checkout — checkout.spec.ts\n\n- Don't test yet. For a large app, this map IS the plan.\n\n## Phase 2 — Pick the scope\n- If a scope was given, cover that. Otherwise show the uncovered lines and ask which to cover now (offer \"all uncovered\"). For a small app, just cover them all.\n\n## Phase 3 — Cover each chosen line, ONE AT A TIME\nFor each line: \\`browser_navigate\\` to its route → \\`browser_snapshot\\` → EXERCISE the real flow (click / fill / select / check — you are a tester, never just describe the page) → \\`assert_visible\\` on the OUTCOME (a success message, the new row, the next screen) → \\`crystallize_spec(\"<short imperative English name>\")\\`. Crystallize the MOMENT each flow is done, before the next — the buffer is per-flow. The tools share ONE browser, so cover lines SEQUENTIALLY.\n\n## Phase 3.5 — Lock the API layer too (SELECTIVELY)\nAs you drive each flow, Hover passively captures the app's xhr/fetch traffic. After a flow, call \\`capture_requests\\` to see what it hit. For a line that exercised a **worthwhile API surface — a data mutation (POST/PUT/DELETE), a clear contract, or an authz boundary** — also lock an API spec:\n- Decide the checks. Contract: the call returns its status + key fields. Authz: the same call WITHOUT the session must be refused — call \\`replay_request\\` with \\`authenticated:false\\` and confirm it's 401/403 before asserting it.\n- VERIFY each check with \\`replay_request\\` first (so you never assert a confabulated status), then \\`crystallize_api_spec(name, checks)\\` → a \\`*.api-test.spec.ts\\`.\n- Be SELECTIVE — skip pure-display reads, analytics, third-party pings. Most UI flows need 0–1 API specs. A static/read-only flow needs none. This is judgment, not a per-flow quota.\n\n## Phase 4 — Update coverage\n- Mark each covered line \\`[x]\\` in \\`.hover/hover-map.md\\` with its spec filename. Report covered vs still-open. A LARGE app doesn't have to finish in one go — covering a batch + updating the map is a complete, resumable unit; re-invoke to continue the uncovered lines.\n\n## Phase 5 — Lift shared flows into Page Objects (ASK first)\nOnce specs are crystallized, call \\`detect_shared_flows\\`. If it reports a NON-login flow repeated across specs (login is already handled by the auth setup), tell the user which specs share it and ASK whether to lift it into a shared Page Object (so a UI change to that flow is a one-place fix). On yes → \\`extract_page_objects\\` (generates \\`pages/*\\` + \\`fixtures.ts\\` and folds the specs to \\`await xPage.x()\\`). If nothing is shared, skip silently — most small suites have nothing to lift; don't force it.\n\n## Understand the business — ASK, then REMEMBER\nWhen you genuinely can't resolve something on your own — is this a bug or by-design? which flows matter? what does this domain term mean? — ASK the user (don't guess, don't stop). When they confirm a durable business RULE, call \\`record_fact\\` to persist it (RULES ONLY — never credentials/secrets/PII). Also ASK when blocked on something only they can provide (login credentials, a file). Stay on the app under test — never navigate to external origins.`;\n}\n"],"mappings":"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","names":["server","ctx"]}
1
+ {"version":3,"sources":["../src/mcp.ts","../src/mcp/controller.ts","../src/mcp/server.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport { chromium, type Browser, type Page } from 'playwright-core';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n launchDebugChrome,\n writeSpec,\n writeApiSpec,\n writeFact,\n recallMemory,\n readFact,\n formatFact,\n readSidecar,\n detectExtractableFlows,\n extractPageObjects,\n buildOptimizeBrief,\n saveOptimizedCandidate,\n lintWiki,\n appendWikiLog,\n type SkillStep,\n type ApiCheck,\n type Redaction,\n} from '@hover-dev/core/engine';\nimport { HoverMcpController } from './mcp/controller.js';\nimport { createHoverMcpServer } from './mcp/server.js';\n\n/*\n * `hover-mcp` — the MCP-first surface. Add it to your OWN agent (Claude Code,\n * Cursor, …); the agent drives the app through Hover's grounded tools and calls\n * crystallize_spec to save a plain Playwright spec. Hover spawns no agent here —\n * the calling agent IS the intelligence; Hover guarantees record==replay at the\n * output. Config via env: HOVER_TARGET, HOVER_CDP_PORT, HOVER_PROJECT_ROOT.\n *\n * NOTE: stdio is the MCP transport — never write to stdout from this process.\n */\n\nconst TARGET = process.env.HOVER_TARGET || 'http://localhost:5173';\nconst PORT = Number(process.env.HOVER_CDP_PORT || 9222);\nconst DEV_ROOT = process.env.HOVER_PROJECT_ROOT || process.cwd();\nconst CDP_URL = `http://localhost:${PORT}`;\n\nconst originOf = (u: string): string | null => {\n try {\n return new URL(u).origin;\n } catch {\n return null;\n }\n};\n\nlet browser: Browser | null = null;\n\n/** Launch/connect the debug Chrome lazily and return the page on the app. */\nasync function getPage(): Promise<Page> {\n if (!browser || !browser.isConnected()) {\n await launchDebugChrome({ port: PORT, url: TARGET });\n browser = await chromium.connectOverCDP(CDP_URL, { timeout: 8000 });\n }\n const pages = browser.contexts().flatMap((ctx) => ctx.pages());\n const want = originOf(TARGET);\n const match = want ? pages.find((p) => originOf(p.url()) === want) : undefined;\n if (match) return match;\n if (pages.length) return pages[pages.length - 1];\n const ctx = browser.contexts()[0] ?? (await browser.newContext());\n return ctx.newPage();\n}\n\nconst controller = new HoverMcpController({\n getPage,\n crystallize: async (name: string, description: string | undefined, steps: SkillStep[], redactions: Redaction[]) => {\n const res = await writeSpec({ devRoot: DEV_ROOT, name, description, steps, redactions, startUrl: TARGET, overwrite: true });\n await appendWikiLog(DEV_ROOT, 'crystallize', `${basename(res.path)} — ${name}`);\n return { path: res.path };\n },\n crystallizeApi: async (name: string, description: string | undefined, checks: ApiCheck[]) => {\n const res = await writeApiSpec({ devRoot: DEV_ROOT, name, description, checks, startUrl: TARGET, overwrite: true });\n await appendWikiLog(DEV_ROOT, 'api', `${basename(res.path)} — ${name}`);\n return { path: res.path };\n },\n recordFact: (title, rule, type) =>\n writeFact(DEV_ROOT, { name: title, description: title, type, body: rule }),\n recall: () => recallMemory(DEV_ROOT),\n recallFact: async (name: string) => {\n const fact = await readFact(DEV_ROOT, name);\n return fact ? formatFact(fact) : null;\n },\n readSpecSteps: async (slug: string) => {\n const sc = await readSidecar(DEV_ROOT, slug);\n return sc ? { steps: sc.steps, startUrl: TARGET } : null;\n },\n detectSharedFlows: () => detectExtractableFlows(DEV_ROOT),\n extractPageObjects: async () => {\n const res = await extractPageObjects(DEV_ROOT);\n if (res.pages.length) {\n await appendWikiLog(DEV_ROOT, 'extract', `${res.pages.length} page object(s), folded ${res.folded.length} spec(s)`);\n }\n return res;\n },\n optimizeBrief: async (slug: string) => {\n try {\n const { prompt } = await buildOptimizeBrief(DEV_ROOT, slug);\n return { prompt };\n } catch (e) {\n return { error: e instanceof Error ? e.message.split('\\n')[0] : String(e) };\n }\n },\n saveOptimized: (slug: string, code: string) => saveOptimizedCandidate(DEV_ROOT, slug, code),\n lintWiki: () => lintWiki(DEV_ROOT),\n});\n\nconst server = createHoverMcpServer(controller);\nawait server.connect(new StdioServerTransport());\n","import { request as pwRequest, type Page } from 'playwright-core';\nimport {\n groundedLocate,\n replayOnPage,\n type GroundedTarget,\n type SkillStep,\n type ApiCheck,\n type ReplayStep,\n type Redaction,\n type SharedFlow,\n type ExtractResult,\n type LintResult,\n} from '@hover-dev/core/engine';\n\n/*\n * The hover-mcp engine, decoupled from the MCP wire layer so it's testable with\n * a mock Page. The user's OWN agent (Claude Code / Cursor) calls these via MCP:\n * it reads the page (snapshot), actuates with GROUNDED targets (role+name →\n * testId → text), and Hover buffers each successful actuation as a SkillStep.\n * `crystallize` turns the buffer into a plain Playwright spec — record==replay,\n * because the buffered selectors ARE the ones that drove the page.\n *\n * API layer: while the agent drives the UI, Hover passively buffers the app's\n * xhr/fetch traffic off the SAME CDP connection (no MITM proxy). The agent reads\n * it with `capture_requests`, verifies a contract/authz check with\n * `replay_request`, and crystallizes the worthwhile ones into a\n * `*.api-test.spec.ts`. record == replay holds for the API layer too.\n */\n\nexport type FactType = 'business-rule' | 'expected-behavior' | 'validation' | 'access-policy';\n\n/** A passively-observed xhr/fetch call (metadata + a light body shape). */\nexport interface CapturedRequest {\n method: string;\n url: string;\n status: number;\n contentType?: string;\n /** Request post data, truncated. */\n requestBody?: string;\n /** Top-level keys of a JSON response (a light shape hint). */\n responseKeys?: string[];\n}\n\nexport interface McpDeps {\n /** Resolve the live page on the app under test (launch/connect lazily). */\n getPage: () => Promise<Page>;\n /** Write the buffered steps to a UI spec; returns the written path. `redactions`\n * parameterize captured credentials into `process.env.<envVar>` refs (and let\n * auth-fixture detect the login prefix). */\n crystallize: (\n name: string,\n description: string | undefined,\n steps: SkillStep[],\n redactions: Redaction[],\n ) => Promise<{ path: string }>;\n /** Write selected API checks to a `*.api-test.spec.ts`; returns the path. */\n crystallizeApi: (name: string, description: string | undefined, checks: ApiCheck[]) => Promise<{ path: string }>;\n /** Persist a learned business rule to .hover/memory/ (rules only — no secrets). */\n recordFact?: (title: string, rule: string, type: FactType) => Promise<{ path: string } | { error: string }>;\n /** Recall known business knowledge from .hover/memory/ ('' if none). Progressive:\n * full bodies when the set is small, the index alone when it's large. */\n recall?: () => Promise<string>;\n /** Read ONE remembered rule's full text by name/slug (behind recall_fact), or\n * null if nothing matches — the on-demand tier of progressive recall. */\n recallFact?: (name: string) => Promise<string | null>;\n /** Read a saved spec's recorded grounded steps (its `.hover/sidecars/<slug>.json`)\n * so self-heal can replay them against the live app. */\n readSpecSteps?: (slug: string) => Promise<{ steps: SkillStep[]; startUrl?: string } | null>;\n /** Detect NON-login flows shared across saved specs (for the extract offer). */\n detectSharedFlows?: () => Promise<SharedFlow[]>;\n /** Lift shared flows into Page Objects + fold the specs that use them. */\n extractPageObjects?: () => Promise<ExtractResult>;\n /** Build the optimize (F7) brief for a spec — the improvement rules + the spec\n * + its observed session + reusable Page Objects — for the user's OWN agent to\n * work from. `{ error }` when the spec doesn't exist. No model runs. */\n optimizeBrief?: (slug: string) => Promise<{ prompt: string } | { error: string }>;\n /** File an agent-improved spec as a REVIEW candidate: validate it against the\n * deterministic guardrails, soft-batch, and write `.hover/cache/optimized/\n * <slug>.spec.ts.draft` (never the original). Throws if it fails validation. */\n saveOptimized?: (slug: string, code: string) => Promise<{ candidatePath: string }>;\n /** Deterministic health check over `.hover/`: map vs spec files vs run ledger. */\n lintWiki?: () => Promise<LintResult>;\n}\n\nfunction describe(g: GroundedTarget): string {\n if (g.role && g.name) return `${g.role} \"${g.name}\"`;\n if (g.testId) return `testId \"${g.testId}\"`;\n if (g.text) return `text \"${g.text}\"`;\n return '(no target)';\n}\n\nconst MAX_CAPTURED = 200; // ring-buffer cap so a long run can't grow unbounded\n\nexport class HoverMcpController {\n /** The grounded-action buffer — sliced by `crystallize`. */\n readonly steps: SkillStep[] = [];\n /** Credentials typed into password fields — parameterized to process.env in\n * the crystallized spec (never written literally) + used to detect the login\n * prefix for auth-as-fixture. */\n private readonly redactions: Redaction[] = [];\n /** Passively-observed xhr/fetch traffic — read by `capture_requests`. */\n private readonly captured: CapturedRequest[] = [];\n /** Pages we've already attached a network listener to (avoid duplicates). */\n private readonly listening = new WeakSet<Page>();\n\n constructor(private readonly deps: McpDeps) {}\n\n private push(tool: string, input: unknown): void {\n this.steps.push({ kind: 'step', tool, input });\n }\n\n /** getPage + ensure the passive network listener is attached to it. */\n private async livePage(): Promise<Page> {\n const page = await this.deps.getPage();\n if (!this.listening.has(page)) {\n this.listening.add(page);\n page.on('response', (response) => {\n void this.onResponse(response).catch(() => {});\n });\n }\n return page;\n }\n\n private async onResponse(response: import('playwright-core').Response): Promise<void> {\n const req = response.request();\n const rt = req.resourceType();\n if (rt !== 'xhr' && rt !== 'fetch') return; // API calls only — skip docs/assets\n const contentType = (response.headers()['content-type'] || '').split(';')[0] || undefined;\n let responseKeys: string[] | undefined;\n if (contentType === 'application/json') {\n try {\n const body = await response.json();\n if (body && typeof body === 'object' && !Array.isArray(body)) {\n responseKeys = Object.keys(body as Record<string, unknown>).slice(0, 24);\n }\n } catch {\n /* body unavailable / not JSON — metadata only */\n }\n }\n const post = req.postData();\n this.captured.push({\n method: req.method(),\n url: req.url(),\n status: response.status(),\n contentType,\n requestBody: post ? post.slice(0, 2000) : undefined,\n responseKeys,\n });\n if (this.captured.length > MAX_CAPTURED) this.captured.splice(0, this.captured.length - MAX_CAPTURED);\n }\n\n async navigate(url: string): Promise<string> {\n const page = await this.livePage();\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });\n this.push('browser_navigate', { url });\n return `navigated to ${url}`;\n }\n\n /** ARIA snapshot of the page — the agent reads role+name from here. */\n async snapshot(): Promise<string> {\n const page = await this.livePage();\n return await page.locator('body').ariaSnapshot();\n }\n\n private async resolve(g: GroundedTarget) {\n const page = await this.livePage();\n const loc = groundedLocate(page, g);\n if (!loc) throw new Error(`pass role+name (preferred), or testId, or text — taken from the snapshot`);\n return loc;\n }\n\n async click(g: GroundedTarget): Promise<string> {\n const loc = await this.resolve(g);\n await loc.click({ timeout: 8000 });\n this.push('click_control', g);\n return `✓ clicked ${describe(g)}`;\n }\n\n async fill(g: GroundedTarget, value: string): Promise<string> {\n const loc = await this.resolve(g);\n await loc.fill(value, { timeout: 8000 });\n this.push('fill_control', { ...g, value });\n // A value typed into a password field is a secret: parameterize it to\n // process.env so it never lands literally in the spec/sidecar, and so\n // auth-as-fixture can detect this fill as part of the login prefix.\n if (value) {\n let isPassword = false;\n try {\n isPassword = (await loc.getAttribute('type')) === 'password';\n } catch {\n /* locator has no getAttribute (test mock) or field gone — treat as non-secret */\n }\n if (isPassword && !this.redactions.some((r) => r.value === value)) {\n this.redactions.push({ value, envVar: 'HOVER_PASSWORD' });\n }\n }\n return `✓ filled ${describe(g)}`;\n }\n\n async select(g: GroundedTarget, value: string): Promise<string> {\n const loc = await this.resolve(g);\n await loc.selectOption(value, { timeout: 8000 });\n this.push('select_control', { ...g, value });\n return `✓ selected ${value} in ${describe(g)}`;\n }\n\n async check(g: GroundedTarget, checked: boolean): Promise<string> {\n const loc = await this.resolve(g);\n if (checked) await loc.check({ timeout: 8000 });\n else await loc.uncheck({ timeout: 8000 });\n this.push('check_control', { ...g, checked });\n return `✓ ${checked ? 'checked' : 'unchecked'} ${describe(g)}`;\n }\n\n async assertVisible(g: GroundedTarget): Promise<string> {\n const loc = await this.resolve(g);\n const visible = await loc.first().isVisible();\n if (!visible) throw new Error(`${describe(g)} is not visible`);\n this.push('assert_visible', { ...g, matcher: 'visible' });\n return `✓ ${describe(g)} is visible`;\n }\n\n /** Return the passively-observed xhr/fetch traffic (optionally filtered) as\n * JSON for the agent to reason over when deciding API checks. */\n captureRequests(filter?: { urlContains?: string; method?: string }): string {\n let rows = this.captured;\n if (filter?.urlContains) rows = rows.filter((r) => r.url.includes(filter.urlContains!));\n if (filter?.method) rows = rows.filter((r) => r.method.toUpperCase() === filter.method!.toUpperCase());\n if (rows.length === 0) {\n return 'No xhr/fetch traffic captured yet. Drive the app (navigate / click) so its API calls fire, then call this again.';\n }\n return JSON.stringify(rows.slice(-60), null, 2);\n }\n\n /** Send a (possibly mutated) request and return the response summary — the\n * API analogue of replaying a grounded step, so an authz/contract check is\n * VERIFIED against the live app before it's crystallized (no confabulation).\n * `authenticated` (default true) replays with the browser session's cookies;\n * false uses a fresh context (no session) for \"requires auth\" checks. */\n async replayRequest(opts: {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: unknown;\n authenticated?: boolean;\n }): Promise<string> {\n const page = await this.livePage();\n const authed = opts.authenticated !== false;\n const ctx = authed ? page.context().request : await pwRequest.newContext();\n try {\n const m = opts.method.toUpperCase();\n const reqOpts: { headers?: Record<string, string>; data?: unknown } = {};\n if (opts.headers) reqOpts.headers = opts.headers;\n if (opts.body !== undefined && m !== 'GET' && m !== 'HEAD') reqOpts.data = opts.body;\n const res = await ctx.fetch(opts.url, { method: m, ...reqOpts });\n const contentType = (res.headers()['content-type'] || '').split(';')[0] || '';\n let preview = '';\n try {\n preview = contentType === 'application/json' ? JSON.stringify(await res.json()).slice(0, 800) : (await res.text()).slice(0, 400);\n } catch {\n /* no body */\n }\n return JSON.stringify({ status: res.status(), ok: res.ok(), contentType, body: preview, authenticated: authed }, null, 2);\n } finally {\n if (!authed) await ctx.dispose();\n }\n }\n\n /** Recall what earlier runs learned about this app's business rules. Progressive:\n * a large memory comes back as an INDEX; use recallFact to pull one rule's body. */\n async recall(): Promise<string> {\n if (!this.deps.recall) return 'No business memory available.';\n const known = await this.deps.recall();\n return known || 'No business knowledge recorded yet for this app.';\n }\n\n /** Read one remembered rule's full text by name (the on-demand tier — used when\n * recall returned only the index and the agent needs a specific rule's body). */\n async recallFact(name: string): Promise<string> {\n if (!this.deps.recallFact) return 'Rule lookup unavailable in this server.';\n const body = await this.deps.recallFact(name);\n return body ?? `No remembered rule matches \"${name}\". Call recall_business_knowledge to see the index of known rules.`;\n }\n\n /** Persist a confirmed business RULE so future runs don't re-ask it. Rules\n * only — never secrets / credentials / PII. */\n async recordFact(title: string, rule: string, type: FactType = 'business-rule'): Promise<string> {\n if (!this.deps.recordFact) return 'Memory channel unavailable; continuing.';\n const res = await this.deps.recordFact(title, rule, type);\n return 'error' in res ? `✗ could not save fact: ${res.error}` : `✓ remembered: ${title}`;\n }\n\n /** Crystallize the buffered UI flow → a plain Playwright spec, then clear the\n * buffer for the next flow. */\n async crystallize(name: string, description?: string): Promise<string> {\n if (this.steps.length === 0) return 'Nothing to crystallize yet — actuate some controls first.';\n const flow = [...this.steps];\n const { path } = await this.deps.crystallize(name, description, flow, [...this.redactions]);\n this.steps.length = 0;\n return `✓ wrote ${path} (${flow.length} step${flow.length === 1 ? '' : 's'})`;\n }\n\n /** Crystallize agent-selected API checks → a `*.api-test.spec.ts`. The agent\n * decides WHICH calls are worth locking (a real contract / authz boundary),\n * not every captured request. */\n async crystallizeApiSpec(name: string, description: string | undefined, checks: ApiCheck[]): Promise<string> {\n if (!checks?.length) return 'No checks provided — pass the API checks you verified worth locking.';\n const { path } = await this.deps.crystallizeApi(name, description, checks);\n return `✓ wrote ${path} (${checks.length} check${checks.length === 1 ? '' : 's'})`;\n }\n\n /** Self-heal detection: replay a saved spec's RECORDED grounded steps against\n * the live app and report the first step that no longer locates — the drift\n * point the agent re-grounds. No `playwright test`, no install; the same\n * grounded replay as creation-verification, seeded from the spec's sidecar. */\n async replaySpec(slug: string): Promise<string> {\n if (!this.deps.readSpecSteps) return 'Spec replay unavailable in this server.';\n const sc = await this.deps.readSpecSteps(slug);\n if (!sc) {\n return `No sidecar for \"${slug}\" — only Hover-crystallized specs can be replayed (looked for .hover/sidecars/${slug}.json).`;\n }\n const page = await this.livePage();\n const devUrl = sc.startUrl ?? page.url();\n const res = await replayOnPage(page, devUrl, sc.steps as ReplayStep[]);\n if (res.ok) {\n return `✓ \"${slug}\" still replays clean — ${res.ran}/${res.total} grounded steps located. No drift to heal.`;\n }\n const f = res.failures[0];\n return JSON.stringify(\n {\n spec: slug,\n drifted: true,\n ranBeforeBreak: res.ran,\n total: res.total,\n brokeAtStep: f.index,\n tool: f.tool,\n lookingFor: f.target,\n error: f.error,\n next: 'Re-snapshot at this point, re-locate the control by the intent in \"lookingFor\" (its label/role may have changed), re-drive from here, then crystallize_spec with the SAME name to overwrite the healed spec.',\n },\n null,\n 2,\n );\n }\n\n /** LLM-Wiki P1 Lint: run the deterministic `.hover/` health check (map vs\n * specs vs runs) and return it as a readable report the agent acts on (heal a\n * regressed line, map an orphan spec, drop a dead ref). The LLM-judged checks\n * (contradictory rules, code routes missing from the map) are driven on top of\n * this by the /mcp__hover__lint prompt. */\n async lintWiki(): Promise<string> {\n if (!this.deps.lintWiki) return 'Wiki lint is unavailable in this server.';\n const res = await this.deps.lintWiki();\n if (!res.hasMap) {\n return 'No .hover/hover-map.md yet — nothing to lint. Run /mcp__hover__test_app first to map the app and crystallize specs.';\n }\n const { areas, lines, covered, specs } = res.summary;\n const head = `Wiki lint — ${covered}/${lines} lines covered across ${areas} area${areas === 1 ? '' : 's'}, ${specs} spec file${specs === 1 ? '' : 's'}.`;\n if (res.findings.length === 0) {\n return `${head}\\n✓ No drift: every mapped spec exists, no covered line is failing, no spec is unmapped.`;\n }\n const icon = { error: '✗', warn: '⚠', info: '·' } as const;\n const body = res.findings\n .map((f) => `${icon[f.severity]} [${f.kind}] ${f.message}${f.fix ? `\\n → ${f.fix}` : ''}`)\n .join('\\n');\n return `${head}\\n${res.findings.length} finding${res.findings.length === 1 ? '' : 's'}:\\n${body}`;\n }\n\n /** Report NON-login flows repeated across the saved specs — so the agent can\n * ASK the user whether to lift them into a shared Page Object. Read-only. */\n async detectSharedFlows(): Promise<string> {\n if (!this.deps.detectSharedFlows) return 'Shared-flow detection unavailable in this server.';\n const flows = await this.deps.detectSharedFlows();\n if (!flows.length) {\n return 'No extractable shared flows — no set of specs shares a non-login entry flow (login is already handled by the auth setup). Nothing to lift.';\n }\n return JSON.stringify(\n flows.map((f) => ({ sharedBy: f.specs, steps: f.prose })),\n null,\n 2,\n );\n }\n\n /** Optimize (F7), MCP-first: return the improvement brief for the user's own\n * agent to work from (the agent IS the model — Hover picks none). Never\n * throws; a missing spec / unavailable channel comes back as a plain message\n * the agent reads (this feeds a PROMPT, which isn't wrapped in the ✗ guard). */\n async optimizeBrief(slug: string): Promise<string> {\n if (!this.deps.optimizeBrief) return `Optimize is unavailable in this server (heal/optimize need spec sidecars).`;\n try {\n const res = await this.deps.optimizeBrief(slug);\n if ('error' in res) {\n return `Can't optimize \"${slug}\": ${res.error}. Optimize only works on a Hover-crystallized spec — list __vibe_tests__/*.spec.ts and pass one by slug.`;\n }\n return res.prompt;\n } catch (e) {\n return `Can't build the optimize brief for \"${slug}\": ${e instanceof Error ? e.message.split('\\n')[0] : String(e)}`;\n }\n }\n\n /** File an agent-improved spec as a review candidate. Validation failures throw\n * (the tool guard turns them into a ✗ the agent can fix and retry against). */\n async saveOptimized(slug: string, code: string): Promise<string> {\n if (!this.deps.saveOptimized) return 'Optimize is unavailable in this server.';\n const { candidatePath } = await this.deps.saveOptimized(slug, code);\n return `✓ filed optimized candidate at ${candidatePath} (your spec is untouched). Tell the user to diff it against __vibe_tests__/${slug}.spec.ts and, to keep it, replace the spec with the candidate.`;\n }\n\n /** Lift the detected shared flows into `pages/*` + `fixtures.ts` and fold the\n * specs that use them. Call ONLY after the user approves (detectSharedFlows\n * → ask → this). Deterministic; login flows are excluded (auth-fixture owns). */\n async extractPageObjects(): Promise<string> {\n if (!this.deps.extractPageObjects) return 'Page Object extraction unavailable in this server.';\n const res = await this.deps.extractPageObjects();\n if (!res.pages.length) return 'Nothing to extract — no shared non-login flow found.';\n return `✓ lifted ${res.pages.length} Page Object${res.pages.length === 1 ? '' : 's'} (${res.pages\n .map((p) => p.className)\n .join(', ')}) into __vibe_tests__/pages + fixtures.ts; folded ${res.folded.length} spec${\n res.folded.length === 1 ? '' : 's'\n } (${res.folded.join(', ')}) to consume them.`;\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { HoverMcpController } from './controller.js';\n\n/* Wrap the controller as an MCP server. Tool names + the GROUNDED target shape\n * mirror Hover's control MCP so an agent that knows one knows the other. Every\n * handler returns text (✓/✗) and never throws — a failed locate/action becomes\n * a ✗ message the calling agent can react to, not an MCP transport error. */\n\nconst md = (text: string) => ({ content: [{ type: 'text' as const, text }] });\nconst errLine = (e: unknown) => (e instanceof Error ? e.message.split('\\n')[0] : String(e));\n\nconst GROUND = {\n role: z.string().optional().describe(\"ARIA role from the snapshot, e.g. 'button', 'textbox', 'link'. Pair with `name`.\"),\n name: z.string().optional().describe('Accessible name from the snapshot, exactly as shown. Pair with `role`.'),\n testId: z.string().optional().describe('A data-testid, if the element has one and no clean role+name.'),\n text: z.string().optional().describe('Real visible text on the element — last resort.'),\n within: z\n .object({ role: z.string(), name: z.string() })\n .optional()\n .describe('Scope the search to a container first (role+name) when a label repeats across groups.'),\n};\n\n/** One asserted API call for crystallize_api_spec — mirrors core's ApiCheck. */\nconst API_CHECK = z.object({\n title: z.string().describe('Short test name, e.g. \"GET /api/cart returns the cart\".'),\n method: z.string(),\n url: z.string().describe('Full URL or same-origin path (same-origin is relativized in the spec).'),\n requestBody: z.any().optional(),\n headers: z.record(z.string(), z.string()).optional(),\n expectStatus: z.number().optional().describe('Expected status — verified with replay_request.'),\n expectBodyKeys: z.array(z.string()).optional().describe('Top-level response keys to assert present.'),\n note: z.string().optional().describe('Emitted as a leading comment, e.g. \"authz: no session → 401\".'),\n});\n\nexport function createHoverMcpServer(c: HoverMcpController): McpServer {\n const server = new McpServer({ name: 'hover', version: '0.1.0' });\n const guard = (fn: () => Promise<string>) => fn().then(md, (e) => md(`✗ ${errLine(e)}`));\n\n server.registerTool(\n 'browser_navigate',\n { description: 'Open a URL in the app under test (the debug Chrome).', inputSchema: { url: z.string() } },\n ({ url }) => guard(() => c.navigate(url)),\n );\n\n server.registerTool(\n 'browser_snapshot',\n {\n description:\n 'Read the current page as an ARIA snapshot (role + accessible-name tree). Call this BEFORE actuating to get the exact role+name to pass to the *_control tools.',\n inputSchema: {},\n },\n () => guard(() => c.snapshot()),\n );\n\n server.registerTool(\n 'click_control',\n {\n description:\n 'Click a control by a GROUNDED target read off the snapshot (role+name preferred → testId → text). Grounded so the saved spec replays exactly what you did.',\n inputSchema: GROUND,\n },\n (g) => guard(() => c.click(g)),\n );\n\n server.registerTool(\n 'fill_control',\n { description: 'Type a value into a textbox/field by a grounded target.', inputSchema: { ...GROUND, value: z.string() } },\n ({ value, ...g }) => guard(() => c.fill(g, value)),\n );\n\n server.registerTool(\n 'select_control',\n { description: 'Choose an option in a <select> by a grounded target.', inputSchema: { ...GROUND, value: z.string() } },\n ({ value, ...g }) => guard(() => c.select(g, value)),\n );\n\n server.registerTool(\n 'check_control',\n {\n description: 'Check or uncheck a checkbox/radio by a grounded target (handles sr-only / hidden inputs).',\n inputSchema: { ...GROUND, checked: z.boolean().optional().describe('true (default) = check; false = uncheck.') },\n },\n ({ checked, ...g }) => guard(() => c.check(g, checked !== false)),\n );\n\n server.registerTool(\n 'assert_visible',\n {\n description: 'Assert a control/text is visible now — captures an expect(...).toBeVisible() into the saved spec.',\n inputSchema: GROUND,\n },\n (g) => guard(() => c.assertVisible(g)),\n );\n\n server.registerTool(\n 'recall_business_knowledge',\n {\n description:\n 'Recall what earlier Hover runs learned about this app (business rules, expected behaviors, access policies). Call this at the START so you do not re-ask settled questions. Treat what it returns as ground truth. For an app with a lot of remembered rules this returns an INDEX (one line per rule); when a rule is relevant to what you are testing, read its full text with recall_fact.',\n inputSchema: {},\n },\n () => guard(() => c.recall()),\n );\n\n server.registerTool(\n 'recall_fact',\n {\n description:\n \"Read ONE remembered business rule's full text by name (the name shown in the recall_business_knowledge index). Use it when recall returned only the index and you need a specific rule's detail before deciding how to test.\",\n inputSchema: {\n name: z.string().describe('The rule name from the recall index (a slug like \"guests-cannot-checkout\").'),\n },\n },\n ({ name }) => guard(() => c.recallFact(name)),\n );\n\n server.registerTool(\n 'record_fact',\n {\n description:\n \"Remember a durable BUSINESS RULE about this app so neither you nor a future run re-asks it — e.g. an expected behavior, a validation rule, an access policy, or the answer to a 'bug or by-design?' the user just confirmed. State it as a clean self-contained rule. RULES ONLY — never store secrets, passwords, tokens, or personal data.\",\n inputSchema: {\n title: z.string().describe('Short title for the rule (becomes its memory filename + index entry).'),\n rule: z.string().describe('The rule itself, stated clearly and self-contained (no secrets/PII).'),\n type: z\n .enum(['business-rule', 'expected-behavior', 'validation', 'access-policy'])\n .optional()\n .describe('What kind of knowledge this is. Defaults to business-rule.'),\n },\n },\n ({ title, rule, type }) => guard(() => c.recordFact(title, rule, type ?? 'business-rule')),\n );\n\n server.registerTool(\n 'crystallize_spec',\n {\n description:\n 'Save the flow you JUST performed (the grounded click/fill/select/check/assert actions since the last crystallize) as a plain Playwright .spec.ts in __vibe_tests__/. Call it the moment you finish a coherent end-to-end flow. Name it in English, imperative (e.g. \"Log in\").',\n inputSchema: {\n name: z.string().describe('Short imperative flow name in English — becomes the spec filename + test name.'),\n description: z.string().optional().describe('One line on what this flow verifies.'),\n },\n },\n ({ name, description }) => guard(() => c.crystallize(name, description)),\n );\n\n // ── API layer ──────────────────────────────────────────────────────────────\n // While you drive the UI, Hover passively buffers the app's xhr/fetch traffic\n // off the same CDP connection (no MITM). Read it, verify a check, crystallize.\n\n server.registerTool(\n 'capture_requests',\n {\n description:\n \"Return the app's xhr/fetch API calls observed while you drove the UI (method, url, status, content-type, request body, response shape). Call it after exercising a flow to see which endpoints it hit. Optionally filter.\",\n inputSchema: {\n urlContains: z.string().optional().describe('Only calls whose URL contains this substring.'),\n method: z.string().optional().describe('Only calls with this HTTP method.'),\n },\n },\n ({ urlContains, method }) => guard(() => Promise.resolve(c.captureRequests({ urlContains, method }))),\n );\n\n server.registerTool(\n 'replay_request',\n {\n description:\n 'Send a (possibly mutated) request and return the response, to VERIFY an API check before crystallizing it (no confabulated status codes). For an authz check, set authenticated:false (fresh context, no session) and expect 401/403; or drop/alter headers / swap an id for IDOR.',\n inputSchema: {\n method: z.string().describe('HTTP method, e.g. GET / POST.'),\n url: z.string().describe('Full URL or same-origin path.'),\n headers: z.record(z.string(), z.string()).optional().describe('Headers to send (omit/blank the auth header for a \"requires auth\" check).'),\n body: z.any().optional().describe('Request body for POST/PUT/PATCH.'),\n authenticated: z\n .boolean()\n .optional()\n .describe('true (default) = replay with the browser session; false = fresh context with NO session (for \"requires auth\" checks).'),\n },\n },\n ({ method, url, headers, body, authenticated }) =>\n guard(() => c.replayRequest({ method, url, headers, body, authenticated })),\n );\n\n server.registerTool(\n 'crystallize_api_spec',\n {\n description:\n 'Save selected API checks as a plain Playwright `*.api-test.spec.ts` (uses the `request` fixture). Use it ALONGSIDE crystallize_spec when a flow exercised a worthwhile API surface — a real contract, a data mutation, or an authz boundary. Be SELECTIVE: lock checks that matter, not every captured call. Verify each with replay_request first.',\n inputSchema: {\n name: z.string().describe('Short imperative English name — becomes the <name>.api-test.spec.ts filename.'),\n description: z.string().optional().describe('One line on what this API spec verifies.'),\n checks: z.array(API_CHECK).describe('The API checks to lock — each a request + its expected status / shape / authz outcome.'),\n },\n },\n ({ name, description, checks }) => guard(() => c.crystallizeApiSpec(name, description, checks)),\n );\n\n // ── Self-heal ────────────────────────────────────────────────────────────\n server.registerTool(\n 'replay_spec',\n {\n description:\n \"Detect drift in a saved spec: replay its RECORDED grounded steps against the LIVE app and report the first step that no longer locates (its index + what it was looking for). No `playwright test` needed. Use this to find what to heal, then re-ground that step and re-crystallize.\",\n inputSchema: {\n slug: z.string().describe('The spec slug = its filename without .spec.ts (e.g. \"login\" for login.spec.ts).'),\n },\n },\n ({ slug }) => guard(() => c.replaySpec(slug)),\n );\n\n // ── Page Object extraction (detect → ask → extract) ──────────────────────\n server.registerTool(\n 'detect_shared_flows',\n {\n description:\n 'After crystallizing the suite, report NON-login flows repeated across specs (login is already handled by the auth setup). Use it to decide whether to OFFER lifting a shared flow into a Page Object — then ASK the user before extracting.',\n inputSchema: {},\n },\n () => guard(() => c.detectSharedFlows()),\n );\n\n server.registerTool(\n 'extract_page_objects',\n {\n description:\n 'Lift the shared flows into __vibe_tests__/pages/* + fixtures.ts and fold the specs that use them (they call `await xPage.x()` from ./fixtures). Deterministic. Call ONLY after the user approves the offer from detect_shared_flows.',\n inputSchema: {},\n },\n () => guard(() => c.extractPageObjects()),\n );\n\n // ── Wiki lint (LLM-Wiki P1) ──────────────────────────────────────────────\n server.registerTool(\n 'lint_map',\n {\n description:\n \"Health-check the app's test wiki (.hover/): cross-check the business map against the real spec files and the run ledger. Reports deleted specs (a line points at a missing *.spec.ts), regressed coverage (a covered line's spec last ran fail/flaky → heal it), and orphan specs (a spec no line maps). Deterministic; no LLM. Run it to find drift, then fix each finding (heal / re-map / drop the stale ref).\",\n inputSchema: {},\n },\n () => guard(() => c.lintWiki()),\n );\n\n // ── Optimize (F7) ────────────────────────────────────────────────────────\n // The IMPROVEMENT is the agent's (the /mcp__hover__optimize prompt gives it the\n // brief); this tool is Hover's guardrail + write path — it validates the agent's\n // result and files it as a review candidate, never touching the spec.\n server.registerTool(\n 'save_optimized_spec',\n {\n description:\n \"File an improved spec (produced from the /optimize brief) as a REVIEW CANDIDATE. Hover validates it (semantic selectors, no waitForTimeout/XPath, parses), soft-batches trailing assertions, and writes .hover/cache/optimized/<slug>.spec.ts.draft — it does NOT overwrite your spec. On a ✗ (rejected check), fix it and call again. This is the only way to file an optimization; don't write the .draft yourself.\",\n inputSchema: {\n slug: z.string().describe('The spec slug being optimized (its filename without .spec.ts).'),\n code: z.string().describe('The COMPLETE improved .ts file contents.'),\n },\n },\n ({ slug, code }) => guard(() => c.saveOptimized(slug, code)),\n );\n\n // The workflow ships WITH the server as an MCP prompt — Claude Code surfaces\n // it as `/mcp__hover__test_app`, so adding the server brings both the tools\n // AND the command. No project scaffolding needed.\n server.registerPrompt(\n 'test_app',\n {\n title: 'Hover — map & crystallize a test suite',\n description: 'Map this app\\'s business lines and crystallize a Playwright suite (incremental, scales to large apps).',\n argsSchema: { scope: z.string().optional().describe('An area/flow to focus on. Omit to cover the whole app.') },\n },\n ({ scope }) => ({\n messages: [{ role: 'user', content: { type: 'text', text: workflowPrompt(scope) } }],\n }),\n );\n\n // Optimize workflow — surfaced as `/mcp__hover__optimize`. Hover builds the\n // brief (spec + observed session + Page Objects); the agent does the thinking.\n server.registerPrompt(\n 'optimize',\n {\n title: 'Hover — enrich a spec with observed assertions',\n description: 'Improve a crystallized spec: add assertions for what the session observed, de-literalize volatile values, reuse Page Objects. Files a review candidate; never overwrites the spec.',\n argsSchema: { spec: z.string().describe('The spec slug to optimize (e.g. \"checkout\" for checkout.spec.ts).') },\n },\n async ({ spec }) => ({\n messages: [{ role: 'user' as const, content: { type: 'text' as const, text: await c.optimizeBrief(spec) } }],\n }),\n );\n\n // Wiki-lint workflow — surfaced as `/mcp__hover__lint`. The tool does the\n // mechanical checks; the prompt drives the LLM-judged half on top.\n server.registerPrompt(\n 'lint',\n {\n title: 'Hover — lint the test wiki',\n description: \"Health-check .hover/: deterministic drift (dead spec refs, regressed coverage, unmapped specs) plus LLM-judged checks (contradictory rules, code routes missing from the map), then offer fixes.\",\n argsSchema: {},\n },\n () => ({\n messages: [{ role: 'user' as const, content: { type: 'text' as const, text: lintPrompt() } }],\n }),\n );\n\n // Query workflow (LLM-Wiki P4) — surfaced as `/mcp__hover__ask`. Read the wiki,\n // answer with citations, optionally file a confirmed new rule back.\n server.registerPrompt(\n 'ask',\n {\n title: 'Hover — ask the test wiki',\n description: \"Answer a question about this app from its .hover/ wiki (business map + remembered rules + specs + run log), with citations. Read-only; can file a confirmed new rule back.\",\n argsSchema: { question: z.string().describe('The question to answer, e.g. \"what happens when a guest tries to check out?\"') },\n },\n ({ question }) => ({\n messages: [{ role: 'user' as const, content: { type: 'text' as const, text: askPrompt(question) } }],\n }),\n );\n\n // Self-heal workflow — surfaced as `/mcp__hover__heal`.\n server.registerPrompt(\n 'heal',\n {\n title: 'Hover — heal a drifted spec',\n description: \"Replay a saved spec against the live app; where the UI drifted, re-ground the broken step and re-crystallize. Pass a spec to heal one, or omit to check all.\",\n argsSchema: { spec: z.string().optional().describe('A spec slug to heal (e.g. \"login\"). Omit to check every spec.') },\n },\n ({ spec }) => ({\n messages: [{ role: 'user', content: { type: 'text', text: healPrompt(spec) } }],\n }),\n );\n\n return server;\n}\n\n/** Query workflow body (LLM-Wiki P4): read the wiki, answer with citations, and\n * optionally file a confirmed new rule back so the next question is cheaper. */\nfunction askPrompt(question: string): string {\n return `Answer a question about this app using its **test wiki** (\\`.hover/\\`) as the source of truth. The wiki = the business map (\\`.hover/hover-map.md\\`), the remembered business rules (\\`.hover/memory/\\` — via \\`recall_business_knowledge\\` / \\`recall_fact\\`), the crystallized specs under \\`__vibe_tests__/\\`, and the run history (\\`.hover/log.md\\`).\n\nQuestion: ${question}\n\n1. **Gather (read-only — don't drive the browser).** Call \\`recall_business_knowledge\\` for the rule index and pull specifics with \\`recall_fact\\`. Read \\`.hover/hover-map.md\\` for the business lines, coverage, and relationships. Skim the relevant \\`*.spec.ts\\` and \\`.hover/log.md\\` with your own file tools.\n2. **Answer directly, with CITATIONS.** Ground every claim in what you read — name the rule, the business line, or the spec file it came from. If the wiki genuinely doesn't cover it, say so plainly instead of guessing (and suggest running \\`/mcp__hover__test_app\\` to cover that area).\n3. **Optionally file it back.** If answering established a durable business RULE the wiki was missing and the user confirms it, persist it with \\`record_fact\\` (RULES only — never secrets / credentials / PII) so the next run doesn't re-derive it.\n\nStay on what the wiki + code actually say; don't invent behavior.`;\n}\n\n/** Wiki-lint workflow body. `lint_map` does the mechanical checks; the agent\n * layers the LLM-judged checks (contradictions, unmapped code routes) and fixes. */\nfunction lintPrompt(): string {\n return `Lint this app's **test wiki** (\\`.hover/\\`) using the Hover MCP tools, then fix what you find. The wiki = the business map (\\`.hover/hover-map.md\\`), the business-rule memory (\\`.hover/memory/*.md\\`), and the crystallized specs it points at.\n\n1. **Deterministic drift** — call \\`lint_map\\`. It cross-checks the map against the real spec files and the run ledger and reports:\n - **deleted-spec** — a line points at a \\`*.spec.ts\\` that's gone. Re-crystallize the flow, or drop the stale reference from the map.\n - **regressed-coverage** — a covered line whose spec last ran fail/flaky. Heal it with \\`/mcp__hover__heal <slug>\\` (or say it's a real app bug to fix).\n - **orphan-spec** — a spec no line maps. Add its business line to the map (\\`[x]\\` with the spec).\n2. **Contradictory rules (LLM-judged)** — read \\`.hover/memory/*.md\\`. If two facts conflict (e.g. one says guests can check out, another says they can't), surface the pair and ASK the user which holds; correct the wrong one with \\`record_fact\\` (or delete it).\n3. **Unmapped code routes (LLM-judged)** — with your own file tools, grep the app's router for user-facing routes. Any real route absent from \\`.hover/hover-map.md\\` is a coverage gap — add it as an uncovered \\`[ ]\\` line under the right area so the map stays honest.\n4. **Report + fix** — summarize the findings, apply the safe fixes (re-map, add gaps, correct rules), and for anything destructive (dropping a spec, deleting a rule) ASK first. Update \\`.hover/hover-map.md\\` as you go.\n\nStay on the app under test. Don't invent business lines that don't exist in the code.`;\n}\n\n/** Self-heal workflow body. The agent uses `replay_spec` to find the drift,\n * re-grounds the broken step by its recorded intent, and re-crystallizes. */\nfunction healPrompt(spec?: string): string {\n const scope = spec?.trim()\n ? `the spec \\`${spec.trim()}\\``\n : 'every spec under `__vibe_tests__/` (list them first, then heal each that drifted)';\n return `Heal ${scope} for this app using the **Hover MCP tools** — repair specs whose UI drifted, without rewriting them by hand.\n\nA spec \"drifted\" when the app changed so a recorded step no longer locates its control (a renamed button, a moved field). Healing = re-grounding ONLY the broken step against the current UI, keeping everything else, so record==replay still holds.\n\nWork ONE spec at a time:\n\n1. **Detect** — \\`replay_spec(\"<slug>\")\\`. It replays the spec's recorded grounded steps against the live app and reports the first step that fails to locate: its index, the tool, and what it was \\`lookingFor\\` (role+name/text). If it replays clean, that spec is fine — move on.\n2. **Re-ground the broken step** — \\`browser_navigate\\` to the spec's route, \\`browser_snapshot\\`, and find the control that NOW serves the intent in \\`lookingFor\\` (e.g. the submit button whose label changed \"Sign in\" → \"Log in\"). Re-drive from the break with the grounded \\`*_control\\` tools. Change ONLY what drifted — don't redesign the flow.\n3. **Re-crystallize** — when the flow runs green again, \\`crystallize_spec\\` with the SAME name as the broken spec to overwrite it with the healed version.\n4. **Report** — say which step drifted, what changed (old target → new target), and that it's re-crystallized. The user reviews the old-vs-new diff in the cockpit before keeping it.\n\nRules: heal by the recorded INTENT (re-locate the same control), never invent a new flow or new assertions. If a step is gone because the FEATURE was removed (not just renamed), don't guess — say so and ASK whether to drop that step or the whole spec. Stay on the app under test.`;\n}\n\n/** The phased, scale-aware workflow, delivered as the prompt body. Mirrors the\n * hover-mcp tool surface; the agent's own file tools do the code-reading. */\nfunction workflowPrompt(scope?: string): string {\n const target = scope?.trim() ? scope.trim() : 'the whole app';\n return `Build (or extend) a Playwright test suite for this web app using the **Hover MCP tools**.\nDrive the browser ONLY through these tools — they actuate via grounded selectors\n(role+name → testId → text), so every spec you save replays EXACTLY what you did\n(record==replay). Never write spec files yourself; only \\`crystallize_spec\\` does.\n\nTools: \\`recall_business_knowledge\\` / \\`recall_fact\\` · \\`browser_navigate\\` ·\n\\`browser_snapshot\\` (ARIA tree — read before acting) · \\`click_control\\` /\n\\`fill_control\\` / \\`select_control\\` / \\`check_control\\` (grounded target from the\nsnapshot) · \\`assert_visible\\` · \\`record_fact\\` · \\`crystallize_spec(name, description?)\\`.\nAPI layer: \\`capture_requests\\` · \\`replay_request\\` · \\`crystallize_api_spec\\`. Suite:\n\\`detect_shared_flows\\` · \\`extract_page_objects\\` · \\`replay_spec\\` · \\`lint_map\\`.\n\nTarget: the app at HOVER_TARGET (set in the server's env). Scope: ${target}.\n\n## First: are you bootstrapping or extending? (load only what this run needs)\nCheck whether \\`.hover/hover-map.md\\` already exists.\n- **It exists → you're EXTENDING.** Read it + call \\`recall_business_knowledge\\`, then go straight to Phase 2 and cover the uncovered \\`[ ]\\` lines. Skip the Phase-1 code-mapping — the map already IS the plan. Only re-map if the user says the app changed.\n- **It's absent → you're BOOTSTRAPPING.** Do the full Phase 1 below to build the map first.\nThis keeps a returning run cheap: you don't re-derive a map you already have.\n\n## Ground rules (they protect record==replay AND the user's real app)\n- **Grounded targets only.** Pass role+name EXACTLY as they appear in the LATEST \\`browser_snapshot\\`. If a locate fails, re-snapshot and read the real target — never guess, invent, or reuse a stale name.\n- **It's the user's REAL app.** Avoid irreversible / destructive actions — real payments, deleting data you didn't create, sending real emails or SMS — unless the user confirms this is a safe test environment. When unsure, ASK first.\n- **Assert stable outcomes.** Assert on semantic, durable signals (a success message, a heading, a new row's label) — NEVER volatile instance data (timestamps, generated ids, \"today\", a one-off order number), which makes the saved spec flaky on replay.\n- **Log in first.** If the app needs auth, do that before anything else — ask for credentials if you don't have them — then crystallize it as its own \"Log in\" spec and stay logged in for the rest of the run.\n\nWork in PHASES — this is what lets it scale from a tiny app to a large one.\n\n## Phase 1 — Map the business lines (read the CODE, don't click around)\n- FIRST call \\`recall_business_knowledge\\` — rules earlier runs learned (and read \\`.hover/hover-map.md\\` if it exists, the running map; CONTINUE it, don't start over). Treat both as ground truth; don't re-ask what they already answer. For an app with many remembered rules this returns an INDEX (one line per rule); when a rule is relevant to what you're about to test, pull its full text with \\`recall_fact(\"<name>\")\\`.\n- Use YOUR OWN file tools (read / grep / glob) to find the app's ROUTES + navigation: the router config, route/page files, the nav components. Enumerate the user-facing BUSINESS LINES (a coherent task a user performs), each with its entry route, grouped by area. Reading code is cheaper + more complete than clicking around, and finds areas behind auth / nav you'd otherwise miss.\n- Write/update \\`.hover/hover-map.md\\` as a checklist (4-space indent = a code block):\n\n # Business map — <app>\n ## Auth\n - [ ] Log in — /login\n - [x] Checkout — /checkout — checkout.spec.ts\n ## Relationships\n - Checkout depends-on Log in\n - Cart shares-state Checkout\n\n- Optionally add a \\`## Relationships\\` block recording inter-line edges you notice — \\`<line> depends-on <line>\\`, \\`<line> shares-state <line>\\`, or \\`<line> navigates-to <line>\\` (names must match lines above). These become graph edges in the cockpit's Business Map; they also tell a later run what a flow depends on. Record only real edges; skip the block if none stand out.\n- Don't test yet. For a large app, this map IS the plan.\n\n## Phase 2 — Pick the scope\n- If a scope was given, cover that. Otherwise show the uncovered lines and ask which to cover now (offer \"all uncovered\"). For a small app, just cover them all.\n\n## Phase 3 — Cover each chosen line, ONE AT A TIME\nFor each line: \\`browser_navigate\\` to its route → \\`browser_snapshot\\` → EXERCISE the real flow (click / fill / select / check — you are a tester, never just describe the page) → \\`assert_visible\\` on the OUTCOME (a success message, the new row, the next screen) → \\`crystallize_spec(\"<short imperative English name>\")\\`. Crystallize the MOMENT each flow is done, before the next — the buffer is per-flow. The tools share ONE browser, so cover lines SEQUENTIALLY.\n\n## Phase 3.5 — Lock the API layer too (SELECTIVELY)\nAs you drive each flow, Hover passively captures the app's xhr/fetch traffic. After a flow, call \\`capture_requests\\` to see what it hit. For a line that exercised a **worthwhile API surface — a data mutation (POST/PUT/DELETE), a clear contract, or an authz boundary** — also lock an API spec:\n- Decide the checks. Contract: the call returns its status + key fields. Authz: the same call WITHOUT the session must be refused — call \\`replay_request\\` with \\`authenticated:false\\` and confirm it's 401/403 before asserting it.\n- VERIFY each check with \\`replay_request\\` first (so you never assert a confabulated status), then \\`crystallize_api_spec(name, checks)\\` → a \\`*.api-test.spec.ts\\`.\n- Be SELECTIVE — skip pure-display reads, analytics, third-party pings. Most UI flows need 0–1 API specs. A static/read-only flow needs none. This is judgment, not a per-flow quota.\n\n## Phase 4 — Update coverage\n- Mark each covered line \\`[x]\\` in \\`.hover/hover-map.md\\` with its spec filename. Report covered vs still-open. A LARGE app doesn't have to finish in one go — covering a batch + updating the map is a complete, resumable unit; re-invoke to continue the uncovered lines.\n- Then call \\`lint_map\\` to catch wiki drift you may have introduced (a covered line whose spec now fails, a stale spec reference, a spec no line maps) and fix or report it. \\`/mcp__hover__lint\\` runs the deeper check any time.\n\n## Phase 5 — Lift shared flows into Page Objects (ASK first)\nOnce specs are crystallized, call \\`detect_shared_flows\\`. If it reports a NON-login flow repeated across specs (login is already handled by the auth setup), tell the user which specs share it and ASK whether to lift it into a shared Page Object (so a UI change to that flow is a one-place fix). On yes → \\`extract_page_objects\\` (generates \\`pages/*\\` + \\`fixtures.ts\\` and folds the specs to \\`await xPage.x()\\`). If nothing is shared, skip silently — most small suites have nothing to lift; don't force it.\n\n## Understand the business — ASK, then REMEMBER\nWhen you genuinely can't resolve something on your own — is this a bug or by-design? which flows matter? what does this domain term mean? — ASK the user (don't guess, don't stop). When they confirm a durable business RULE, call \\`record_fact\\` to persist it (RULES ONLY — never credentials/secrets/PII). Also ASK when blocked on something only they can provide (login credentials, a file). Stay on the app under test — never navigate to external origins.`;\n}\n"],"mappings":"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","names":["server","ctx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/mcp",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Hover MCP server — grounded browser actuation + crystallize a Playwright suite, for your own coding agent",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hyperyond",
@@ -36,7 +36,7 @@
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "playwright-core": "^1.50.0",
38
38
  "zod": "^4.4.3",
39
- "@hover-dev/core": "0.23.0"
39
+ "@hover-dev/core": "0.24.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^22.0.0",