@hover-dev/mcp 0.19.0 → 0.20.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
@@ -9,12 +9,16 @@ import {
9
9
  writeApiSpec,
10
10
  writeFact,
11
11
  loadMemory,
12
- formatMemoryForPrompt
12
+ formatMemoryForPrompt,
13
+ readSidecar
13
14
  } from "@hover-dev/core/engine";
14
15
 
15
16
  // src/mcp/controller.ts
16
17
  import { request as pwRequest } from "playwright-core";
17
- import { groundedLocate } from "@hover-dev/core/engine";
18
+ import {
19
+ groundedLocate,
20
+ replayOnPage
21
+ } from "@hover-dev/core/engine";
18
22
  function describe(g) {
19
23
  if (g.role && g.name) return `${g.role} "${g.name}"`;
20
24
  if (g.testId) return `testId "${g.testId}"`;
@@ -190,6 +194,39 @@ var HoverMcpController = class {
190
194
  const { path } = await this.deps.crystallizeApi(name, description, checks);
191
195
  return `\u2713 wrote ${path} (${checks.length} check${checks.length === 1 ? "" : "s"})`;
192
196
  }
197
+ /** Self-heal detection: replay a saved spec's RECORDED grounded steps against
198
+ * the live app and report the first step that no longer locates — the drift
199
+ * point the agent re-grounds. No `playwright test`, no install; the same
200
+ * grounded replay as creation-verification, seeded from the spec's sidecar. */
201
+ async replaySpec(slug) {
202
+ if (!this.deps.readSpecSteps) return "Spec replay unavailable in this server.";
203
+ const sc = await this.deps.readSpecSteps(slug);
204
+ if (!sc) {
205
+ return `No sidecar for "${slug}" \u2014 only Hover-crystallized specs can be replayed (looked for .hover/sidecars/${slug}.json).`;
206
+ }
207
+ const page = await this.livePage();
208
+ const devUrl = sc.startUrl ?? page.url();
209
+ const res = await replayOnPage(page, devUrl, sc.steps);
210
+ if (res.ok) {
211
+ return `\u2713 "${slug}" still replays clean \u2014 ${res.ran}/${res.total} grounded steps located. No drift to heal.`;
212
+ }
213
+ const f = res.failures[0];
214
+ return JSON.stringify(
215
+ {
216
+ spec: slug,
217
+ drifted: true,
218
+ ranBeforeBreak: res.ran,
219
+ total: res.total,
220
+ brokeAtStep: f.index,
221
+ tool: f.tool,
222
+ lookingFor: f.target,
223
+ error: f.error,
224
+ 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.'
225
+ },
226
+ null,
227
+ 2
228
+ );
229
+ }
193
230
  };
194
231
 
195
232
  // src/mcp/server.ts
@@ -332,6 +369,16 @@ function createHoverMcpServer(c) {
332
369
  },
333
370
  ({ name, description, checks }) => guard(() => c.crystallizeApiSpec(name, description, checks))
334
371
  );
372
+ server2.registerTool(
373
+ "replay_spec",
374
+ {
375
+ description: "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.",
376
+ inputSchema: {
377
+ slug: z.string().describe('The spec slug = its filename without .spec.ts (e.g. "login" for login.spec.ts).')
378
+ }
379
+ },
380
+ ({ slug }) => guard(() => c.replaySpec(slug))
381
+ );
335
382
  server2.registerPrompt(
336
383
  "test_app",
337
384
  {
@@ -343,8 +390,34 @@ function createHoverMcpServer(c) {
343
390
  messages: [{ role: "user", content: { type: "text", text: workflowPrompt(scope) } }]
344
391
  })
345
392
  );
393
+ server2.registerPrompt(
394
+ "heal",
395
+ {
396
+ title: "Hover \u2014 heal a drifted spec",
397
+ 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.",
398
+ argsSchema: { spec: z.string().optional().describe('A spec slug to heal (e.g. "login"). Omit to check every spec.') }
399
+ },
400
+ ({ spec }) => ({
401
+ messages: [{ role: "user", content: { type: "text", text: healPrompt(spec) } }]
402
+ })
403
+ );
346
404
  return server2;
347
405
  }
406
+ function healPrompt(spec) {
407
+ const scope = spec?.trim() ? `the spec \`${spec.trim()}\`` : "every spec under `__vibe_tests__/` (list them first, then heal each that drifted)";
408
+ return `Heal ${scope} for this app using the **Hover MCP tools** \u2014 repair specs whose UI drifted, without rewriting them by hand.
409
+
410
+ A 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.
411
+
412
+ Work ONE spec at a time:
413
+
414
+ 1. **Detect** \u2014 \`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 \u2014 move on.
415
+ 2. **Re-ground the broken step** \u2014 \`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" \u2192 "Log in"). Re-drive from the break with the grounded \`*_control\` tools. Change ONLY what drifted \u2014 don't redesign the flow.
416
+ 3. **Re-crystallize** \u2014 when the flow runs green again, \`crystallize_spec\` with the SAME name as the broken spec to overwrite it with the healed version.
417
+ 4. **Report** \u2014 say which step drifted, what changed (old target \u2192 new target), and that it's re-crystallized. The user reviews the old-vs-new diff in the cockpit before keeping it.
418
+
419
+ Rules: 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 \u2014 say so and ASK whether to drop that step or the whole spec. Stay on the app under test.`;
420
+ }
348
421
  function workflowPrompt(scope) {
349
422
  const target = scope?.trim() ? scope.trim() : "the whole app";
350
423
  return `Build (or extend) a Playwright test suite for this web app using the **Hover MCP tools**.
@@ -360,6 +433,12 @@ tree \u2014 read before acting) \xB7 \`click_control\` / \`fill_control\` / \`se
360
433
 
361
434
  Target: the app at HOVER_TARGET (set in the server's env). Scope: ${target}.
362
435
 
436
+ ## Ground rules (they protect record==replay AND the user's real app)
437
+ - **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.
438
+ - **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.
439
+ - **Assert stable outcomes.** Assert on semantic, durable signals (a success message, a heading, a new row's label) \u2014 NEVER volatile instance data (timestamps, generated ids, "today", a one-off order number), which makes the saved spec flaky on replay.
440
+ - **Log in first.** If the app needs auth, do that before anything else \u2014 ask for credentials if you don't have them \u2014 then crystallize it as its own "Log in" spec and stay logged in for the rest of the run.
441
+
363
442
  Work in PHASES \u2014 this is what lets it scale from a tiny app to a large one.
364
443
 
365
444
  ## Phase 1 \u2014 Map the business lines (read the CODE, don't click around)
@@ -430,7 +509,11 @@ var controller = new HoverMcpController({
430
509
  return { path: res.path };
431
510
  },
432
511
  recordFact: (title, rule, type) => writeFact(DEV_ROOT, { name: title, description: title, type, body: rule }),
433
- recall: async () => formatMemoryForPrompt(await loadMemory(DEV_ROOT))
512
+ recall: async () => formatMemoryForPrompt(await loadMemory(DEV_ROOT)),
513
+ readSpecSteps: async (slug) => {
514
+ const sc = await readSidecar(DEV_ROOT, slug);
515
+ return sc ? { steps: sc.steps, startUrl: TARGET } : null;
516
+ }
434
517
  });
435
518
  var server = createHoverMcpServer(controller);
436
519
  await server.connect(new StdioServerTransport());
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 loadMemory,\n formatMemoryForPrompt,\n type SkillStep,\n type ApiCheck,\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[]) => {\n const res = await writeSpec({ devRoot: DEV_ROOT, name, description, steps, 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: async () => formatMemoryForPrompt(await loadMemory(DEV_ROOT)),\n});\n\nconst server = createHoverMcpServer(controller);\nawait server.connect(new StdioServerTransport());\n","import { request as pwRequest, type Page } from 'playwright-core';\nimport { groundedLocate, type GroundedTarget, type SkillStep, type ApiCheck } 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. */\n crystallize: (name: string, description: string | undefined, steps: SkillStep[]) => 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/ as a prompt block ('' if none). */\n recall?: () => Promise<string>;\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 /** 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 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. */\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 /** 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);\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","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.',\n inputSchema: {},\n },\n () => guard(() => c.recall()),\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 // 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 return server;\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\\`.\n\nTarget: the app at HOVER_TARGET (set in the server's env). Scope: ${target}.\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## 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 { 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 loadMemory,\n formatMemoryForPrompt,\n readSidecar,\n type SkillStep,\n type ApiCheck,\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[]) => {\n const res = await writeSpec({ devRoot: DEV_ROOT, name, description, steps, 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: async () => formatMemoryForPrompt(await loadMemory(DEV_ROOT)),\n readSpecSteps: async (slug: string) => {\n const sc = await readSidecar(DEV_ROOT, slug);\n return sc ? { steps: sc.steps, startUrl: TARGET } : null;\n },\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} 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. */\n crystallize: (name: string, description: string | undefined, steps: SkillStep[]) => 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/ as a prompt block ('' if none). */\n recall?: () => Promise<string>;\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}\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 /** 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 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. */\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 /** 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);\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","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.',\n inputSchema: {},\n },\n () => guard(() => c.recall()),\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 // 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 // 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/** 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\\`.\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## 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.19.0",
3
+ "version": "0.20.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.19.0"
39
+ "@hover-dev/core": "0.20.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^22.0.0",