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

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/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  | 页面列表 | Vite entry + Runtime | Vite entry + Runtime + CDP target | 用于多页面和多 tab 场景下选择调试目标 |
12
12
  | DOM tree | Runtime Hook | CDP 优先,Hook 兜底 | 可返回裁剪后的运行时 DOM 结构 |
13
13
  | DOM selector 查询 | Runtime Hook | CDP 优先,Hook 兜底 | 可按 selector 返回节点文本、属性和布局信息 |
14
+ | 页面截图 | snapdom DOM 截图 | CDP 真截图优先,snapdom 降级 | 返回视口、整页或指定元素的 base64 图片,并标记截图来源 |
14
15
  | Console 日志 | Runtime Hook | CDP 优先,Hook 兜底 | 采集 `log/info/warn/error/debug` 和运行时日志 |
15
16
  | Evaluate 控制台执行 | Runtime Hook | CDP 优先,Hook 兜底 | 默认关闭,必须显式开启 |
16
17
  | Network 请求 | Runtime Hook | CDP 优先,Hook 兜底 | 返回请求 URL、query、body、status、headers、response body |
@@ -122,6 +123,17 @@ vueMcpNext({
122
123
  },
123
124
  console: {
124
125
  maxRecords: 1000
126
+ },
127
+ screenshot: {
128
+ prefer: 'auto',
129
+ maxBytes: 5 * 1024 * 1024,
130
+ snapdom: {
131
+ options: {
132
+ scale: 1,
133
+ useProxy: undefined
134
+ },
135
+ plugins: []
136
+ }
125
137
  }
126
138
  })
127
139
  ```
@@ -136,6 +148,7 @@ vueMcpNext({
136
148
  | `mcpClients` | `{ cursor?: boolean; codex?: boolean; claudeCode?: boolean; trae?: boolean; serverName?: string }` | 全部启用 | 是否自动写入 Cursor、Codex、Claude Code、Trae 的项目级 MCP 配置 |
137
149
  | `updateCursorMcpJson` | `boolean | { enabled: boolean; serverName?: string }` | `true` | 兼容旧配置,建议新项目使用 `mcpClients` |
138
150
  | `appendTo` | `string | RegExp` | `undefined` | 非 HTML 入口注入点。配置后会在匹配入口模块前追加 runtime import |
151
+ | `screenshot` | `ScreenshotOptions` | CDP 优先,snapdom 降级 | 页面截图配置,控制真截图、DOM 降级截图、体积上限和 snapdom 扩展 |
139
152
 
140
153
  `appendTo` 适合 playground、框架包装入口、或不希望通过 `transformIndexHtml` 注入的场景:
141
154
 
@@ -242,6 +255,60 @@ Hook Network 覆盖 `fetch` 和 `XMLHttpRequest`。CDP Network 更接近 Chrome
242
255
 
243
256
  DOM 默认跳过 `script`、`style`、`noscript`,并隐藏 password input 的值。这样做是为了避免 MCP 上下文被大页面或敏感字段污染。
244
257
 
258
+ ### Screenshot 配置
259
+
260
+ `take_screenshot` 默认优先使用 CDP 真截图;没有 CDP 时,`prefer: 'auto'` 会降级到 `snapdom` DOM 截图。`source: 'snapdom'` 表示 DOM 渲染截图,不等同于浏览器真实像素截图。
261
+
262
+ ```ts
263
+ vueMcpNext({
264
+ screenshot: {
265
+ prefer: 'auto',
266
+ maxBytes: 5 * 1024 * 1024,
267
+ snapdom: {
268
+ options: {
269
+ scale: 2,
270
+ useProxy: 'http://localhost:3000/proxy?url=',
271
+ exclude: ['[data-no-screenshot]']
272
+ },
273
+ plugins: [
274
+ '/src/screenshot/snapdom-watermark.ts',
275
+ {
276
+ path: '@/screenshot/snapdom-mask-sensitive',
277
+ exportName: 'createMaskPlugin',
278
+ options: {
279
+ selectors: ['.token', '.password']
280
+ }
281
+ }
282
+ ],
283
+ filter: '/src/screenshot/snapdom-filter.ts',
284
+ fallbackURL: '/src/screenshot/snapdom-fallback-url.ts'
285
+ }
286
+ }
287
+ })
288
+ ```
289
+
290
+ `screenshot.snapdom.options` 继承 `snapdom` 的可序列化 options,常用字段包括 `scale`、`quality`、`cache`、`embedFonts`、`localFonts`、`useProxy`、`exclude`。`useProxy` 适合处理跨域图片或字体资源。
291
+
292
+ `plugins`、`filter`、`fallbackURL` 必须使用 Vite import 路径。不要在 `vite.config.ts` 中直接传函数,因为配置运行在 Node 侧,而截图执行发生在浏览器 runtime。
293
+
294
+ ```ts
295
+ // /src/screenshot/snapdom-mask-sensitive.ts
296
+ export function createMaskPlugin(options: { selectors: string[] }) {
297
+ return {
298
+ name: 'mask-sensitive',
299
+ afterClone(context: { clone: Document }) {
300
+ for (const selector of options.selectors) {
301
+ context.clone.querySelectorAll(selector).forEach((node) => {
302
+ node.textContent = '******'
303
+ })
304
+ }
305
+ }
306
+ }
307
+ }
308
+ ```
309
+
310
+ `snapdom` 降级截图可能受跨域图片、跨域 iframe、video、WebGL/canvas 污染、复杂 CSS 和字体加载时序影响。需要最高准确度时,请配置 CDP。
311
+
245
312
  ## MCP 工具清单
246
313
 
247
314
  ### 页面工具
@@ -262,8 +329,9 @@ DOM 默认跳过 `script`、`style`、`noscript`,并隐藏 password input 的
262
329
  |---|---|---|---|
263
330
  | `get_dom_tree` | `pageId?`、`maxDepth?`、`maxNodes?` | `source`、`snapshot` | 获取裁剪后的 DOM tree |
264
331
  | `query_dom` | `pageId?`、`selector`、`limit?` | `source`、`nodes` | 按 selector 查询元素摘要 |
332
+ | `take_screenshot` | `pageId?`、`target?`、`selector?`、`format?`、`prefer?` | `source`、`mimeType`、`data`、`width`、`height` | 页面截图,CDP 优先,snapdom 降级 |
265
333
 
266
- CDP 可用时输出 `source: 'cdp'`,否则使用 Runtime Hook 输出 `source: 'hook'`。
334
+ CDP 可用时 DOM 工具输出 `source: 'cdp'`,否则使用 Runtime Hook 输出 `source: 'hook'`。截图工具输出 `source: 'cdp' | 'snapdom'`。
267
335
 
268
336
  ### Console 工具
269
337
 
@@ -439,12 +507,13 @@ CDP 验证通过时,以下工具返回中应出现 `source: 'cdp'`:
439
507
  当前实现已通过以下验证:
440
508
 
441
509
  - MCP SSE 服务可连接
442
- - `tools/list` 可枚举 16 个 MCP 工具
510
+ - `tools/list` 可枚举 17 个 MCP 工具
443
511
  - `list_pages` 可返回 Vite entry、runtime 页面和 CDP target
444
512
  - Runtime Hook 可读取 DOM、Console、Network
445
513
  - Runtime Hook 可执行已授权表达式
446
514
  - Vue Runtime Bridge 可读取组件树、Router、Pinia tree/state
447
515
  - CDP 可读取 DOM tree、selector 查询、Evaluate、Console、Network
516
+ - CDP 可执行真截图,CDP 不可用时可使用 snapdom DOM 截图降级
448
517
  - `pnpm run check` 通过
449
518
  - `git diff --check` 通过
450
519
 
package/dist/index.cjs CHANGED
@@ -53,11 +53,13 @@ var DEFAULT_MASK_HEADERS = [
53
53
 
54
54
  // src/constants.ts
55
55
  var DEFAULT_MCP_PATH = "/__mcp";
56
+ var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
56
57
  var MCP_TOOL_NAMES = {
57
58
  listPages: "list_pages",
58
59
  getPageState: "get_page_state",
59
60
  getDomTree: "get_dom_tree",
60
61
  queryDom: "query_dom",
62
+ takeScreenshot: "take_screenshot",
61
63
  getConsoleLogs: "get_console_logs",
62
64
  clearConsoleLogs: "clear_console_logs",
63
65
  evaluateScript: "evaluate_script",
@@ -74,6 +76,8 @@ var MCP_TOOL_NAMES = {
74
76
  };
75
77
  var VIRTUAL_RUNTIME_ID = "virtual:vite-plugin-vue-mcp-next/runtime";
76
78
  var RESOLVED_VIRTUAL_RUNTIME_ID = `\0${VIRTUAL_RUNTIME_ID}`;
79
+ var VIRTUAL_SCREENSHOT_CONFIG_ID = "virtual:vite-plugin-vue-mcp-next/screenshot-config";
80
+ var RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID = `\0${VIRTUAL_SCREENSHOT_CONFIG_ID}`;
77
81
  var DEFAULT_MCP_CLIENT_SERVER_NAME = "vue-mcp-next";
78
82
  var DEFAULT_OPTIONS = {
79
83
  mcpPath: DEFAULT_MCP_PATH,
@@ -113,6 +117,14 @@ var DEFAULT_OPTIONS = {
113
117
  },
114
118
  console: {
115
119
  maxRecords: DEFAULT_CONSOLE_MAX_RECORDS
120
+ },
121
+ screenshot: {
122
+ prefer: "auto",
123
+ maxBytes: DEFAULT_SCREENSHOT_MAX_BYTES,
124
+ snapdom: {
125
+ options: {},
126
+ plugins: []
127
+ }
116
128
  }
117
129
  };
118
130
  function mergeMcpClientOptions(cursorConfig, mcpClients) {
@@ -163,6 +175,21 @@ function mergeOptions(options = {}) {
163
175
  console: {
164
176
  ...DEFAULT_OPTIONS.console,
165
177
  ...options.console
178
+ },
179
+ screenshot: {
180
+ ...DEFAULT_OPTIONS.screenshot,
181
+ ...options.screenshot,
182
+ snapdom: {
183
+ ...DEFAULT_OPTIONS.screenshot.snapdom,
184
+ ...options.screenshot?.snapdom,
185
+ options: {
186
+ ...DEFAULT_OPTIONS.screenshot.snapdom.options,
187
+ ...options.screenshot?.snapdom?.options
188
+ },
189
+ plugins: options.screenshot?.snapdom?.plugins ?? [
190
+ ...DEFAULT_OPTIONS.screenshot.snapdom.plugins
191
+ ]
192
+ }
166
193
  }
167
194
  };
168
195
  }
@@ -717,9 +744,225 @@ function getPathname(url) {
717
744
  }
718
745
  }
719
746
 
747
+ // src/mcp/tools/screenshot.ts
748
+ var import_zod5 = require("zod");
749
+
750
+ // src/cdp/cdpScreenshot.ts
751
+ async function cdpCaptureScreenshot(options) {
752
+ if (options.target === "element") {
753
+ return captureElementScreenshot(options);
754
+ }
755
+ if (options.target === "fullPage") {
756
+ return captureFullPageScreenshot(options);
757
+ }
758
+ return captureViewportScreenshot(options);
759
+ }
760
+ async function captureViewportScreenshot(options) {
761
+ const metrics = await options.client.Page.getLayoutMetrics();
762
+ const width = Math.ceil(metrics.cssLayoutViewport.clientWidth);
763
+ const height = Math.ceil(metrics.cssLayoutViewport.clientHeight);
764
+ const result = await options.client.Page.captureScreenshot(
765
+ omitUndefined({
766
+ format: options.format,
767
+ quality: createQuality(options),
768
+ captureBeyondViewport: false
769
+ })
770
+ );
771
+ return { data: result.data, width, height };
772
+ }
773
+ async function captureFullPageScreenshot(options) {
774
+ const metrics = await options.client.Page.getLayoutMetrics();
775
+ const width = Math.ceil(metrics.cssContentSize.width);
776
+ const height = Math.ceil(metrics.cssContentSize.height);
777
+ const result = await options.client.Page.captureScreenshot(
778
+ omitUndefined({
779
+ format: options.format,
780
+ quality: createQuality(options),
781
+ captureBeyondViewport: true,
782
+ clip: { x: 0, y: 0, width, height, scale: 1 }
783
+ })
784
+ );
785
+ return { data: result.data, width, height };
786
+ }
787
+ async function captureElementScreenshot(options) {
788
+ if (!options.selector) {
789
+ throw new Error("selector is required when target is element");
790
+ }
791
+ const rect = await getElementRect(options.client, options.selector);
792
+ const result = await options.client.Page.captureScreenshot(
793
+ omitUndefined({
794
+ format: options.format,
795
+ quality: createQuality(options),
796
+ captureBeyondViewport: true,
797
+ clip: {
798
+ x: rect.x,
799
+ y: rect.y,
800
+ width: rect.width,
801
+ height: rect.height,
802
+ scale: 1
803
+ }
804
+ })
805
+ );
806
+ return { data: result.data, width: rect.width, height: rect.height };
807
+ }
808
+ async function getElementRect(client, selector) {
809
+ const result = await client.Runtime.evaluate({
810
+ expression: createElementRectExpression(selector),
811
+ awaitPromise: true,
812
+ returnByValue: true
813
+ });
814
+ if (result.exceptionDetails) {
815
+ throw new Error(result.exceptionDetails.text || "element query failed");
816
+ }
817
+ const value = result.result.value;
818
+ if (!isElementRect(value)) {
819
+ throw new Error(`element not found: ${selector}`);
820
+ }
821
+ return value;
822
+ }
823
+ function createElementRectExpression(selector) {
824
+ return `(() => {
825
+ const el = document.querySelector(${JSON.stringify(selector)});
826
+ if (!el) return null;
827
+ const rect = el.getBoundingClientRect();
828
+ return {
829
+ x: rect.x + window.scrollX,
830
+ y: rect.y + window.scrollY,
831
+ width: rect.width,
832
+ height: rect.height
833
+ };
834
+ })()`;
835
+ }
836
+ function createQuality(options) {
837
+ return options.format === "png" ? void 0 : options.quality;
838
+ }
839
+ function omitUndefined(value) {
840
+ return Object.fromEntries(
841
+ Object.entries(value).filter(([, item]) => item !== void 0)
842
+ );
843
+ }
844
+ function isElementRect(value) {
845
+ if (!value || typeof value !== "object") {
846
+ return false;
847
+ }
848
+ const rect = value;
849
+ return typeof rect.x === "number" && typeof rect.y === "number" && typeof rect.width === "number" && rect.width > 0 && typeof rect.height === "number" && rect.height > 0;
850
+ }
851
+
852
+ // src/mcp/tools/screenshot.ts
853
+ var DEFAULT_SCREENSHOT_TARGET = "viewport";
854
+ var DEFAULT_SCREENSHOT_FORMAT = "png";
855
+ var screenshotInputSchema = {
856
+ pageId: import_zod5.z.string().optional(),
857
+ target: import_zod5.z.enum(["viewport", "fullPage", "element"]).optional(),
858
+ selector: import_zod5.z.string().optional(),
859
+ format: import_zod5.z.enum(["png", "jpeg", "webp"]).optional(),
860
+ prefer: import_zod5.z.enum(["auto", "cdp", "runtime"]).optional(),
861
+ quality: import_zod5.z.number().optional(),
862
+ scale: import_zod5.z.number().optional(),
863
+ snapdom: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).optional()
864
+ };
865
+ function registerScreenshotTools(server, ctx) {
866
+ server.registerTool(
867
+ MCP_TOOL_NAMES.takeScreenshot,
868
+ {
869
+ description: "Take a page screenshot using CDP or snapdom fallback.",
870
+ inputSchema: screenshotInputSchema
871
+ },
872
+ async (input) => handleTakeScreenshot(ctx, input)
873
+ );
874
+ }
875
+ async function handleTakeScreenshot(ctx, input) {
876
+ const target = input.target ?? DEFAULT_SCREENSHOT_TARGET;
877
+ const format = input.format ?? DEFAULT_SCREENSHOT_FORMAT;
878
+ const prefer = input.prefer ?? ctx.options.screenshot.prefer;
879
+ if (target === "element" && !input.selector) {
880
+ return createToolError("selector is required when target is element");
881
+ }
882
+ if (prefer !== "runtime") {
883
+ const cdp = await connectCdpForPage(ctx, input.pageId);
884
+ if (cdp) {
885
+ try {
886
+ const screenshot = await cdpCaptureScreenshot({
887
+ client: cdp.client,
888
+ target,
889
+ selector: input.selector,
890
+ format,
891
+ quality: input.quality
892
+ });
893
+ return createScreenshotResponse(ctx, {
894
+ source: "cdp",
895
+ target,
896
+ format,
897
+ data: screenshot.data,
898
+ width: screenshot.width,
899
+ height: screenshot.height,
900
+ mimeType: createMimeType(format),
901
+ byteLength: getBase64ByteLength(screenshot.data)
902
+ });
903
+ } finally {
904
+ await closeCdpClient(cdp.client);
905
+ }
906
+ }
907
+ if (prefer === "cdp") {
908
+ return createToolError("CDP screenshot is unavailable");
909
+ }
910
+ }
911
+ return createRuntimeScreenshot(ctx, input, { target, format });
912
+ }
913
+ async function createRuntimeScreenshot(ctx, input, normalized) {
914
+ const result = await requestRuntimeData(ctx, (event) => {
915
+ void ctx.rpcServer?.takeScreenshot({
916
+ event,
917
+ target: normalized.target,
918
+ selector: input.selector,
919
+ format: normalized.format,
920
+ quality: input.quality,
921
+ scale: input.scale,
922
+ snapdom: {
923
+ ...ctx.options.screenshot.snapdom,
924
+ options: {
925
+ ...ctx.options.screenshot.snapdom.options,
926
+ ...input.snapdom
927
+ }
928
+ }
929
+ });
930
+ });
931
+ if (isScreenshotTooLarge(ctx, result)) {
932
+ return createToolError(
933
+ `screenshot is too large: ${String(result.byteLength)} bytes`
934
+ );
935
+ }
936
+ if (!isPlainRecord(result)) {
937
+ return createToolError("runtime screenshot returned an invalid response");
938
+ }
939
+ return createToolResponse(result);
940
+ }
941
+ function createScreenshotResponse(ctx, result) {
942
+ if (result.byteLength > ctx.options.screenshot.maxBytes) {
943
+ return createToolError(
944
+ `screenshot is too large: ${String(result.byteLength)} bytes`
945
+ );
946
+ }
947
+ return createToolResponse(result);
948
+ }
949
+ function isScreenshotTooLarge(ctx, result) {
950
+ return isPlainRecord(result) && "byteLength" in result && typeof result.byteLength === "number" && result.byteLength > ctx.options.screenshot.maxBytes;
951
+ }
952
+ function isPlainRecord(value) {
953
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
954
+ }
955
+ function createMimeType(format) {
956
+ return `image/${format}`;
957
+ }
958
+ function getBase64ByteLength(data) {
959
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
960
+ return Math.ceil(data.length * 3 / 4) - padding;
961
+ }
962
+
720
963
  // src/mcp/tools/vue.ts
721
964
  var import_nanoid2 = require("nanoid");
722
- var import_zod5 = require("zod");
965
+ var import_zod6 = require("zod");
723
966
  function registerVueTools(server, ctx) {
724
967
  server.registerTool(
725
968
  MCP_TOOL_NAMES.getComponentTree,
@@ -732,7 +975,7 @@ function registerVueTools(server, ctx) {
732
975
  MCP_TOOL_NAMES.getComponentState,
733
976
  {
734
977
  description: "Get Vue component state.",
735
- inputSchema: { componentName: import_zod5.z.string() }
978
+ inputSchema: { componentName: import_zod6.z.string() }
736
979
  },
737
980
  async ({ componentName }) => requestVueData(ctx, (event) => {
738
981
  void ctx.rpcServer?.getInspectorState({ event, componentName });
@@ -743,10 +986,10 @@ function registerVueTools(server, ctx) {
743
986
  {
744
987
  description: "Edit Vue component state.",
745
988
  inputSchema: {
746
- componentName: import_zod5.z.string(),
747
- path: import_zod5.z.array(import_zod5.z.string()),
748
- value: import_zod5.z.string(),
749
- valueType: import_zod5.z.enum(["string", "number", "boolean", "object", "array"])
989
+ componentName: import_zod6.z.string(),
990
+ path: import_zod6.z.array(import_zod6.z.string()),
991
+ value: import_zod6.z.string(),
992
+ valueType: import_zod6.z.enum(["string", "number", "boolean", "object", "array"])
750
993
  }
751
994
  },
752
995
  ({ componentName, path: path5, value, valueType }) => {
@@ -766,7 +1009,7 @@ function registerVueTools(server, ctx) {
766
1009
  MCP_TOOL_NAMES.highlightComponent,
767
1010
  {
768
1011
  description: "Highlight a Vue component.",
769
- inputSchema: { componentName: import_zod5.z.string() }
1012
+ inputSchema: { componentName: import_zod6.z.string() }
770
1013
  },
771
1014
  ({ componentName }) => {
772
1015
  if (!ctx.rpcServer) {
@@ -794,7 +1037,7 @@ function registerVueTools(server, ctx) {
794
1037
  MCP_TOOL_NAMES.getPiniaState,
795
1038
  {
796
1039
  description: "Get Pinia store state.",
797
- inputSchema: { storeName: import_zod5.z.string() }
1040
+ inputSchema: { storeName: import_zod6.z.string() }
798
1041
  },
799
1042
  async ({ storeName }) => requestVueData(ctx, (event) => {
800
1043
  void ctx.rpcServer?.getPiniaState({ event, storeName });
@@ -837,6 +1080,7 @@ function createMcpServer(ctx, vite) {
837
1080
  });
838
1081
  registerPageTools(server, ctx, vite);
839
1082
  registerDomTools(server, ctx);
1083
+ registerScreenshotTools(server, ctx);
840
1084
  registerConsoleTools(server, ctx);
841
1085
  registerEvaluateTools(server, ctx);
842
1086
  registerNetworkTools(server, ctx);
@@ -919,6 +1163,10 @@ function createServerVueRuntimeRpc(ctx) {
919
1163
  onEvaluateScriptUpdated: (event, data) => {
920
1164
  void ctx.hooks.callHook(event, data);
921
1165
  },
1166
+ takeScreenshot: () => void 0,
1167
+ onScreenshotTaken: (event, data) => {
1168
+ void ctx.hooks.callHook(event, data);
1169
+ },
922
1170
  getInspectorTree: () => void 0,
923
1171
  onInspectorTreeUpdated: (event, data) => {
924
1172
  void ctx.hooks.callHook(event, data);
@@ -1210,13 +1458,19 @@ function createRuntimeInjectionController(options, getConfig) {
1210
1458
  if (importee === VIRTUAL_RUNTIME_ID) {
1211
1459
  return RESOLVED_VIRTUAL_RUNTIME_ID;
1212
1460
  }
1461
+ if (importee === VIRTUAL_SCREENSHOT_CONFIG_ID) {
1462
+ return RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID;
1463
+ }
1213
1464
  return void 0;
1214
1465
  },
1215
1466
  load(id) {
1216
- if (id !== RESOLVED_VIRTUAL_RUNTIME_ID) {
1217
- return void 0;
1467
+ if (id === RESOLVED_VIRTUAL_RUNTIME_ID) {
1468
+ return "import { startRuntimeClient } from '@xiaou66/vite-plugin-vue-mcp-next/runtime/client';\nvoid startRuntimeClient();";
1469
+ }
1470
+ if (id === RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID) {
1471
+ return createScreenshotConfigModule(options);
1218
1472
  }
1219
- return "import { startRuntimeClient } from '@xiaou66/vite-plugin-vue-mcp-next/runtime/client';\nvoid startRuntimeClient();";
1473
+ return void 0;
1220
1474
  },
1221
1475
  transformIndexHtml(html) {
1222
1476
  if (options.appendTo) {
@@ -1251,6 +1505,32 @@ ${code}`;
1251
1505
  }
1252
1506
  };
1253
1507
  }
1508
+ function createScreenshotConfigModule(options) {
1509
+ const paths = collectScreenshotImportPaths(options);
1510
+ const imports = paths.map((item, index) => `import * as m${String(index)} from ${JSON.stringify(item)};`).join("\n");
1511
+ const entries = paths.map((item, index) => `${JSON.stringify(item)}: m${String(index)}`).join(",\n ");
1512
+ return `${imports}
1513
+ export const screenshotModuleRegistry = {
1514
+ ${entries}
1515
+ };
1516
+ `;
1517
+ }
1518
+ function collectScreenshotImportPaths(options) {
1519
+ const paths = /* @__PURE__ */ new Set();
1520
+ for (const plugin of options.screenshot.snapdom.plugins) {
1521
+ paths.add(getPluginPath(plugin));
1522
+ }
1523
+ if (options.screenshot.snapdom.filter) {
1524
+ paths.add(options.screenshot.snapdom.filter);
1525
+ }
1526
+ if (options.screenshot.snapdom.fallbackURL) {
1527
+ paths.add(options.screenshot.snapdom.fallbackURL);
1528
+ }
1529
+ return [...paths];
1530
+ }
1531
+ function getPluginPath(plugin) {
1532
+ return typeof plugin === "string" ? plugin : plugin.path;
1533
+ }
1254
1534
 
1255
1535
  // src/plugin/mcpClientConfig/index.ts
1256
1536
  var import_node_path4 = __toESM(require("path"), 1);
@@ -1336,11 +1616,11 @@ var import_node_path3 = __toESM(require("path"), 1);
1336
1616
  async function updateJsonMcpClientConfig(options) {
1337
1617
  try {
1338
1618
  const config = await readJsonConfig(options.configPath);
1339
- if (!isPlainRecord(config)) {
1619
+ if (!isPlainRecord2(config)) {
1340
1620
  warnConfigFailure(options, "config root must be a JSON object");
1341
1621
  return;
1342
1622
  }
1343
- const mcpServers = isPlainRecord(config.mcpServers) ? config.mcpServers : {};
1623
+ const mcpServers = isPlainRecord2(config.mcpServers) ? config.mcpServers : {};
1344
1624
  if (Object.hasOwn(mcpServers, options.serverName)) {
1345
1625
  return;
1346
1626
  }
@@ -1373,7 +1653,7 @@ async function readOptionalTextFile2(filePath) {
1373
1653
  throw error;
1374
1654
  }
1375
1655
  }
1376
- function isPlainRecord(value) {
1656
+ function isPlainRecord2(value) {
1377
1657
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1378
1658
  }
1379
1659
  function warnConfigFailure(options, reason) {