@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/README.md +71 -2
- package/dist/index.cjs +302 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +302 -14
- package/dist/index.js.map +1 -1
- package/dist/runtime/client.cjs +148 -1
- package/dist/runtime/client.cjs.map +1 -1
- package/dist/runtime/client.d.cts +15 -1
- package/dist/runtime/client.d.ts +15 -1
- package/dist/runtime/client.js +137 -1
- package/dist/runtime/client.js.map +1 -1
- package/package.json +2 -1
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
|
|
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` 可枚举
|
|
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
|
|
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:
|
|
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:
|
|
747
|
-
path:
|
|
748
|
-
value:
|
|
749
|
-
valueType:
|
|
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:
|
|
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:
|
|
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
|
|
1217
|
-
return
|
|
1467
|
+
if (id === RESOLVED_VIRTUAL_RUNTIME_ID) {
|
|
1468
|
+
return createRuntimeModule();
|
|
1469
|
+
}
|
|
1470
|
+
if (id === RESOLVED_VIRTUAL_SCREENSHOT_CONFIG_ID) {
|
|
1471
|
+
return createScreenshotConfigModule(options);
|
|
1218
1472
|
}
|
|
1219
|
-
return
|
|
1473
|
+
return void 0;
|
|
1220
1474
|
},
|
|
1221
1475
|
transformIndexHtml(html) {
|
|
1222
1476
|
if (options.appendTo) {
|
|
@@ -1251,6 +1505,40 @@ ${code}`;
|
|
|
1251
1505
|
}
|
|
1252
1506
|
};
|
|
1253
1507
|
}
|
|
1508
|
+
function createRuntimeModule() {
|
|
1509
|
+
return [
|
|
1510
|
+
"import { setScreenshotModuleRegistry, startRuntimeClient } from '@xiaou66/vite-plugin-vue-mcp-next/runtime/client';",
|
|
1511
|
+
`import { screenshotModuleRegistry } from '${VIRTUAL_SCREENSHOT_CONFIG_ID}';`,
|
|
1512
|
+
"setScreenshotModuleRegistry(screenshotModuleRegistry);",
|
|
1513
|
+
"void startRuntimeClient();"
|
|
1514
|
+
].join("\n");
|
|
1515
|
+
}
|
|
1516
|
+
function createScreenshotConfigModule(options) {
|
|
1517
|
+
const paths = collectScreenshotImportPaths(options);
|
|
1518
|
+
const imports = paths.map((item, index) => `import * as m${String(index)} from ${JSON.stringify(item)};`).join("\n");
|
|
1519
|
+
const entries = paths.map((item, index) => `${JSON.stringify(item)}: m${String(index)}`).join(",\n ");
|
|
1520
|
+
return `${imports}
|
|
1521
|
+
export const screenshotModuleRegistry = {
|
|
1522
|
+
${entries}
|
|
1523
|
+
};
|
|
1524
|
+
`;
|
|
1525
|
+
}
|
|
1526
|
+
function collectScreenshotImportPaths(options) {
|
|
1527
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1528
|
+
for (const plugin of options.screenshot.snapdom.plugins) {
|
|
1529
|
+
paths.add(getPluginPath(plugin));
|
|
1530
|
+
}
|
|
1531
|
+
if (options.screenshot.snapdom.filter) {
|
|
1532
|
+
paths.add(options.screenshot.snapdom.filter);
|
|
1533
|
+
}
|
|
1534
|
+
if (options.screenshot.snapdom.fallbackURL) {
|
|
1535
|
+
paths.add(options.screenshot.snapdom.fallbackURL);
|
|
1536
|
+
}
|
|
1537
|
+
return [...paths];
|
|
1538
|
+
}
|
|
1539
|
+
function getPluginPath(plugin) {
|
|
1540
|
+
return typeof plugin === "string" ? plugin : plugin.path;
|
|
1541
|
+
}
|
|
1254
1542
|
|
|
1255
1543
|
// src/plugin/mcpClientConfig/index.ts
|
|
1256
1544
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -1336,11 +1624,11 @@ var import_node_path3 = __toESM(require("path"), 1);
|
|
|
1336
1624
|
async function updateJsonMcpClientConfig(options) {
|
|
1337
1625
|
try {
|
|
1338
1626
|
const config = await readJsonConfig(options.configPath);
|
|
1339
|
-
if (!
|
|
1627
|
+
if (!isPlainRecord2(config)) {
|
|
1340
1628
|
warnConfigFailure(options, "config root must be a JSON object");
|
|
1341
1629
|
return;
|
|
1342
1630
|
}
|
|
1343
|
-
const mcpServers =
|
|
1631
|
+
const mcpServers = isPlainRecord2(config.mcpServers) ? config.mcpServers : {};
|
|
1344
1632
|
if (Object.hasOwn(mcpServers, options.serverName)) {
|
|
1345
1633
|
return;
|
|
1346
1634
|
}
|
|
@@ -1373,7 +1661,7 @@ async function readOptionalTextFile2(filePath) {
|
|
|
1373
1661
|
throw error;
|
|
1374
1662
|
}
|
|
1375
1663
|
}
|
|
1376
|
-
function
|
|
1664
|
+
function isPlainRecord2(value) {
|
|
1377
1665
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1378
1666
|
}
|
|
1379
1667
|
function warnConfigFailure(options, reason) {
|