weixin-devtools-mcp 0.0.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/build/index.js ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 微信开发者工具自动化 MCP 服务器
4
+ * 提供微信小程序自动化测试功能,包括:
5
+ * - 连接微信开发者工具
6
+ * - 获取页面快照和元素信息
7
+ * - 点击页面元素
8
+ * - 其他自动化操作
9
+ */
10
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
13
+ // 导入微信小程序自动化 SDK
14
+ import automator from "miniprogram-automator";
15
+ import path from "path";
16
+ // 导入模块化工具系统
17
+ import { allTools } from './tools/index.js';
18
+ // 导入zod-to-json-schema用于schema转换
19
+ import { zodToJsonSchema } from 'zod-to-json-schema';
20
+ /**
21
+ * 全局状态管理
22
+ */
23
+ const state = {
24
+ miniProgram: null, // MiniProgram 实例
25
+ currentPage: null, // 当前页面实例
26
+ elementMap: new Map(), // uid -> ElementMapInfo 映射
27
+ consoleStorage: {
28
+ consoleMessages: [],
29
+ exceptionMessages: [],
30
+ isMonitoring: false,
31
+ startTime: null
32
+ }, // Console存储
33
+ networkStorage: {
34
+ requests: [],
35
+ isMonitoring: false,
36
+ startTime: null,
37
+ originalMethods: {}
38
+ }, // 网络存储
39
+ };
40
+ /**
41
+ * 模块化工具适配器基础设施
42
+ */
43
+ // MockResponse 适配器类 - 适配模块化工具的响应接口
44
+ class MockResponse {
45
+ lines = [];
46
+ includeSnapshot = false;
47
+ attachedImages = [];
48
+ appendResponseLine(line) {
49
+ this.lines.push(line);
50
+ }
51
+ setIncludeSnapshot(include) {
52
+ this.includeSnapshot = include;
53
+ }
54
+ attachImage(data, mimeType) {
55
+ this.attachedImages.push({ data, mimeType });
56
+ }
57
+ getLines() {
58
+ return this.lines;
59
+ }
60
+ getAttachedImages() {
61
+ return this.attachedImages;
62
+ }
63
+ }
64
+ // 状态转换函数 - 将全局状态转换为ToolContext
65
+ function createToolContext() {
66
+ return {
67
+ miniProgram: state.miniProgram,
68
+ currentPage: state.currentPage,
69
+ elementMap: state.elementMap,
70
+ consoleStorage: state.consoleStorage,
71
+ networkStorage: state.networkStorage
72
+ };
73
+ }
74
+ // 创建工具处理器映射
75
+ const toolHandlers = new Map();
76
+ allTools.forEach(tool => {
77
+ toolHandlers.set(tool.name, tool);
78
+ });
79
+ // 工具定义转换函数 - 将模块化工具转换为传统MCP格式
80
+ function convertToolDefinition(tool) {
81
+ return {
82
+ name: tool.name,
83
+ description: tool.description,
84
+ inputSchema: zodToJsonSchema(tool.schema, {
85
+ strictUnions: true
86
+ }),
87
+ annotations: tool.annotations
88
+ };
89
+ }
90
+ /**
91
+ * 创建 MCP 服务器,提供微信开发者工具自动化功能
92
+ */
93
+ const server = new Server({
94
+ name: "weixin-devtools-mcp",
95
+ version: "0.3.3",
96
+ }, {
97
+ capabilities: {
98
+ resources: {},
99
+ tools: {},
100
+ },
101
+ });
102
+ /**
103
+ * 生成元素的唯一标识符 (uid)
104
+ * 使用CSS选择器路径作为uid
105
+ */
106
+ async function generateElementUid(element, index) {
107
+ try {
108
+ const tagName = element.tagName;
109
+ const className = await element.attribute('class').catch(() => '');
110
+ const id = await element.attribute('id').catch(() => '');
111
+ let selector = tagName;
112
+ if (id) {
113
+ selector += `#${id}`;
114
+ }
115
+ else if (className) {
116
+ selector += `.${className.split(' ')[0]}`;
117
+ }
118
+ else {
119
+ selector += `:nth-child(${index + 1})`;
120
+ }
121
+ return selector;
122
+ }
123
+ catch (error) {
124
+ return `${element.tagName || 'unknown'}:nth-child(${index + 1})`;
125
+ }
126
+ }
127
+ /**
128
+ * 递归获取页面所有元素的快照
129
+ */
130
+ async function getElementsSnapshot(container, prefix = '') {
131
+ const elements = [];
132
+ try {
133
+ // 获取所有子元素
134
+ const childElements = await container.$$('*').catch(() => []);
135
+ for (let i = 0; i < childElements.length; i++) {
136
+ const element = childElements[i];
137
+ try {
138
+ const uid = await generateElementUid(element, i);
139
+ const fullUid = prefix ? `${prefix} ${uid}` : uid;
140
+ const snapshot = {
141
+ uid: fullUid,
142
+ tagName: element.tagName || 'unknown',
143
+ };
144
+ // 获取元素文本
145
+ try {
146
+ const text = await element.text();
147
+ if (text && text.trim()) {
148
+ snapshot.text = text.trim();
149
+ }
150
+ }
151
+ catch (error) {
152
+ // 忽略无法获取文本的元素
153
+ }
154
+ // 获取元素位置信息
155
+ try {
156
+ const [size, offset] = await Promise.all([
157
+ element.size(),
158
+ element.offset()
159
+ ]);
160
+ snapshot.position = {
161
+ left: offset.left,
162
+ top: offset.top,
163
+ width: size.width,
164
+ height: size.height
165
+ };
166
+ }
167
+ catch (error) {
168
+ // 忽略无法获取位置的元素
169
+ }
170
+ // 获取常用属性
171
+ try {
172
+ const attributes = {};
173
+ const commonAttrs = ['class', 'id', 'data-*'];
174
+ for (const attr of commonAttrs) {
175
+ try {
176
+ const value = await element.attribute(attr);
177
+ if (value) {
178
+ attributes[attr] = value;
179
+ }
180
+ }
181
+ catch (error) {
182
+ // 忽略不存在的属性
183
+ }
184
+ }
185
+ if (Object.keys(attributes).length > 0) {
186
+ snapshot.attributes = attributes;
187
+ }
188
+ }
189
+ catch (error) {
190
+ // 忽略属性获取错误
191
+ }
192
+ elements.push(snapshot);
193
+ // 存储uid到ElementMapInfo的映射
194
+ state.elementMap.set(fullUid, {
195
+ selector: fullUid,
196
+ index: 0
197
+ });
198
+ }
199
+ catch (error) {
200
+ console.warn(`Error processing element ${i}:`, error);
201
+ }
202
+ }
203
+ }
204
+ catch (error) {
205
+ console.warn('Error getting elements snapshot:', error);
206
+ }
207
+ return elements;
208
+ }
209
+ /**
210
+ * 处理资源列表请求
211
+ * 提供可用的资源,如连接状态和页面快照
212
+ */
213
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
214
+ const resources = [];
215
+ // 连接状态资源
216
+ resources.push({
217
+ uri: "weixin://connection/status",
218
+ mimeType: "application/json",
219
+ name: "连接状态",
220
+ description: "微信开发者工具连接状态"
221
+ });
222
+ // 如果已连接,提供页面快照资源
223
+ if (state.miniProgram && state.currentPage) {
224
+ resources.push({
225
+ uri: "weixin://page/snapshot",
226
+ mimeType: "application/json",
227
+ name: "页面快照",
228
+ description: "当前页面的元素快照"
229
+ });
230
+ }
231
+ return { resources };
232
+ });
233
+ /**
234
+ * 处理资源读取请求
235
+ */
236
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
237
+ const url = new URL(request.params.uri);
238
+ if (url.pathname === "/connection/status") {
239
+ const status = {
240
+ connected: !!state.miniProgram,
241
+ hasCurrentPage: !!state.currentPage,
242
+ pagePath: state.currentPage ? await state.currentPage.path : null
243
+ };
244
+ return {
245
+ contents: [{
246
+ uri: request.params.uri,
247
+ mimeType: "application/json",
248
+ text: JSON.stringify(status, null, 2)
249
+ }]
250
+ };
251
+ }
252
+ if (url.pathname === "/page/snapshot") {
253
+ if (!state.currentPage) {
254
+ throw new Error("当前没有活动页面");
255
+ }
256
+ try {
257
+ const elements = await getElementsSnapshot(state.currentPage);
258
+ const snapshot = {
259
+ path: await state.currentPage.path,
260
+ elements
261
+ };
262
+ return {
263
+ contents: [{
264
+ uri: request.params.uri,
265
+ mimeType: "application/json",
266
+ text: JSON.stringify(snapshot, null, 2)
267
+ }]
268
+ };
269
+ }
270
+ catch (error) {
271
+ throw new Error(`获取页面快照失败: ${error instanceof Error ? error.message : String(error)}`);
272
+ }
273
+ }
274
+ throw new Error(`未知的资源: ${request.params.uri}`);
275
+ });
276
+ /**
277
+ * 处理工具列表请求
278
+ * 提供可用的微信自动化工具(包含所有模块化工具)
279
+ */
280
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
281
+ // 动态生成工具列表,包含所有29个工具
282
+ const tools = allTools.map(tool => convertToolDefinition(tool));
283
+ return { tools };
284
+ });
285
+ /**
286
+ * 处理工具调用请求
287
+ * 执行微信自动化相关操作
288
+ */
289
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
290
+ switch (request.params.name) {
291
+ case "connect_devtools": {
292
+ // 安全地提取参数,避免将undefined转换为字符串"undefined"
293
+ const projectPathArg = request.params.arguments?.projectPath;
294
+ if (!projectPathArg || typeof projectPathArg !== 'string') {
295
+ throw new Error("项目路径是必需的,且必须是有效的字符串路径");
296
+ }
297
+ const projectPath = String(projectPathArg);
298
+ const cliPath = request.params.arguments?.cliPath ? String(request.params.arguments.cliPath) : undefined;
299
+ const port = request.params.arguments?.port ? Number(request.params.arguments.port) : undefined;
300
+ try {
301
+ // 处理@playground/wx格式的路径,转换为绝对文件系统路径
302
+ let resolvedProjectPath = projectPath;
303
+ if (projectPath.startsWith('@playground/')) {
304
+ // 转换为相对路径,然后解析为绝对路径
305
+ const relativePath = projectPath.replace('@playground/', 'playground/');
306
+ resolvedProjectPath = path.resolve(process.cwd(), relativePath);
307
+ }
308
+ else if (!path.isAbsolute(projectPath)) {
309
+ // 如果不是绝对路径,转换为绝对路径
310
+ resolvedProjectPath = path.resolve(process.cwd(), projectPath);
311
+ }
312
+ const options = { projectPath: resolvedProjectPath };
313
+ if (cliPath)
314
+ options.cliPath = cliPath;
315
+ if (port)
316
+ options.port = port;
317
+ // 启动并连接微信开发者工具
318
+ state.miniProgram = await automator.launch(options);
319
+ // 获取当前页面
320
+ state.currentPage = await state.miniProgram.currentPage();
321
+ return {
322
+ content: [{
323
+ type: "text",
324
+ text: `成功连接到微信开发者工具\n项目路径: ${resolvedProjectPath}\n当前页面: ${state.currentPage ? await state.currentPage.path : '未知'}`
325
+ }]
326
+ };
327
+ }
328
+ catch (error) {
329
+ throw new Error(`连接微信开发者工具失败: ${error instanceof Error ? error.message : String(error)}`);
330
+ }
331
+ }
332
+ case "get_current_page": {
333
+ if (!state.miniProgram) {
334
+ throw new Error("请先连接到微信开发者工具");
335
+ }
336
+ try {
337
+ state.currentPage = await state.miniProgram.currentPage();
338
+ const pagePath = await state.currentPage.path;
339
+ return {
340
+ content: [{
341
+ type: "text",
342
+ text: `当前页面: ${pagePath}`
343
+ }]
344
+ };
345
+ }
346
+ catch (error) {
347
+ throw new Error(`获取当前页面失败: ${error instanceof Error ? error.message : String(error)}`);
348
+ }
349
+ }
350
+ case "get_page_snapshot": {
351
+ if (!state.currentPage) {
352
+ throw new Error("请先获取当前页面");
353
+ }
354
+ try {
355
+ // 清空之前的元素映射
356
+ state.elementMap.clear();
357
+ // 获取页面快照
358
+ const elements = await getElementsSnapshot(state.currentPage);
359
+ const snapshot = {
360
+ path: await state.currentPage.path,
361
+ elements
362
+ };
363
+ return {
364
+ content: [{
365
+ type: "text",
366
+ text: `页面快照获取成功\n页面路径: ${snapshot.path}\n元素数量: ${elements.length}\n\n${JSON.stringify(snapshot, null, 2)}`
367
+ }]
368
+ };
369
+ }
370
+ catch (error) {
371
+ throw new Error(`获取页面快照失败: ${error instanceof Error ? error.message : String(error)}`);
372
+ }
373
+ }
374
+ case "click": {
375
+ const uid = String(request.params.arguments?.uid);
376
+ const dblClick = Boolean(request.params.arguments?.dblClick);
377
+ if (!uid) {
378
+ throw new Error("元素uid是必需的");
379
+ }
380
+ if (!state.currentPage) {
381
+ throw new Error("请先获取当前页面");
382
+ }
383
+ try {
384
+ // 通过uid查找元素
385
+ const mapInfo = state.elementMap.get(uid);
386
+ if (!mapInfo) {
387
+ throw new Error(`找不到uid为 ${uid} 的元素,请先获取页面快照`);
388
+ }
389
+ // 获取所有匹配的元素
390
+ const elements = await state.currentPage.$(mapInfo.selector);
391
+ if (!elements || mapInfo.index >= elements.length) {
392
+ throw new Error(`无法找到选择器为 ${mapInfo.selector} 的元素,索引: ${mapInfo.index}`);
393
+ }
394
+ const element = elements[mapInfo.index];
395
+ // 执行点击操作
396
+ await element.tap();
397
+ // 如果是双击,再点击一次
398
+ if (dblClick) {
399
+ await new Promise(resolve => setTimeout(resolve, 100)); // 短暂延迟
400
+ await element.tap();
401
+ }
402
+ return {
403
+ content: [{
404
+ type: "text",
405
+ text: `${dblClick ? '双击' : '点击'}元素成功\nUID: ${uid}\n选择器: ${mapInfo.selector}[${mapInfo.index}]`
406
+ }]
407
+ };
408
+ }
409
+ catch (error) {
410
+ throw new Error(`点击元素失败: ${error instanceof Error ? error.message : String(error)}`);
411
+ }
412
+ }
413
+ case "screenshot": {
414
+ if (!state.miniProgram) {
415
+ throw new Error("请先连接到微信开发者工具");
416
+ }
417
+ const path = request.params.arguments?.path ? String(request.params.arguments.path) : undefined;
418
+ try {
419
+ // 确保页面状态稳定
420
+ if (!state.currentPage) {
421
+ state.currentPage = await state.miniProgram.currentPage();
422
+ }
423
+ // 确保页面完全加载和稳定
424
+ try {
425
+ if (state.currentPage && typeof state.currentPage.waitFor === 'function') {
426
+ // 等待页面稳定,增加等待时间
427
+ await state.currentPage.waitFor(1000);
428
+ }
429
+ }
430
+ catch (waitError) {
431
+ console.warn('页面等待失败,继续尝试截图:', waitError);
432
+ }
433
+ // 重试机制执行截图
434
+ let result = undefined;
435
+ let lastError;
436
+ for (let attempt = 1; attempt <= 3; attempt++) {
437
+ try {
438
+ if (path) {
439
+ // 保存到指定路径
440
+ await state.miniProgram.screenshot({ path });
441
+ result = path;
442
+ break;
443
+ }
444
+ else {
445
+ // 返回base64数据
446
+ const base64Data = await state.miniProgram.screenshot();
447
+ if (base64Data && typeof base64Data === 'string' && base64Data.length > 0) {
448
+ result = base64Data;
449
+ break;
450
+ }
451
+ else {
452
+ throw new Error('截图返回空数据');
453
+ }
454
+ }
455
+ }
456
+ catch (error) {
457
+ lastError = error instanceof Error ? error : new Error(String(error));
458
+ if (attempt < 3) {
459
+ // 重试前等待更长时间,让页面稳定
460
+ await new Promise(resolve => setTimeout(resolve, 1000 + attempt * 500));
461
+ }
462
+ }
463
+ }
464
+ if (!result && !path) {
465
+ throw new Error(`截图失败,已重试3次。最后错误: ${lastError?.message || '未知错误'}`);
466
+ }
467
+ if (path) {
468
+ return {
469
+ content: [{
470
+ type: "text",
471
+ text: `截图已保存到: ${path}`
472
+ }]
473
+ };
474
+ }
475
+ else {
476
+ const base64Data = result;
477
+ return {
478
+ content: [{
479
+ type: "text",
480
+ text: `截图获取成功\nBase64数据长度: ${base64Data.length} 字符\n格式: ${base64Data.startsWith('data:image') ? 'data URL' : 'base64'}`
481
+ }, {
482
+ type: "image",
483
+ data: base64Data,
484
+ mimeType: "image/png"
485
+ }]
486
+ };
487
+ }
488
+ }
489
+ catch (error) {
490
+ throw new Error(`截图失败: ${error instanceof Error ? error.message : String(error)}`);
491
+ }
492
+ }
493
+ default: {
494
+ // 使用模块化工具处理器处理新工具
495
+ const toolHandler = toolHandlers.get(request.params.name);
496
+ if (toolHandler) {
497
+ try {
498
+ const toolContext = createToolContext();
499
+ const mockResponse = new MockResponse();
500
+ // 创建模拟的请求对象
501
+ const toolRequest = {
502
+ params: request.params.arguments || {}
503
+ };
504
+ // 调用模块化工具处理器
505
+ await toolHandler.handler(toolRequest, mockResponse, toolContext);
506
+ // 更新全局状态(从ToolContext同步回来)
507
+ state.miniProgram = toolContext.miniProgram;
508
+ state.currentPage = toolContext.currentPage;
509
+ state.elementMap = toolContext.elementMap;
510
+ state.consoleStorage = toolContext.consoleStorage;
511
+ // 返回响应
512
+ const responseLines = mockResponse.getLines();
513
+ if (responseLines.length === 0) {
514
+ responseLines.push(`工具 ${request.params.name} 执行成功`);
515
+ }
516
+ return {
517
+ content: [{
518
+ type: "text",
519
+ text: responseLines.join('\n')
520
+ }]
521
+ };
522
+ }
523
+ catch (error) {
524
+ throw new Error(`工具 ${request.params.name} 执行失败: ${error instanceof Error ? error.message : String(error)}`);
525
+ }
526
+ }
527
+ throw new Error(`未知的工具: ${request.params.name}`);
528
+ }
529
+ }
530
+ });
531
+ /**
532
+ * Start the server using stdio transport.
533
+ * This allows the server to communicate via standard input/output streams.
534
+ */
535
+ async function main() {
536
+ const transport = new StdioServerTransport();
537
+ await server.connect(transport);
538
+ }
539
+ main().catch((error) => {
540
+ console.error("Server error:", error);
541
+ process.exit(1);
542
+ });