@xiaou66/vite-plugin-vue-mcp-next 0.0.3 → 0.0.6

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
@@ -16,11 +16,13 @@ var DEFAULT_MASK_HEADERS = [
16
16
 
17
17
  // src/constants.ts
18
18
  var DEFAULT_MCP_PATH = "/__mcp";
19
+ var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
19
20
  var MCP_TOOL_NAMES = {
20
21
  listPages: "list_pages",
21
22
  getPageState: "get_page_state",
22
23
  getDomTree: "get_dom_tree",
23
24
  queryDom: "query_dom",
25
+ takeScreenshot: "take_screenshot",
24
26
  getConsoleLogs: "get_console_logs",
25
27
  clearConsoleLogs: "clear_console_logs",
26
28
  evaluateScript: "evaluate_script",
@@ -37,6 +39,8 @@ var MCP_TOOL_NAMES = {
37
39
  };
38
40
  var VIRTUAL_RUNTIME_ID = "virtual:vite-plugin-vue-mcp-next/runtime";
39
41
  var RESOLVED_VIRTUAL_RUNTIME_ID = `\0${VIRTUAL_RUNTIME_ID}`;
42
+ var VIRTUAL_SCREENSHOT_CONFIG_ID = "virtual:vite-plugin-vue-mcp-next/screenshot-config";
43
+ var RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID = `\0${VIRTUAL_SCREENSHOT_CONFIG_ID}`;
40
44
  var DEFAULT_MCP_CLIENT_SERVER_NAME = "vue-mcp-next";
41
45
  var DEFAULT_OPTIONS = {
42
46
  mcpPath: DEFAULT_MCP_PATH,
@@ -76,6 +80,14 @@ var DEFAULT_OPTIONS = {
76
80
  },
77
81
  console: {
78
82
  maxRecords: DEFAULT_CONSOLE_MAX_RECORDS
83
+ },
84
+ screenshot: {
85
+ prefer: "auto",
86
+ maxBytes: DEFAULT_SCREENSHOT_MAX_BYTES,
87
+ snapdom: {
88
+ options: {},
89
+ plugins: []
90
+ }
79
91
  }
80
92
  };
81
93
  function mergeMcpClientOptions(cursorConfig, mcpClients) {
@@ -126,6 +138,21 @@ function mergeOptions(options = {}) {
126
138
  console: {
127
139
  ...DEFAULT_OPTIONS.console,
128
140
  ...options.console
141
+ },
142
+ screenshot: {
143
+ ...DEFAULT_OPTIONS.screenshot,
144
+ ...options.screenshot,
145
+ snapdom: {
146
+ ...DEFAULT_OPTIONS.screenshot.snapdom,
147
+ ...options.screenshot?.snapdom,
148
+ options: {
149
+ ...DEFAULT_OPTIONS.screenshot.snapdom.options,
150
+ ...options.screenshot?.snapdom?.options
151
+ },
152
+ plugins: options.screenshot?.snapdom?.plugins ?? [
153
+ ...DEFAULT_OPTIONS.screenshot.snapdom.plugins
154
+ ]
155
+ }
129
156
  }
130
157
  };
131
158
  }
@@ -680,9 +707,225 @@ function getPathname(url) {
680
707
  }
681
708
  }
682
709
 
710
+ // src/mcp/tools/screenshot.ts
711
+ import { z as z5 } from "zod";
712
+
713
+ // src/cdp/cdpScreenshot.ts
714
+ async function cdpCaptureScreenshot(options) {
715
+ if (options.target === "element") {
716
+ return captureElementScreenshot(options);
717
+ }
718
+ if (options.target === "fullPage") {
719
+ return captureFullPageScreenshot(options);
720
+ }
721
+ return captureViewportScreenshot(options);
722
+ }
723
+ async function captureViewportScreenshot(options) {
724
+ const metrics = await options.client.Page.getLayoutMetrics();
725
+ const width = Math.ceil(metrics.cssLayoutViewport.clientWidth);
726
+ const height = Math.ceil(metrics.cssLayoutViewport.clientHeight);
727
+ const result = await options.client.Page.captureScreenshot(
728
+ omitUndefined({
729
+ format: options.format,
730
+ quality: createQuality(options),
731
+ captureBeyondViewport: false
732
+ })
733
+ );
734
+ return { data: result.data, width, height };
735
+ }
736
+ async function captureFullPageScreenshot(options) {
737
+ const metrics = await options.client.Page.getLayoutMetrics();
738
+ const width = Math.ceil(metrics.cssContentSize.width);
739
+ const height = Math.ceil(metrics.cssContentSize.height);
740
+ const result = await options.client.Page.captureScreenshot(
741
+ omitUndefined({
742
+ format: options.format,
743
+ quality: createQuality(options),
744
+ captureBeyondViewport: true,
745
+ clip: { x: 0, y: 0, width, height, scale: 1 }
746
+ })
747
+ );
748
+ return { data: result.data, width, height };
749
+ }
750
+ async function captureElementScreenshot(options) {
751
+ if (!options.selector) {
752
+ throw new Error("selector is required when target is element");
753
+ }
754
+ const rect = await getElementRect(options.client, options.selector);
755
+ const result = await options.client.Page.captureScreenshot(
756
+ omitUndefined({
757
+ format: options.format,
758
+ quality: createQuality(options),
759
+ captureBeyondViewport: true,
760
+ clip: {
761
+ x: rect.x,
762
+ y: rect.y,
763
+ width: rect.width,
764
+ height: rect.height,
765
+ scale: 1
766
+ }
767
+ })
768
+ );
769
+ return { data: result.data, width: rect.width, height: rect.height };
770
+ }
771
+ async function getElementRect(client, selector) {
772
+ const result = await client.Runtime.evaluate({
773
+ expression: createElementRectExpression(selector),
774
+ awaitPromise: true,
775
+ returnByValue: true
776
+ });
777
+ if (result.exceptionDetails) {
778
+ throw new Error(result.exceptionDetails.text || "element query failed");
779
+ }
780
+ const value = result.result.value;
781
+ if (!isElementRect(value)) {
782
+ throw new Error(`element not found: ${selector}`);
783
+ }
784
+ return value;
785
+ }
786
+ function createElementRectExpression(selector) {
787
+ return `(() => {
788
+ const el = document.querySelector(${JSON.stringify(selector)});
789
+ if (!el) return null;
790
+ const rect = el.getBoundingClientRect();
791
+ return {
792
+ x: rect.x + window.scrollX,
793
+ y: rect.y + window.scrollY,
794
+ width: rect.width,
795
+ height: rect.height
796
+ };
797
+ })()`;
798
+ }
799
+ function createQuality(options) {
800
+ return options.format === "png" ? void 0 : options.quality;
801
+ }
802
+ function omitUndefined(value) {
803
+ return Object.fromEntries(
804
+ Object.entries(value).filter(([, item]) => item !== void 0)
805
+ );
806
+ }
807
+ function isElementRect(value) {
808
+ if (!value || typeof value !== "object") {
809
+ return false;
810
+ }
811
+ const rect = value;
812
+ return typeof rect.x === "number" && typeof rect.y === "number" && typeof rect.width === "number" && rect.width > 0 && typeof rect.height === "number" && rect.height > 0;
813
+ }
814
+
815
+ // src/mcp/tools/screenshot.ts
816
+ var DEFAULT_SCREENSHOT_TARGET = "viewport";
817
+ var DEFAULT_SCREENSHOT_FORMAT = "png";
818
+ var screenshotInputSchema = {
819
+ pageId: z5.string().optional(),
820
+ target: z5.enum(["viewport", "fullPage", "element"]).optional(),
821
+ selector: z5.string().optional(),
822
+ format: z5.enum(["png", "jpeg", "webp"]).optional(),
823
+ prefer: z5.enum(["auto", "cdp", "runtime"]).optional(),
824
+ quality: z5.number().optional(),
825
+ scale: z5.number().optional(),
826
+ snapdom: z5.record(z5.string(), z5.unknown()).optional()
827
+ };
828
+ function registerScreenshotTools(server, ctx) {
829
+ server.registerTool(
830
+ MCP_TOOL_NAMES.takeScreenshot,
831
+ {
832
+ description: "Take a page screenshot using CDP or snapdom fallback.",
833
+ inputSchema: screenshotInputSchema
834
+ },
835
+ async (input) => handleTakeScreenshot(ctx, input)
836
+ );
837
+ }
838
+ async function handleTakeScreenshot(ctx, input) {
839
+ const target = input.target ?? DEFAULT_SCREENSHOT_TARGET;
840
+ const format = input.format ?? DEFAULT_SCREENSHOT_FORMAT;
841
+ const prefer = input.prefer ?? ctx.options.screenshot.prefer;
842
+ if (target === "element" && !input.selector) {
843
+ return createToolError("selector is required when target is element");
844
+ }
845
+ if (prefer !== "runtime") {
846
+ const cdp = await connectCdpForPage(ctx, input.pageId);
847
+ if (cdp) {
848
+ try {
849
+ const screenshot = await cdpCaptureScreenshot({
850
+ client: cdp.client,
851
+ target,
852
+ selector: input.selector,
853
+ format,
854
+ quality: input.quality
855
+ });
856
+ return createScreenshotResponse(ctx, {
857
+ source: "cdp",
858
+ target,
859
+ format,
860
+ data: screenshot.data,
861
+ width: screenshot.width,
862
+ height: screenshot.height,
863
+ mimeType: createMimeType(format),
864
+ byteLength: getBase64ByteLength(screenshot.data)
865
+ });
866
+ } finally {
867
+ await closeCdpClient(cdp.client);
868
+ }
869
+ }
870
+ if (prefer === "cdp") {
871
+ return createToolError("CDP screenshot is unavailable");
872
+ }
873
+ }
874
+ return createRuntimeScreenshot(ctx, input, { target, format });
875
+ }
876
+ async function createRuntimeScreenshot(ctx, input, normalized) {
877
+ const result = await requestRuntimeData(ctx, (event) => {
878
+ void ctx.rpcServer?.takeScreenshot({
879
+ event,
880
+ target: normalized.target,
881
+ selector: input.selector,
882
+ format: normalized.format,
883
+ quality: input.quality,
884
+ scale: input.scale,
885
+ snapdom: {
886
+ ...ctx.options.screenshot.snapdom,
887
+ options: {
888
+ ...ctx.options.screenshot.snapdom.options,
889
+ ...input.snapdom
890
+ }
891
+ }
892
+ });
893
+ });
894
+ if (isScreenshotTooLarge(ctx, result)) {
895
+ return createToolError(
896
+ `screenshot is too large: ${String(result.byteLength)} bytes`
897
+ );
898
+ }
899
+ if (!isPlainRecord(result)) {
900
+ return createToolError("runtime screenshot returned an invalid response");
901
+ }
902
+ return createToolResponse(result);
903
+ }
904
+ function createScreenshotResponse(ctx, result) {
905
+ if (result.byteLength > ctx.options.screenshot.maxBytes) {
906
+ return createToolError(
907
+ `screenshot is too large: ${String(result.byteLength)} bytes`
908
+ );
909
+ }
910
+ return createToolResponse(result);
911
+ }
912
+ function isScreenshotTooLarge(ctx, result) {
913
+ return isPlainRecord(result) && "byteLength" in result && typeof result.byteLength === "number" && result.byteLength > ctx.options.screenshot.maxBytes;
914
+ }
915
+ function isPlainRecord(value) {
916
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
917
+ }
918
+ function createMimeType(format) {
919
+ return `image/${format}`;
920
+ }
921
+ function getBase64ByteLength(data) {
922
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
923
+ return Math.ceil(data.length * 3 / 4) - padding;
924
+ }
925
+
683
926
  // src/mcp/tools/vue.ts
684
927
  import { nanoid as nanoid2 } from "nanoid";
685
- import { z as z5 } from "zod";
928
+ import { z as z6 } from "zod";
686
929
  function registerVueTools(server, ctx) {
687
930
  server.registerTool(
688
931
  MCP_TOOL_NAMES.getComponentTree,
@@ -695,7 +938,7 @@ function registerVueTools(server, ctx) {
695
938
  MCP_TOOL_NAMES.getComponentState,
696
939
  {
697
940
  description: "Get Vue component state.",
698
- inputSchema: { componentName: z5.string() }
941
+ inputSchema: { componentName: z6.string() }
699
942
  },
700
943
  async ({ componentName }) => requestVueData(ctx, (event) => {
701
944
  void ctx.rpcServer?.getInspectorState({ event, componentName });
@@ -706,10 +949,10 @@ function registerVueTools(server, ctx) {
706
949
  {
707
950
  description: "Edit Vue component state.",
708
951
  inputSchema: {
709
- componentName: z5.string(),
710
- path: z5.array(z5.string()),
711
- value: z5.string(),
712
- valueType: z5.enum(["string", "number", "boolean", "object", "array"])
952
+ componentName: z6.string(),
953
+ path: z6.array(z6.string()),
954
+ value: z6.string(),
955
+ valueType: z6.enum(["string", "number", "boolean", "object", "array"])
713
956
  }
714
957
  },
715
958
  ({ componentName, path: path5, value, valueType }) => {
@@ -729,7 +972,7 @@ function registerVueTools(server, ctx) {
729
972
  MCP_TOOL_NAMES.highlightComponent,
730
973
  {
731
974
  description: "Highlight a Vue component.",
732
- inputSchema: { componentName: z5.string() }
975
+ inputSchema: { componentName: z6.string() }
733
976
  },
734
977
  ({ componentName }) => {
735
978
  if (!ctx.rpcServer) {
@@ -757,7 +1000,7 @@ function registerVueTools(server, ctx) {
757
1000
  MCP_TOOL_NAMES.getPiniaState,
758
1001
  {
759
1002
  description: "Get Pinia store state.",
760
- inputSchema: { storeName: z5.string() }
1003
+ inputSchema: { storeName: z6.string() }
761
1004
  },
762
1005
  async ({ storeName }) => requestVueData(ctx, (event) => {
763
1006
  void ctx.rpcServer?.getPiniaState({ event, storeName });
@@ -800,6 +1043,7 @@ function createMcpServer(ctx, vite) {
800
1043
  });
801
1044
  registerPageTools(server, ctx, vite);
802
1045
  registerDomTools(server, ctx);
1046
+ registerScreenshotTools(server, ctx);
803
1047
  registerConsoleTools(server, ctx);
804
1048
  registerEvaluateTools(server, ctx);
805
1049
  registerNetworkTools(server, ctx);
@@ -882,6 +1126,10 @@ function createServerVueRuntimeRpc(ctx) {
882
1126
  onEvaluateScriptUpdated: (event, data) => {
883
1127
  void ctx.hooks.callHook(event, data);
884
1128
  },
1129
+ takeScreenshot: () => void 0,
1130
+ onScreenshotTaken: (event, data) => {
1131
+ void ctx.hooks.callHook(event, data);
1132
+ },
885
1133
  getInspectorTree: () => void 0,
886
1134
  onInspectorTreeUpdated: (event, data) => {
887
1135
  void ctx.hooks.callHook(event, data);
@@ -1173,13 +1421,19 @@ function createRuntimeInjectionController(options, getConfig) {
1173
1421
  if (importee === VIRTUAL_RUNTIME_ID) {
1174
1422
  return RESOLVED_VIRTUAL_RUNTIME_ID;
1175
1423
  }
1424
+ if (importee === VIRTUAL_SCREENSHOT_CONFIG_ID) {
1425
+ return RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID;
1426
+ }
1176
1427
  return void 0;
1177
1428
  },
1178
1429
  load(id) {
1179
- if (id !== RESOLVED_VIRTUAL_RUNTIME_ID) {
1180
- return void 0;
1430
+ if (id === RESOLVED_VIRTUAL_RUNTIME_ID) {
1431
+ return createRuntimeModule();
1432
+ }
1433
+ if (id === RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID) {
1434
+ return createScreenshotConfigModule(options);
1181
1435
  }
1182
- return "import { startRuntimeClient } from '@xiaou66/vite-plugin-vue-mcp-next/runtime/client';\nvoid startRuntimeClient();";
1436
+ return void 0;
1183
1437
  },
1184
1438
  transformIndexHtml(html) {
1185
1439
  if (options.appendTo) {
@@ -1214,6 +1468,40 @@ ${code}`;
1214
1468
  }
1215
1469
  };
1216
1470
  }
1471
+ function createRuntimeModule() {
1472
+ return [
1473
+ "import { setScreenshotModuleRegistry, startRuntimeClient } from '@xiaou66/vite-plugin-vue-mcp-next/runtime/client';",
1474
+ `import { screenshotModuleRegistry } from '${VIRTUAL_SCREENSHOT_CONFIG_ID}';`,
1475
+ "setScreenshotModuleRegistry(screenshotModuleRegistry);",
1476
+ "void startRuntimeClient();"
1477
+ ].join("\n");
1478
+ }
1479
+ function createScreenshotConfigModule(options) {
1480
+ const paths = collectScreenshotImportPaths(options);
1481
+ const imports = paths.map((item, index) => `import * as m${String(index)} from ${JSON.stringify(item)};`).join("\n");
1482
+ const entries = paths.map((item, index) => `${JSON.stringify(item)}: m${String(index)}`).join(",\n ");
1483
+ return `${imports}
1484
+ export const screenshotModuleRegistry = {
1485
+ ${entries}
1486
+ };
1487
+ `;
1488
+ }
1489
+ function collectScreenshotImportPaths(options) {
1490
+ const paths = /* @__PURE__ */ new Set();
1491
+ for (const plugin of options.screenshot.snapdom.plugins) {
1492
+ paths.add(getPluginPath(plugin));
1493
+ }
1494
+ if (options.screenshot.snapdom.filter) {
1495
+ paths.add(options.screenshot.snapdom.filter);
1496
+ }
1497
+ if (options.screenshot.snapdom.fallbackURL) {
1498
+ paths.add(options.screenshot.snapdom.fallbackURL);
1499
+ }
1500
+ return [...paths];
1501
+ }
1502
+ function getPluginPath(plugin) {
1503
+ return typeof plugin === "string" ? plugin : plugin.path;
1504
+ }
1217
1505
 
1218
1506
  // src/plugin/mcpClientConfig/index.ts
1219
1507
  import path4 from "path";
@@ -1299,11 +1587,11 @@ import path3 from "path";
1299
1587
  async function updateJsonMcpClientConfig(options) {
1300
1588
  try {
1301
1589
  const config = await readJsonConfig(options.configPath);
1302
- if (!isPlainRecord(config)) {
1590
+ if (!isPlainRecord2(config)) {
1303
1591
  warnConfigFailure(options, "config root must be a JSON object");
1304
1592
  return;
1305
1593
  }
1306
- const mcpServers = isPlainRecord(config.mcpServers) ? config.mcpServers : {};
1594
+ const mcpServers = isPlainRecord2(config.mcpServers) ? config.mcpServers : {};
1307
1595
  if (Object.hasOwn(mcpServers, options.serverName)) {
1308
1596
  return;
1309
1597
  }
@@ -1336,7 +1624,7 @@ async function readOptionalTextFile2(filePath) {
1336
1624
  throw error;
1337
1625
  }
1338
1626
  }
1339
- function isPlainRecord(value) {
1627
+ function isPlainRecord2(value) {
1340
1628
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1341
1629
  }
1342
1630
  function warnConfigFailure(options, reason) {