@xiaou66/vite-plugin-vue-mcp-next 1.2.0 → 1.3.1

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/index.js CHANGED
@@ -51,7 +51,8 @@ var MCP_TOOL_NAMES = {
51
51
  highlightComponent: "highlight_component",
52
52
  getRouterInfo: "get_router_info",
53
53
  getPiniaTree: "get_pinia_tree",
54
- getPiniaState: "get_pinia_state"
54
+ getPiniaState: "get_pinia_state",
55
+ getElementContext: "get_element_context"
55
56
  };
56
57
  var VIRTUAL_RUNTIME_ID = "virtual:vite-plugin-vue-mcp-next/runtime";
57
58
  var RESOLVED_VIRTUAL_RUNTIME_ID = `\0${VIRTUAL_RUNTIME_ID}`;
@@ -62,6 +63,12 @@ var RESOLVED_VIRTUAL_SNAPDOM_LOADER_ID = `\0${VIRTUAL_SNAPDOM_LOADER_ID}`;
62
63
  var DEFAULT_MCP_CLIENT_SERVER_NAME = "vite-mcp-next";
63
64
  var LEGACY_MCP_CLIENT_SERVER_NAMES = ["vue-mcp-next"];
64
65
  var RUNTIME_PAGE_RECONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-reconnected";
66
+ var RUNTIME_PAGE_CONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-connected";
67
+ var RUNTIME_PAGE_DISCONNECTED_EVENT = "vite-plugin-vue-mcp-next:page-disconnected";
68
+ var RUNTIME_PAGE_HEARTBEAT_EVENT = "vite-plugin-vue-mcp-next:heartbeat";
69
+ var DEFAULT_RUNTIME_PAGE_HEARTBEAT_TIMEOUT_MS = 45e3;
70
+ var DEFAULT_RUNTIME_PAGE_HEARTBEAT_SCAN_INTERVAL_MS = 45e3;
71
+ var DEFAULT_ELEMENT_PICKER_TOAST_DURATION_MS = 2200;
65
72
  var DEFAULT_OPTIONS = {
66
73
  mcpPath: DEFAULT_MCP_PATH,
67
74
  host: "localhost",
@@ -80,6 +87,16 @@ var DEFAULT_OPTIONS = {
80
87
  skill: {
81
88
  autoConfig: true
82
89
  },
90
+ elementPicker: {
91
+ enabled: true,
92
+ shortcut: {
93
+ altKey: true,
94
+ shiftKey: true,
95
+ metaKey: false,
96
+ ctrlKey: false
97
+ },
98
+ toastDurationMs: DEFAULT_ELEMENT_PICKER_TOAST_DURATION_MS
99
+ },
83
100
  runtime: {
84
101
  mode: "auto",
85
102
  evaluate: {
@@ -154,6 +171,14 @@ function mergeOptions(options = {}) {
154
171
  ...DEFAULT_OPTIONS.skill,
155
172
  ...options.skill
156
173
  },
174
+ elementPicker: {
175
+ ...DEFAULT_OPTIONS.elementPicker,
176
+ ...options.elementPicker,
177
+ shortcut: {
178
+ ...DEFAULT_OPTIONS.elementPicker.shortcut,
179
+ ...options.elementPicker?.shortcut
180
+ }
181
+ },
157
182
  runtime: {
158
183
  ...DEFAULT_OPTIONS.runtime,
159
184
  ...options.runtime,
@@ -574,9 +599,164 @@ function registerDomTools(server, ctx) {
574
599
  );
575
600
  }
576
601
 
577
- // src/mcp/tools/evaluate.ts
602
+ // src/mcp/tools/elementContext.ts
578
603
  import { z as z3 } from "zod";
579
604
 
605
+ // src/shared/elementId.ts
606
+ var PROJECT_SOURCE_ID_PATTERN = /^(.+\.(?:vue|tsx|jsx|ts|js)):(\d+):(\d+)$/;
607
+ var RUNTIME_ID_PATTERN = /^runtime:([A-Za-z0-9_-]+)$/;
608
+ var PACKAGE_ID_PREFIX = "pkg:";
609
+ function parseElementId(elementId) {
610
+ const sourceMatch = PROJECT_SOURCE_ID_PATTERN.exec(elementId);
611
+ if (sourceMatch) {
612
+ return {
613
+ kind: "project-source",
614
+ elementId,
615
+ file: sourceMatch[1],
616
+ line: Number(sourceMatch[2]),
617
+ column: Number(sourceMatch[3])
618
+ };
619
+ }
620
+ if (elementId.startsWith(PACKAGE_ID_PREFIX)) {
621
+ return parsePackageElementId(elementId);
622
+ }
623
+ const runtimeMatch = RUNTIME_ID_PATTERN.exec(elementId);
624
+ if (runtimeMatch) {
625
+ return {
626
+ kind: "runtime",
627
+ elementId,
628
+ runtimeId: runtimeMatch[1]
629
+ };
630
+ }
631
+ return {
632
+ kind: "invalid",
633
+ elementId,
634
+ reason: "unsupported elementId format"
635
+ };
636
+ }
637
+ function parsePackageElementId(elementId) {
638
+ const value = elementId.slice(PACKAGE_ID_PREFIX.length);
639
+ const parts = value.split("/").filter(Boolean);
640
+ if (parts.length < 2) {
641
+ return {
642
+ kind: "invalid",
643
+ elementId,
644
+ reason: "package elementId must include packageName and entryFile"
645
+ };
646
+ }
647
+ const scoped = parts[0]?.startsWith("@");
648
+ const packageName = scoped ? parts.slice(0, 2).join("/") : parts[0];
649
+ const entryFile = parts.slice(scoped ? 2 : 1).join("/");
650
+ if (!entryFile) {
651
+ return {
652
+ kind: "invalid",
653
+ elementId,
654
+ reason: "package elementId must include entryFile"
655
+ };
656
+ }
657
+ return {
658
+ kind: "package",
659
+ elementId,
660
+ packageName,
661
+ entryFile
662
+ };
663
+ }
664
+
665
+ // src/mcp/tools/elementContext.ts
666
+ function registerElementContextTools(server, ctx) {
667
+ server.registerTool(
668
+ MCP_TOOL_NAMES.getElementContext,
669
+ {
670
+ description: "Get editable source, Vue component, and DOM context for a copied elementId.",
671
+ inputSchema: {
672
+ elementId: z3.string(),
673
+ pageId: z3.string().optional()
674
+ }
675
+ },
676
+ async (input) => {
677
+ const runtimeResult = await tryRuntimeElementContext(
678
+ ctx,
679
+ input.elementId,
680
+ input.pageId
681
+ );
682
+ if (runtimeResult) {
683
+ return createToolResponse(runtimeResult);
684
+ }
685
+ return createToolResponse(createStaticElementContext(input.elementId));
686
+ }
687
+ );
688
+ }
689
+ async function tryRuntimeElementContext(ctx, elementId, pageId) {
690
+ if (!ctx.rpcServer) {
691
+ return parseElementId(elementId).kind === "runtime" ? createRuntimeUnavailableContext(elementId) : void 0;
692
+ }
693
+ try {
694
+ resolvePageTarget(ctx, pageId);
695
+ } catch (error) {
696
+ const parsed = parseElementId(elementId);
697
+ if (parsed.kind === "project-source" || parsed.kind === "package") {
698
+ return void 0;
699
+ }
700
+ return {
701
+ ok: false,
702
+ elementId,
703
+ error: error instanceof Error ? error.message : String(error),
704
+ limitations: ["call list_pages and pass pageId when multiple pages exist"]
705
+ };
706
+ }
707
+ return await requestRuntimeData(ctx, (event) => {
708
+ void ctx.rpcServer?.getElementContext({ event, elementId });
709
+ });
710
+ }
711
+ function createStaticElementContext(elementId) {
712
+ const parsed = parseElementId(elementId);
713
+ if (parsed.kind === "project-source") {
714
+ return {
715
+ ok: true,
716
+ elementId,
717
+ editable: true,
718
+ codeLocation: {
719
+ file: parsed.file,
720
+ line: parsed.line,
721
+ column: parsed.column
722
+ },
723
+ limitations: ["runtime unavailable, DOM and component context omitted"]
724
+ };
725
+ }
726
+ if (parsed.kind === "package") {
727
+ return {
728
+ ok: true,
729
+ elementId,
730
+ editable: false,
731
+ packageLocation: {
732
+ packageName: parsed.packageName,
733
+ entryFile: parsed.entryFile
734
+ },
735
+ limitations: ["third-party package source is not editable from this project"]
736
+ };
737
+ }
738
+ if (parsed.kind === "runtime") {
739
+ return createRuntimeUnavailableContext(elementId);
740
+ }
741
+ return {
742
+ ok: false,
743
+ elementId,
744
+ error: parsed.reason,
745
+ limitations: ["please provide a copied elementId from the element picker"]
746
+ };
747
+ }
748
+ function createRuntimeUnavailableContext(elementId) {
749
+ return {
750
+ ok: false,
751
+ elementId,
752
+ error: "runtime bridge is not connected",
753
+ limitations: ["runtime ids are only valid while the page is connected"]
754
+ };
755
+ }
756
+
757
+ // src/mcp/tools/evaluate.ts
758
+ import { z as z4 } from "zod";
759
+
580
760
  // src/cdp/cdpEvaluate.ts
581
761
  async function cdpEvaluate(options) {
582
762
  const result = await options.client.Runtime.evaluate({
@@ -597,9 +777,9 @@ function registerEvaluateTools(server, ctx) {
597
777
  {
598
778
  description: "Evaluate a script in the selected page when explicitly enabled.",
599
779
  inputSchema: {
600
- pageId: z3.string().optional(),
601
- expression: z3.string(),
602
- awaitPromise: z3.boolean().optional()
780
+ pageId: z4.string().optional(),
781
+ expression: z4.string(),
782
+ awaitPromise: z4.boolean().optional()
603
783
  }
604
784
  },
605
785
  async (input) => {
@@ -648,18 +828,18 @@ function assertEvaluateEnabled(options) {
648
828
  }
649
829
 
650
830
  // src/mcp/tools/network.ts
651
- import { z as z4 } from "zod";
831
+ import { z as z5 } from "zod";
652
832
  function registerNetworkTools(server, ctx) {
653
833
  server.registerTool(
654
834
  MCP_TOOL_NAMES.getNetworkRequests,
655
835
  {
656
836
  description: "Get captured network request summaries.",
657
837
  inputSchema: {
658
- pageId: z4.string().optional(),
659
- urlContains: z4.string().optional(),
660
- method: z4.string().optional(),
661
- status: z4.number().optional(),
662
- limit: z4.number().optional()
838
+ pageId: z5.string().optional(),
839
+ urlContains: z5.string().optional(),
840
+ method: z5.string().optional(),
841
+ status: z5.number().optional(),
842
+ limit: z5.number().optional()
663
843
  }
664
844
  },
665
845
  (input) => {
@@ -682,7 +862,7 @@ function registerNetworkTools(server, ctx) {
682
862
  MCP_TOOL_NAMES.getNetworkRequestDetail,
683
863
  {
684
864
  description: "Get captured network request detail by id.",
685
- inputSchema: { id: z4.string() }
865
+ inputSchema: { id: z5.string() }
686
866
  },
687
867
  (input) => {
688
868
  if (ctx.options.network.mode === "off") {
@@ -699,7 +879,7 @@ function registerNetworkTools(server, ctx) {
699
879
  {
700
880
  description: "Clear cached network requests.",
701
881
  inputSchema: {
702
- pageId: z4.string().optional()
882
+ pageId: z5.string().optional()
703
883
  }
704
884
  },
705
885
  () => {
@@ -710,7 +890,7 @@ function registerNetworkTools(server, ctx) {
710
890
  }
711
891
 
712
892
  // src/mcp/tools/performance.ts
713
- import { z as z5 } from "zod";
893
+ import { z as z6 } from "zod";
714
894
 
715
895
  // src/performance/summary.ts
716
896
  function buildPerformanceSummary(input) {
@@ -992,10 +1172,10 @@ function registerPerformanceTools(server, ctx) {
992
1172
  {
993
1173
  description: "Record a performance sample for the selected page.",
994
1174
  inputSchema: {
995
- pageId: z5.string().optional(),
996
- durationMs: z5.number().optional(),
997
- includeMemory: z5.boolean().optional(),
998
- includeStacks: z5.boolean().optional()
1175
+ pageId: z6.string().optional(),
1176
+ durationMs: z6.number().optional(),
1177
+ includeMemory: z6.boolean().optional(),
1178
+ includeStacks: z6.boolean().optional()
999
1179
  }
1000
1180
  },
1001
1181
  async (input) => handleRecordPerformance(ctx, input)
@@ -1005,9 +1185,9 @@ function registerPerformanceTools(server, ctx) {
1005
1185
  {
1006
1186
  description: "Start a performance recording session.",
1007
1187
  inputSchema: {
1008
- pageId: z5.string().optional(),
1009
- includeMemory: z5.boolean().optional(),
1010
- includeStacks: z5.boolean().optional()
1188
+ pageId: z6.string().optional(),
1189
+ includeMemory: z6.boolean().optional(),
1190
+ includeStacks: z6.boolean().optional()
1011
1191
  }
1012
1192
  },
1013
1193
  async (input) => handleStartPerformanceRecording(ctx, input)
@@ -1017,7 +1197,7 @@ function registerPerformanceTools(server, ctx) {
1017
1197
  {
1018
1198
  description: "Stop a performance recording session.",
1019
1199
  inputSchema: {
1020
- recordingId: z5.string()
1200
+ recordingId: z6.string()
1021
1201
  }
1022
1202
  },
1023
1203
  async (input) => handleStopPerformanceRecording(ctx, input)
@@ -1027,9 +1207,9 @@ function registerPerformanceTools(server, ctx) {
1027
1207
  {
1028
1208
  description: "Get cached performance reports and active sessions.",
1029
1209
  inputSchema: {
1030
- pageId: z5.string().optional(),
1031
- recordingId: z5.string().optional(),
1032
- limit: z5.number().optional()
1210
+ pageId: z6.string().optional(),
1211
+ recordingId: z6.string().optional(),
1212
+ limit: z6.number().optional()
1033
1213
  }
1034
1214
  },
1035
1215
  (input) => handleGetPerformanceReport(ctx, input)
@@ -1039,7 +1219,7 @@ function registerPerformanceTools(server, ctx) {
1039
1219
  {
1040
1220
  description: "Take a heap snapshot with CDP.",
1041
1221
  inputSchema: {
1042
- pageId: z5.string().optional()
1222
+ pageId: z6.string().optional()
1043
1223
  }
1044
1224
  },
1045
1225
  async (input) => handleTakeHeapSnapshot(ctx, input)
@@ -1296,7 +1476,7 @@ function toStructuredRecord(value) {
1296
1476
  }
1297
1477
 
1298
1478
  // src/mcp/tools/pages.ts
1299
- import { z as z6 } from "zod";
1479
+ import { z as z7 } from "zod";
1300
1480
 
1301
1481
  // src/plugin/entryDiscovery.ts
1302
1482
  import fs from "fs";
@@ -1321,10 +1501,10 @@ function walkHtmlEntries(root, dir, entries) {
1321
1501
  if (!item.isFile() || !item.name.endsWith(".html")) {
1322
1502
  continue;
1323
1503
  }
1324
- const relative2 = normalizePath(path.relative(root, fullPath));
1504
+ const relative3 = normalizePath(path.relative(root, fullPath));
1325
1505
  entries.push({
1326
- file: relative2,
1327
- pathname: relative2 === "index.html" ? "/" : `/${relative2}`
1506
+ file: relative3,
1507
+ pathname: relative3 === "index.html" ? "/" : `/${relative3}`
1328
1508
  });
1329
1509
  }
1330
1510
  }
@@ -1336,7 +1516,7 @@ function registerPageTools(server, ctx, vite) {
1336
1516
  {
1337
1517
  description: "List Vite page entries and connected runtime/CDP targets.",
1338
1518
  inputSchema: {
1339
- includeDisconnected: z6.boolean().optional()
1519
+ includeDisconnected: z7.boolean().optional()
1340
1520
  }
1341
1521
  },
1342
1522
  async (input) => {
@@ -1359,8 +1539,8 @@ function registerPageTools(server, ctx, vite) {
1359
1539
  {
1360
1540
  description: "Reload the selected page. CDP uses ignoreCache; Runtime Hook falls back to normal reload.",
1361
1541
  inputSchema: {
1362
- pageId: z6.string().optional(),
1363
- ignoreCache: z6.boolean().optional()
1542
+ pageId: z7.string().optional(),
1543
+ ignoreCache: z7.boolean().optional()
1364
1544
  }
1365
1545
  },
1366
1546
  async (input) => {
@@ -1512,7 +1692,7 @@ function getPathname(url) {
1512
1692
  }
1513
1693
 
1514
1694
  // src/mcp/tools/storage.ts
1515
- import { z as z7 } from "zod";
1695
+ import { z as z8 } from "zod";
1516
1696
 
1517
1697
  // src/cdp/cdpStorage.ts
1518
1698
  function createCdpStorageAdapter(client) {
@@ -1756,7 +1936,7 @@ function registerStorageTools(server, ctx) {
1756
1936
  registerStorageTool(server, MCP_TOOL_NAMES.listStorage, {
1757
1937
  description: "List same-origin storage and CDP cookies when available.",
1758
1938
  inputSchema: {
1759
- pageId: z7.string().optional()
1939
+ pageId: z8.string().optional()
1760
1940
  },
1761
1941
  action: "list",
1762
1942
  handler: (input) => handleListStorage(ctx, input.pageId)
@@ -1800,23 +1980,23 @@ function registerStorageTool(server, name, options) {
1800
1980
  }
1801
1981
  function createStorageInputSchema() {
1802
1982
  return {
1803
- pageId: z7.string().optional(),
1804
- scope: z7.enum(["localStorage", "sessionStorage", "indexedDB", "cookie"]).optional(),
1805
- key: z7.string().optional(),
1806
- value: z7.string().optional(),
1807
- databaseName: z7.string().optional(),
1808
- objectStoreName: z7.string().optional(),
1809
- indexName: z7.string().optional(),
1810
- cookie: z7.object({
1811
- name: z7.string(),
1812
- value: z7.string().optional(),
1813
- domain: z7.string().optional(),
1814
- path: z7.string().optional(),
1815
- url: z7.string().optional(),
1816
- httpOnly: z7.boolean().optional(),
1817
- secure: z7.boolean().optional(),
1818
- sameSite: z7.enum(["strict", "lax", "none"]).optional(),
1819
- expires: z7.number().optional()
1983
+ pageId: z8.string().optional(),
1984
+ scope: z8.enum(["localStorage", "sessionStorage", "indexedDB", "cookie"]).optional(),
1985
+ key: z8.string().optional(),
1986
+ value: z8.string().optional(),
1987
+ databaseName: z8.string().optional(),
1988
+ objectStoreName: z8.string().optional(),
1989
+ indexName: z8.string().optional(),
1990
+ cookie: z8.object({
1991
+ name: z8.string(),
1992
+ value: z8.string().optional(),
1993
+ domain: z8.string().optional(),
1994
+ path: z8.string().optional(),
1995
+ url: z8.string().optional(),
1996
+ httpOnly: z8.boolean().optional(),
1997
+ secure: z8.boolean().optional(),
1998
+ sameSite: z8.enum(["strict", "lax", "none"]).optional(),
1999
+ expires: z8.number().optional()
1820
2000
  }).optional()
1821
2001
  };
1822
2002
  }
@@ -1934,7 +2114,7 @@ function hasCdpConfig2(ctx) {
1934
2114
  }
1935
2115
 
1936
2116
  // src/mcp/tools/screenshot.ts
1937
- import { z as z8 } from "zod";
2117
+ import { z as z9 } from "zod";
1938
2118
 
1939
2119
  // src/cdp/cdpScreenshot.ts
1940
2120
  async function cdpCaptureScreenshot(options) {
@@ -2094,14 +2274,14 @@ function createProjectRelativePath(ctx, filePath) {
2094
2274
  var DEFAULT_SCREENSHOT_TARGET = "viewport";
2095
2275
  var DEFAULT_SCREENSHOT_FORMAT = "png";
2096
2276
  var screenshotInputSchema = {
2097
- pageId: z8.string().optional(),
2098
- target: z8.enum(["viewport", "fullPage", "element"]).optional(),
2099
- selector: z8.string().optional(),
2100
- format: z8.enum(["png", "jpeg", "webp"]).optional(),
2101
- prefer: z8.enum(["auto", "cdp", "runtime"]).optional(),
2102
- quality: z8.number().optional(),
2103
- scale: z8.number().optional(),
2104
- snapdom: z8.record(z8.string(), z8.unknown()).optional()
2277
+ pageId: z9.string().optional(),
2278
+ target: z9.enum(["viewport", "fullPage", "element"]).optional(),
2279
+ selector: z9.string().optional(),
2280
+ format: z9.enum(["png", "jpeg", "webp"]).optional(),
2281
+ prefer: z9.enum(["auto", "cdp", "runtime"]).optional(),
2282
+ quality: z9.number().optional(),
2283
+ scale: z9.number().optional(),
2284
+ snapdom: z9.record(z9.string(), z9.unknown()).optional()
2105
2285
  };
2106
2286
  function registerScreenshotTools(server, ctx) {
2107
2287
  server.registerTool(
@@ -2221,7 +2401,7 @@ function isScreenshotImagePayload(result) {
2221
2401
 
2222
2402
  // src/mcp/tools/vue.ts
2223
2403
  import { nanoid as nanoid3 } from "nanoid";
2224
- import { z as z9 } from "zod";
2404
+ import { z as z10 } from "zod";
2225
2405
  function registerVueTools(server, ctx) {
2226
2406
  server.registerTool(
2227
2407
  MCP_TOOL_NAMES.getComponentTree,
@@ -2234,7 +2414,7 @@ function registerVueTools(server, ctx) {
2234
2414
  MCP_TOOL_NAMES.getComponentState,
2235
2415
  {
2236
2416
  description: "Get Vue component state.",
2237
- inputSchema: { componentName: z9.string() }
2417
+ inputSchema: { componentName: z10.string() }
2238
2418
  },
2239
2419
  async ({ componentName }) => requestVueData(ctx, (event) => {
2240
2420
  void ctx.rpcServer?.getInspectorState({ event, componentName });
@@ -2245,10 +2425,10 @@ function registerVueTools(server, ctx) {
2245
2425
  {
2246
2426
  description: "Edit Vue component state.",
2247
2427
  inputSchema: {
2248
- componentName: z9.string(),
2249
- path: z9.array(z9.string()),
2250
- value: z9.string(),
2251
- valueType: z9.enum(["string", "number", "boolean", "object", "array"])
2428
+ componentName: z10.string(),
2429
+ path: z10.array(z10.string()),
2430
+ value: z10.string(),
2431
+ valueType: z10.enum(["string", "number", "boolean", "object", "array"])
2252
2432
  }
2253
2433
  },
2254
2434
  ({ componentName, path: path8, value, valueType }) => {
@@ -2268,7 +2448,7 @@ function registerVueTools(server, ctx) {
2268
2448
  MCP_TOOL_NAMES.highlightComponent,
2269
2449
  {
2270
2450
  description: "Highlight a Vue component.",
2271
- inputSchema: { componentName: z9.string() }
2451
+ inputSchema: { componentName: z10.string() }
2272
2452
  },
2273
2453
  ({ componentName }) => {
2274
2454
  if (!ctx.rpcServer) {
@@ -2296,7 +2476,7 @@ function registerVueTools(server, ctx) {
2296
2476
  MCP_TOOL_NAMES.getPiniaState,
2297
2477
  {
2298
2478
  description: "Get Pinia store state.",
2299
- inputSchema: { storeName: z9.string() }
2479
+ inputSchema: { storeName: z10.string() }
2300
2480
  },
2301
2481
  async ({ storeName }) => requestVueData(ctx, (event) => {
2302
2482
  void ctx.rpcServer?.getPiniaState({ event, storeName });
@@ -2339,6 +2519,7 @@ function createMcpServer(ctx, vite) {
2339
2519
  });
2340
2520
  registerPageTools(server, ctx, vite);
2341
2521
  registerDomTools(server, ctx);
2522
+ registerElementContextTools(server, ctx);
2342
2523
  registerScreenshotTools(server, ctx);
2343
2524
  registerConsoleTools(server, ctx);
2344
2525
  registerEvaluateTools(server, ctx);
@@ -2420,6 +2601,10 @@ function createServerVueRuntimeRpc(ctx) {
2420
2601
  onDomQueryUpdated: (event, data) => {
2421
2602
  void ctx.hooks.callHook(event, data);
2422
2603
  },
2604
+ getElementContext: () => void 0,
2605
+ onElementContextUpdated: (event, data) => {
2606
+ void ctx.hooks.callHook(event, data);
2607
+ },
2423
2608
  reloadPage: () => void 0,
2424
2609
  onPageReloaded: (event, data) => {
2425
2610
  void ctx.hooks.callHook(event, data);
@@ -2732,6 +2917,82 @@ async function startCdpObservers(ctx, target, client) {
2732
2917
  }
2733
2918
  }
2734
2919
 
2920
+ // src/plugin/elementInstrumentation.ts
2921
+ import { parse } from "@vue/compiler-sfc";
2922
+ import MagicString from "magic-string";
2923
+ import { relative as relative2 } from "path";
2924
+ var VUE_FILE_SUFFIX = ".vue";
2925
+ var ELEMENT_NODE_TYPE = 1;
2926
+ var SKIPPED_TAGS = /* @__PURE__ */ new Set(["template", "slot", "script", "style"]);
2927
+ var MCP_ID_ATTR = "data-v-mcp-id";
2928
+ function createElementInstrumentationController(options) {
2929
+ return {
2930
+ transform(code, id, ssr) {
2931
+ if (ssr || shouldSkipInstrumentation(id)) {
2932
+ return void 0;
2933
+ }
2934
+ const filename = id.split("?", 1)[0];
2935
+ if (!filename.endsWith(VUE_FILE_SUFFIX)) {
2936
+ return void 0;
2937
+ }
2938
+ const parsed = parse(code, { filename });
2939
+ const template = parsed.descriptor.template;
2940
+ if (!template?.ast) {
2941
+ return void 0;
2942
+ }
2943
+ const s = new MagicString(code);
2944
+ const relativeFile = normalizePath2(relative2(options.root, filename));
2945
+ for (const node of template.ast.children) {
2946
+ injectNodeId(s, node, relativeFile);
2947
+ }
2948
+ if (!s.hasChanged()) {
2949
+ return void 0;
2950
+ }
2951
+ return {
2952
+ code: s.toString(),
2953
+ map: s.generateMap({ hires: true })
2954
+ };
2955
+ }
2956
+ };
2957
+ }
2958
+ function shouldSkipInstrumentation(id) {
2959
+ if (id.startsWith("\0")) {
2960
+ return true;
2961
+ }
2962
+ const normalized = normalizePath2(id);
2963
+ if (normalized.includes("/node_modules/")) {
2964
+ return true;
2965
+ }
2966
+ return !normalized.startsWith("/") && !/^[A-Za-z]:\//.test(normalized);
2967
+ }
2968
+ function normalizePath2(path8) {
2969
+ return path8.replace(/\\/g, "/");
2970
+ }
2971
+ function injectNodeId(s, node, relativeFile) {
2972
+ if (node.type !== ELEMENT_NODE_TYPE || !node.tag || !node.loc) {
2973
+ return;
2974
+ }
2975
+ if (!SKIPPED_TAGS.has(node.tag) && !hasMcpIdAttr(node)) {
2976
+ const id = `${relativeFile}:${String(node.loc.start.line)}:${String(node.loc.start.column)}`;
2977
+ const insertAt = node.loc.start.offset + node.tag.length + 1;
2978
+ s.appendLeft(insertAt, ` ${MCP_ID_ATTR}="${id}"`);
2979
+ }
2980
+ for (const child of node.children ?? []) {
2981
+ injectNodeId(s, child, relativeFile);
2982
+ }
2983
+ }
2984
+ function hasMcpIdAttr(node) {
2985
+ return (node.props ?? []).some((prop) => {
2986
+ if (!isTemplateProp(prop)) {
2987
+ return false;
2988
+ }
2989
+ return prop.name === MCP_ID_ATTR;
2990
+ });
2991
+ }
2992
+ function isTemplateProp(value) {
2993
+ return Boolean(value && typeof value === "object" && "name" in value);
2994
+ }
2995
+
2735
2996
  // src/plugin/injectRuntime.ts
2736
2997
  import { createRequire } from "module";
2737
2998
  import { join } from "path";
@@ -2751,7 +3012,7 @@ function createRuntimeInjectionController(options, getConfig) {
2751
3012
  },
2752
3013
  load(id) {
2753
3014
  if (id === RESOLVED_VIRTUAL_RUNTIME_ID) {
2754
- return createRuntimeModule();
3015
+ return createRuntimeModule(options, getConfig()?.root);
2755
3016
  }
2756
3017
  if (id === RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID) {
2757
3018
  return createScreenshotConfigModule(options);
@@ -2794,14 +3055,17 @@ ${code}`;
2794
3055
  }
2795
3056
  };
2796
3057
  }
2797
- function createRuntimeModule() {
3058
+ function createRuntimeModule(options, root) {
2798
3059
  return [
2799
3060
  "import { setScreenshotModuleRegistry, setSnapdomLoader, startRuntimeClient } from '@xiaou66/vite-plugin-vue-mcp-next/runtime/client';",
2800
3061
  `import { screenshotModuleRegistry } from '${VIRTUAL_SCREENSHOT_CONFIG_ID}';`,
2801
3062
  `import { loadSnapdom } from '${VIRTUAL_SNAPDOM_LOADER_ID}';`,
2802
3063
  "setScreenshotModuleRegistry(screenshotModuleRegistry);",
2803
3064
  "setSnapdomLoader(loadSnapdom);",
2804
- "void startRuntimeClient();"
3065
+ `void startRuntimeClient(${JSON.stringify({
3066
+ elementPicker: options.elementPicker,
3067
+ projectRoot: root
3068
+ })});`
2805
3069
  ].join("\n");
2806
3070
  }
2807
3071
  function createSnapdomLoaderModule(root) {
@@ -3320,6 +3584,7 @@ function vueMcpNext(userOptions = {}) {
3320
3584
  options,
3321
3585
  () => config
3322
3586
  );
3587
+ let elementInstrumentation;
3323
3588
  const cdpLifecycle = createCdpLifecycleController(ctx);
3324
3589
  ctx.cdpLifecycle = cdpLifecycle;
3325
3590
  return {
@@ -3328,6 +3593,9 @@ function vueMcpNext(userOptions = {}) {
3328
3593
  apply: "serve",
3329
3594
  configResolved(resolvedConfig) {
3330
3595
  config = resolvedConfig;
3596
+ elementInstrumentation = createElementInstrumentationController({
3597
+ root: resolvedConfig.root
3598
+ });
3331
3599
  },
3332
3600
  async configureServer(server) {
3333
3601
  ctx.server = server;
@@ -3344,32 +3612,53 @@ function vueMcpNext(userOptions = {}) {
3344
3612
  () => createMcpServer(ctx, server),
3345
3613
  server
3346
3614
  );
3347
- server.ws.on(
3348
- "vite-plugin-vue-mcp-next:page-connected",
3349
- (payload) => {
3350
- if (isRuntimePageTarget(payload)) {
3351
- ctx.pages.upsert(payload);
3352
- void ctx.hooks.callHook(RUNTIME_PAGE_RECONNECTED_EVENT, payload);
3353
- void cdpLifecycle.connectPage(payload);
3615
+ const lastSeenAt = /* @__PURE__ */ new Map();
3616
+ const heartbeatTimer = setInterval(() => {
3617
+ const now = Date.now();
3618
+ for (const [pageId, seenAt] of lastSeenAt) {
3619
+ const target = ctx.pages.get(pageId);
3620
+ if (!target || target.source !== "runtime" || !target.connected) {
3621
+ lastSeenAt.delete(pageId);
3622
+ continue;
3354
3623
  }
3355
- }
3356
- );
3357
- server.ws.on(
3358
- "vite-plugin-vue-mcp-next:console-record",
3359
- (payload) => {
3360
- if (isConsoleRecord(payload)) {
3361
- ctx.consoleRecords.push(payload);
3624
+ if (now - seenAt >= DEFAULT_RUNTIME_PAGE_HEARTBEAT_TIMEOUT_MS) {
3625
+ ctx.pages.disconnect(pageId, now);
3626
+ lastSeenAt.delete(pageId);
3362
3627
  }
3363
3628
  }
3364
- );
3365
- server.ws.on(
3366
- "vite-plugin-vue-mcp-next:network-record",
3367
- (payload) => {
3368
- if (isNetworkRecord(payload)) {
3369
- ctx.networkRecords.push(payload);
3629
+ }, DEFAULT_RUNTIME_PAGE_HEARTBEAT_SCAN_INTERVAL_MS);
3630
+ server.ws.on(RUNTIME_PAGE_CONNECTED_EVENT, (payload) => {
3631
+ if (isRuntimePageTarget(payload)) {
3632
+ ctx.pages.upsert(payload);
3633
+ lastSeenAt.set(payload.pageId, Date.now());
3634
+ void ctx.hooks.callHook(RUNTIME_PAGE_RECONNECTED_EVENT, payload);
3635
+ void cdpLifecycle.connectPage(payload);
3636
+ }
3637
+ });
3638
+ server.ws.on(RUNTIME_PAGE_HEARTBEAT_EVENT, (payload) => {
3639
+ if (isRuntimeHeartbeatTarget(payload)) {
3640
+ const target = ctx.pages.get(payload.pageId);
3641
+ if (target?.source === "runtime" && target.connected) {
3642
+ lastSeenAt.set(payload.pageId, payload.timestamp);
3370
3643
  }
3371
3644
  }
3372
- );
3645
+ });
3646
+ server.ws.on(RUNTIME_PAGE_DISCONNECTED_EVENT, (payload) => {
3647
+ if (isRuntimeDisconnectTarget(payload)) {
3648
+ ctx.pages.disconnect(payload.pageId);
3649
+ lastSeenAt.delete(payload.pageId);
3650
+ }
3651
+ });
3652
+ server.ws.on("vite-plugin-vue-mcp-next:console-record", (payload) => {
3653
+ if (isConsoleRecord(payload)) {
3654
+ ctx.consoleRecords.push(payload);
3655
+ }
3656
+ });
3657
+ server.ws.on("vite-plugin-vue-mcp-next:network-record", (payload) => {
3658
+ if (isNetworkRecord(payload)) {
3659
+ ctx.networkRecords.push(payload);
3660
+ }
3661
+ });
3373
3662
  server.ws.on(
3374
3663
  "vite-plugin-vue-mcp-next:performance-record",
3375
3664
  (payload) => {
@@ -3399,6 +3688,7 @@ function vueMcpNext(userOptions = {}) {
3399
3688
  }, 300);
3400
3689
  }
3401
3690
  server.httpServer?.once("close", () => {
3691
+ clearInterval(heartbeatTimer);
3402
3692
  void cdpLifecycle.closeAll();
3403
3693
  });
3404
3694
  },
@@ -3409,7 +3699,21 @@ function vueMcpNext(userOptions = {}) {
3409
3699
  return runtimeInjection.load(id);
3410
3700
  },
3411
3701
  transform(code, id, transformOptions) {
3412
- return runtimeInjection.transform(code, id, transformOptions?.ssr);
3702
+ const instrumented = elementInstrumentation?.transform(
3703
+ code,
3704
+ id,
3705
+ transformOptions?.ssr
3706
+ );
3707
+ const nextCode = instrumented && typeof instrumented === "object" && "code" in instrumented && typeof instrumented.code === "string" ? instrumented.code : code;
3708
+ const runtimeInjected = runtimeInjection.transform(
3709
+ nextCode,
3710
+ id,
3711
+ transformOptions?.ssr
3712
+ );
3713
+ if (runtimeInjected) {
3714
+ return runtimeInjected;
3715
+ }
3716
+ return instrumented;
3413
3717
  },
3414
3718
  transformIndexHtml(html) {
3415
3719
  return runtimeInjection.transformIndexHtml(html);
@@ -3423,6 +3727,20 @@ function isRuntimePageTarget(payload) {
3423
3727
  const target = payload;
3424
3728
  return target.source === "runtime" && typeof target.pageId === "string" && typeof target.url === "string" && typeof target.pathname === "string" && typeof target.connected === "boolean" && (target.runtimeClientId === void 0 || typeof target.runtimeClientId === "string");
3425
3729
  }
3730
+ function isRuntimeHeartbeatTarget(payload) {
3731
+ if (!payload || typeof payload !== "object") {
3732
+ return false;
3733
+ }
3734
+ const target = payload;
3735
+ return typeof target.pageId === "string" && typeof target.timestamp === "number";
3736
+ }
3737
+ function isRuntimeDisconnectTarget(payload) {
3738
+ if (!payload || typeof payload !== "object") {
3739
+ return false;
3740
+ }
3741
+ const target = payload;
3742
+ return typeof target.pageId === "string";
3743
+ }
3426
3744
  function isConsoleRecord(payload) {
3427
3745
  if (!payload || typeof payload !== "object") {
3428
3746
  return false;