@skillbuilder/minitool-e2e 0.0.0-beta-20261027053041051
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 +61 -0
- package/lib/assets.js +94 -0
- package/lib/browser.js +63 -0
- package/lib/codegen-port.js +146 -0
- package/lib/contract.js +112 -0
- package/lib/contract.test.js +130 -0
- package/lib/gate.js +58 -0
- package/lib/gate.test.js +144 -0
- package/lib/host-page.js +68 -0
- package/lib/host-server.js +121 -0
- package/lib/index.js +48 -0
- package/lib/mcp/server.js +224 -0
- package/lib/mcp/stdio-entry.js +12 -0
- package/lib/mcp/stdio.js +19 -0
- package/lib/session.js +592 -0
- package/lib/upload.js +41 -0
- package/lib/upload.test.js +44 -0
- package/package.json +25 -0
- package/types/assets.d.ts +9 -0
- package/types/browser.d.ts +28 -0
- package/types/codegen-port.d.ts +82 -0
- package/types/contract.d.ts +43 -0
- package/types/contract.test.d.ts +1 -0
- package/types/gate.d.ts +29 -0
- package/types/gate.test.d.ts +1 -0
- package/types/host-page.d.ts +15 -0
- package/types/host-server.d.ts +17 -0
- package/types/index.d.ts +30 -0
- package/types/mcp/server.d.ts +28 -0
- package/types/mcp/stdio-entry.d.ts +2 -0
- package/types/mcp/stdio.d.ts +7 -0
- package/types/session.d.ts +182 -0
- package/types/upload.d.ts +31 -0
- package/types/upload.test.d.ts +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerMinitoolE2ETools = registerMinitoolE2ETools;
|
|
4
|
+
exports.createMinitoolE2EMcpServer = createMinitoolE2EMcpServer;
|
|
5
|
+
/**
|
|
6
|
+
* 把一套会话暴露成 14 个 MCP tool(stdio + JSON-RPC)。工具分两组、名字就是边界,权威清单在
|
|
7
|
+
* `../contract.ts`;只剩 network 尚未实现,如实返回 not_implemented。
|
|
8
|
+
*
|
|
9
|
+
* 下面的名字仍写字面量(比 `E2E_TOOL_NAMES[3]` 可读),靠 `contract.test.ts` 断言
|
|
10
|
+
* 「注册集合 === contract 声明集合」双向一致兜底 —— 少注册或拼错都会红。
|
|
11
|
+
*
|
|
12
|
+
* MCP 这一层的意义是能力隔离:独立进程、独立依赖(chromium 只在这侧),出码 Agent 只认工具名,
|
|
13
|
+
* 以后换远端浏览器集群或给别的 Agent 复用都只需换端点。
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ 一个进程只服务**一个**会话。并发靠起多个进程 —— 沙箱 1 核 2GiB,并发浏览器只会互相拖死。
|
|
16
|
+
*/
|
|
17
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
18
|
+
const zod_1 = require("zod");
|
|
19
|
+
const contract_1 = require("../contract");
|
|
20
|
+
const session_1 = require("../session");
|
|
21
|
+
/**
|
|
22
|
+
* ★ 必须取 contract.ts 那个常量,不能在这里另写一遍字面量:
|
|
23
|
+
* prompt 里点名的工具名是 `mcp__{这个 server 名}__{工具名}`(由 contract 的 `toMcpToolName` 推导)。
|
|
24
|
+
* 两边一旦漂移,**每一个** prompt 里的工具名都会是错的,而下面那个一致性测试比的是裸工具名、
|
|
25
|
+
* 抓不到这种漂移 —— 所以只能靠同源。
|
|
26
|
+
*/
|
|
27
|
+
const SERVER_NAME = contract_1.MINITOOL_E2E_MCP_SERVER_NAME;
|
|
28
|
+
const SERVER_VERSION = "0.0.1";
|
|
29
|
+
/**
|
|
30
|
+
* 把 E2E tools 注册到调用方提供的 server,供 redminitool 聚合认证、构建、发布等能力。
|
|
31
|
+
* E2E 包只拥有自己的工具与会话状态,不再强制拥有整个 MCP server。
|
|
32
|
+
*/
|
|
33
|
+
function registerMinitoolE2ETools(server, opts = {}) {
|
|
34
|
+
let session = null;
|
|
35
|
+
const requireSession = () => {
|
|
36
|
+
if (!session)
|
|
37
|
+
throw new Error("尚未打开会话,请先调用 e2e_open。");
|
|
38
|
+
return session;
|
|
39
|
+
};
|
|
40
|
+
const asJson = (value) => ({
|
|
41
|
+
content: [{ type: "text", text: JSON.stringify(value ?? null, null, 2) }],
|
|
42
|
+
});
|
|
43
|
+
// ══ 通用能力:任何产物形态都成立 ══
|
|
44
|
+
server.registerTool("e2e_open", {
|
|
45
|
+
title: "Open artifact",
|
|
46
|
+
description: "Launch Chromium + miniapp-simulator and load the built artifact. Must be called first. " +
|
|
47
|
+
"Does NOT probe the game bridge — call e2e_game_attach for that (games only).",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
distDir: zod_1.z.string().optional().describe("Artifact directory containing index.html"),
|
|
50
|
+
entry: zod_1.z.string().optional(),
|
|
51
|
+
viewport: zod_1.z
|
|
52
|
+
.object({
|
|
53
|
+
width: zod_1.z.number().int().positive(),
|
|
54
|
+
height: zod_1.z.number().int().positive(),
|
|
55
|
+
deviceScaleFactor: zod_1.z.number().positive().optional(),
|
|
56
|
+
})
|
|
57
|
+
.optional(),
|
|
58
|
+
},
|
|
59
|
+
}, async ({ distDir, entry, viewport }) => {
|
|
60
|
+
const target = distDir ?? opts.distDir ?? process.env.MINITOOL_E2E_DIST_DIR;
|
|
61
|
+
if (!target) {
|
|
62
|
+
throw new Error("缺少产物目录:请传 distDir 或设置 MINITOOL_E2E_DIST_DIR。");
|
|
63
|
+
}
|
|
64
|
+
if (session) {
|
|
65
|
+
await session.close().catch(() => undefined);
|
|
66
|
+
session = null;
|
|
67
|
+
}
|
|
68
|
+
// ★ open 失败必须就地收尾,否则进程再也退不掉(实测踩过):create() 先起 Chromium、
|
|
69
|
+
// open() 才读产物目录 ⇒ 目录有问题时**浏览器已经在跑**。留着它会让事件循环永不为空,
|
|
70
|
+
// MCP client 断开后进程也不退,在沙箱里就是个吃满内存的孤儿;后续工具还会打到一个
|
|
71
|
+
// 什么都没加载的页面上,报出完全指错方向的错。
|
|
72
|
+
const pending = await session_1.MinitoolE2ESession.create({ ...opts, viewport: opts.viewport });
|
|
73
|
+
try {
|
|
74
|
+
const result = await pending.open({ distDir: target, entry, viewport });
|
|
75
|
+
session = pending;
|
|
76
|
+
return asJson(result);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
await pending.close().catch(() => undefined);
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.registerTool("e2e_logs", {
|
|
84
|
+
title: "Read runtime logs",
|
|
85
|
+
description: "Drain simulator console output, guest errors, CSP violations and failed requests.",
|
|
86
|
+
inputSchema: { level: zod_1.z.enum(["debug", "info", "warn", "error"]).optional() },
|
|
87
|
+
}, async ({ level }) => asJson(await requireSession().logs(level)));
|
|
88
|
+
server.registerTool("e2e_screenshot", {
|
|
89
|
+
title: "Capture frame",
|
|
90
|
+
description: "Capture the current rendered frame for visual verification.",
|
|
91
|
+
inputSchema: {},
|
|
92
|
+
}, async () => {
|
|
93
|
+
const shot = await requireSession().screenshot();
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
// ★ 这个 text part 同时是「图片已进桶」的**声明**:出码侧 event-bridge 看它里面有没有非空
|
|
97
|
+
// `url`,来决定要不要把下面的 image part 剥掉再推给浏览器(见 agentbase 的
|
|
98
|
+
// `stripUploadedImageParts`,`codegen-agent/src/codegen/shared/event-bridge.ts` 是调用方)。
|
|
99
|
+
// 改这里的 JSON 形状前先看那两处 —— 形状一变,剥图判定就失效,base64 又会开始往前端传,
|
|
100
|
+
// 而前端还在等 url。
|
|
101
|
+
// `description` 是人话,模型读的是它;`url` / `fileKey` 是给前端和排障用的。
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: JSON.stringify({
|
|
105
|
+
description: shot.description,
|
|
106
|
+
url: shot.url ?? null,
|
|
107
|
+
fileKey: shot.fileKey ?? null,
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
// 模型必须真看到这一帧(`contract.ts` 的流程明确要求「看首帧」),所以 image part 删不得 ——
|
|
111
|
+
// 被剥掉的只是推给浏览器的那一份副本。
|
|
112
|
+
{ type: "image", data: shot.data, mimeType: shot.mimeType },
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
server.registerTool("e2e_close", {
|
|
117
|
+
title: "Close session",
|
|
118
|
+
description: "Close the e2e session and release Chromium after collecting evidence.",
|
|
119
|
+
inputSchema: {},
|
|
120
|
+
}, async () => {
|
|
121
|
+
if (!session)
|
|
122
|
+
return asJson({ closed: false });
|
|
123
|
+
const result = await session.close();
|
|
124
|
+
session = null;
|
|
125
|
+
return asJson(result);
|
|
126
|
+
});
|
|
127
|
+
// 以下 1 个**尚未实现**(network)。description 第一句与返回体都刻意写明「不可作为判定依据」且
|
|
128
|
+
// 不含空集合 —— 否则模型会把空结果读成「查过了没问题」并据此判 PASS,那比没有这个工具更糟。
|
|
129
|
+
server.registerTool("e2e_network", {
|
|
130
|
+
title: "Observe network (not implemented)",
|
|
131
|
+
description: "NOT IMPLEMENTED — the result must not be used as evidence. " +
|
|
132
|
+
"Intended to list requests the mini-tool made, with status code and response body.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
scope: zod_1.z.enum(["all", "failed"]).optional(),
|
|
135
|
+
limit: zod_1.z.number().int().positive().optional(),
|
|
136
|
+
},
|
|
137
|
+
}, async ({ scope, limit }) => asJson(await requireSession().network({ scope, limit })));
|
|
138
|
+
server.registerTool("e2e_dom", {
|
|
139
|
+
title: "Inspect rendered DOM",
|
|
140
|
+
description: "Summarise the rendered DOM so blank-screen and whether a required element exists can " +
|
|
141
|
+
"be judged without a screenshot. Each node carries a stable CSS selector, tag, text and " +
|
|
142
|
+
"rect (in logical CSS px, same space as e2e_screenshot and e2e_interact x/y). For canvas " +
|
|
143
|
+
"/ game artifacts this also returns sceneTree — the read-only Pixi/Three scene hierarchy " +
|
|
144
|
+
"from the simulator inspector. Each scene node has: ref (unique, `<providerId>:<node>`), " +
|
|
145
|
+
"label, visible, selectable (whether it can be hit), and source.file + source.definitionKey " +
|
|
146
|
+
"(source location). Multiple canvases appear as multiple providers (each with providerId + " +
|
|
147
|
+
"engine); refs are providerId-prefixed so they stay unique.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
selector: zod_1.z.string().optional(),
|
|
150
|
+
maxNodes: zod_1.z.number().int().positive().optional(),
|
|
151
|
+
},
|
|
152
|
+
}, async ({ selector, maxNodes }) => asJson(await requireSession().dom({ selector, maxNodes })));
|
|
153
|
+
server.registerTool("e2e_interact", {
|
|
154
|
+
title: "Dispatch a simulator gesture or guest input",
|
|
155
|
+
description: "Dispatch a simulator-backed gesture, reload or guest input against any artifact (unlike e2e_game_act, " +
|
|
156
|
+
"which uses the game bridge). Prefer `selector` (targets the element center); only use x/y for " +
|
|
157
|
+
"canvas/composite UI where a selector cannot reach. x/y are logical CSS px — same scale as " +
|
|
158
|
+
"e2e_screenshot and e2e_dom rect (see the viewport field on e2e_open).",
|
|
159
|
+
inputSchema: {
|
|
160
|
+
action: zod_1.z.enum(["tap", "doubleTap", "longPress", "scroll", "swipe", "input", "reload"]),
|
|
161
|
+
selector: zod_1.z.string().optional(),
|
|
162
|
+
text: zod_1.z.string().optional(),
|
|
163
|
+
x: zod_1.z.number().optional(),
|
|
164
|
+
y: zod_1.z.number().optional(),
|
|
165
|
+
toX: zod_1.z.number().optional(),
|
|
166
|
+
toY: zod_1.z.number().optional(),
|
|
167
|
+
deltaX: zod_1.z
|
|
168
|
+
.number()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Horizontal scroll distance; scroll requires a non-zero deltaX or deltaY"),
|
|
171
|
+
deltaY: zod_1.z
|
|
172
|
+
.number()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("Vertical scroll distance; scroll requires a non-zero deltaX or deltaY"),
|
|
175
|
+
duration: zod_1.z.number().optional(),
|
|
176
|
+
},
|
|
177
|
+
}, async (input) => asJson(await requireSession().interact(input)));
|
|
178
|
+
server.registerTool("e2e_health", {
|
|
179
|
+
title: "Runtime health summary",
|
|
180
|
+
description: "Summarise uncaught guest errors, console errors/warnings and navigation into explicit " +
|
|
181
|
+
"counts plus a small sample. Returns status ok/degraded; never an empty collection.",
|
|
182
|
+
inputSchema: {},
|
|
183
|
+
}, async () => asJson(await requireSession().health()));
|
|
184
|
+
// ══ 游戏专属:全部经 window.__GAME_TEST__ 桥,只有 game_2d/game_3d 模板在 ?playtest=1 下才挂。
|
|
185
|
+
// 非游戏产物调下面任何一个都会 reject ⇒ 先用 e2e_game_attach 判定桥在不在。
|
|
186
|
+
server.registerTool("e2e_game_attach", {
|
|
187
|
+
title: "Attach game bridge",
|
|
188
|
+
description: "Wait for the artifact's game test bridge and report whether it exists. " +
|
|
189
|
+
"Games only: call it after e2e_open and before any other e2e_game_* tool. " +
|
|
190
|
+
"A non-game artifact will wait out the timeout and report bridgeAvailable=false.",
|
|
191
|
+
inputSchema: { timeoutMs: zod_1.z.number().int().positive().max(60_000).optional() },
|
|
192
|
+
}, async ({ timeoutMs }) => asJson(await requireSession().gameAttach(timeoutMs)));
|
|
193
|
+
server.registerTool("e2e_game_manifest", {
|
|
194
|
+
title: "Read game rules",
|
|
195
|
+
description: "Read the game test manifest: objective, supported actions, milestones, win/loss rules.",
|
|
196
|
+
inputSchema: {},
|
|
197
|
+
}, async () => asJson(await requireSession().manifest()));
|
|
198
|
+
server.registerTool("e2e_game_snapshot", {
|
|
199
|
+
title: "Inspect game state",
|
|
200
|
+
description: "Read a semantic snapshot of current game state (phase, player, score, entities).",
|
|
201
|
+
inputSchema: {},
|
|
202
|
+
}, async () => asJson(await requireSession().snapshot()));
|
|
203
|
+
server.registerTool("e2e_game_act", {
|
|
204
|
+
title: "Play game action",
|
|
205
|
+
description: "Perform one manifest-declared action, then return the observed result.",
|
|
206
|
+
inputSchema: { action: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()) },
|
|
207
|
+
}, async ({ action }) => asJson(await requireSession().act(action)));
|
|
208
|
+
server.registerTool("e2e_game_step", {
|
|
209
|
+
title: "Advance game clock",
|
|
210
|
+
description: "Advance the deterministic clock by N ms. Also the way to force a render frame — " +
|
|
211
|
+
"playtest mode stops the ticker, and pointer hit-testing silently fails before the first render.",
|
|
212
|
+
inputSchema: { ms: zod_1.z.number().int().min(1).max(30_000) },
|
|
213
|
+
}, async ({ ms }) => asJson(await requireSession().step(ms)));
|
|
214
|
+
server.registerTool("e2e_game_reset", {
|
|
215
|
+
title: "Reset game",
|
|
216
|
+
description: "Reset the game, optionally with a deterministic random seed.",
|
|
217
|
+
inputSchema: { seed: zod_1.z.number().int().optional() },
|
|
218
|
+
}, async ({ seed }) => asJson(await requireSession().reset(seed)));
|
|
219
|
+
}
|
|
220
|
+
function createMinitoolE2EMcpServer(opts = {}) {
|
|
221
|
+
const server = new mcp_js_1.McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
222
|
+
registerMinitoolE2ETools(server, opts);
|
|
223
|
+
return server;
|
|
224
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* MCP stdio 入口。★ stdout 是 JSON-RPC 通道,**任何**多余输出都会破坏协议帧 ——
|
|
6
|
+
* 本进程所有日志一律走 stderr,别在任何地方 console.log。
|
|
7
|
+
*/
|
|
8
|
+
const stdio_1 = require("./stdio");
|
|
9
|
+
(0, stdio_1.runMinitoolE2EStdioServer)().catch((err) => {
|
|
10
|
+
process.stderr.write(`[minitool-e2e] MCP server failed: ${err instanceof Error ? err.stack : String(err)}\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/lib/mcp/stdio.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runMinitoolE2EStdioServer = runMinitoolE2EStdioServer;
|
|
4
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5
|
+
const server_1 = require("./server");
|
|
6
|
+
/**
|
|
7
|
+
* 在 stdio 上启动小工具 MCP server。
|
|
8
|
+
*
|
|
9
|
+
* stdout 是 MCP JSON-RPC 专用通道,这里以及调用方都不得向 stdout 输出日志。
|
|
10
|
+
*/
|
|
11
|
+
async function runMinitoolE2EStdioServer(opts = {}) {
|
|
12
|
+
const server = (0, server_1.createMinitoolE2EMcpServer)({
|
|
13
|
+
...opts,
|
|
14
|
+
distDir: opts.distDir ?? process.env.MINITOOL_E2E_DIST_DIR,
|
|
15
|
+
});
|
|
16
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
process.stderr.write("[minitool-e2e] MCP server ready on stdio\n");
|
|
19
|
+
}
|