wechat-dev-mcp 1.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/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # WeChat Developer Tools MCP Server
2
+
3
+ [中文](README_zh-CN.md)
4
+
5
+ This is a Model Context Protocol (MCP) server that connects to WeChat Developer Tools via `miniprogram-automator`. It allows you to control the IDE and the mini-program from an MCP client (like Claude Desktop or an AI agent).
6
+
7
+ ## Prerequisites
8
+
9
+ 1. **Node.js**: Version 18+ is recommended (though it may work on older versions with some polyfills, this project is set up for modern Node).
10
+ 2. **WeChat Developer Tools**: Must be installed and running.
11
+ 3. **Enable Automation**: In WeChat Developer Tools, go to **Settings -> Security Settings** and enable **Service Port** (CLI/HTTP invocation).
12
+
13
+ ## Quick Start
14
+
15
+ ### Using with Claude Desktop (Recommended)
16
+
17
+ Add the following to your `claude_desktop_config.json` (e.g., `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "wechat-devtools": {
23
+ "command": "npx",
24
+ "args": [
25
+ "-y",
26
+ "wechat-dev-mcp"
27
+ ]
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Manual Installation
34
+
35
+ To install globally:
36
+
37
+ ```bash
38
+ npm install -g wechat-dev-mcp
39
+ ```
40
+
41
+ Then configure:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "wechat-devtools": {
47
+ "command": "wechat-dev-mcp",
48
+ "args": []
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Local Development
55
+
56
+ 1. Clone the repository
57
+ 2. Install dependencies:
58
+ ```bash
59
+ npm install
60
+ ```
61
+ 3. Build and run locally:
62
+ ```bash
63
+ node index.js
64
+ ```
65
+ 4. Configure Claude Desktop to point to your local file:
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "wechat-devtools": {
70
+ "command": "node",
71
+ "args": ["/absolute/path/to/wechat-dev-mcp/index.js"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Available Tools
78
+
79
+ * **`launch`**: Launch and connect to a mini-program project.
80
+ * `projectPath`: Absolute path to the project.
81
+ * `cliPath`: (Optional) Path to the DevTools CLI.
82
+ * **`connect`**: Connect to an already running DevTools instance.
83
+ * `wsEndpoint`: WebSocket endpoint (e.g., `ws://localhost:9420`).
84
+ * **`navigate_to`**: Navigate to a page (e.g., `/pages/index/index`).
85
+ * **`get_page_data`**: Get data from the current page.
86
+ * **`set_page_data`**: Set data on the current page.
87
+ * **`get_element`**: Get text, attributes, wxml of an element or tap it.
88
+ * **`call_method`**: Call a method on the current page instance.
89
+ * **`disconnect`**: Disconnect automation.
90
+
91
+ ## Troubleshooting
92
+
93
+ * **Connection Refused**: Ensure WeChat Developer Tools is running and the Service Port is enabled in Settings.
94
+ * **Path Issues**: Use absolute paths for `projectPath`.
@@ -0,0 +1,94 @@
1
+ # WeChat Developer Tools MCP Server
2
+
3
+ [English](README.md)
4
+
5
+ 这是一个连接微信开发者工具(WeChat Developer Tools)的 Model Context Protocol (MCP) 服务器,基于 `miniprogram-automator` 实现。它允许你通过 MCP 客户端(如 Claude Desktop 或 AI Agent)控制 IDE 和小程序。
6
+
7
+ ## 前置条件
8
+
9
+ 1. **Node.js**: 建议使用版本 18+(尽管可能在旧版本上也能运行,但本项目是针对现代 Node 环境设置的)。
10
+ 2. **微信开发者工具**: 必须已安装并运行。
11
+ 3. **开启自动化**: 在微信开发者工具中,进入 **设置 -> 安全设置**,开启 **服务端口**(CLI/HTTP 调用)。
12
+
13
+ ## 快速开始
14
+
15
+ ### 在 Claude Desktop 中使用(推荐)
16
+
17
+ 将以下内容添加到你的 `claude_desktop_config.json`(例如 macOS 上的 `~/Library/Application Support/Claude/claude_desktop_config.json`):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "wechat-devtools": {
23
+ "command": "npx",
24
+ "args": [
25
+ "-y",
26
+ "wechat-dev-mcp"
27
+ ]
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## 手动安装
34
+
35
+ 全局安装:
36
+
37
+ ```bash
38
+ npm install -g wechat-dev-mcp
39
+ ```
40
+
41
+ 配置:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "wechat-devtools": {
47
+ "command": "wechat-dev-mcp",
48
+ "args": []
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## 本地开发
55
+
56
+ 1. 克隆仓库
57
+ 2. 安装依赖:
58
+ ```bash
59
+ npm install
60
+ ```
61
+ 3. 本地运行:
62
+ ```bash
63
+ node index.js
64
+ ```
65
+ 4. 配置 Claude Desktop 指向本地文件:
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "wechat-devtools": {
70
+ "command": "node",
71
+ "args": ["/absolute/path/to/wechat-dev-mcp/index.js"]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## 可用工具
78
+
79
+ * **`launch`**: 启动并连接到一个小程序项目。
80
+ * `projectPath`: 项目的绝对路径。
81
+ * `cliPath`: (可选)开发者工具 CLI 的路径。
82
+ * **`connect`**: 连接到一个已经在运行的开发者工具实例。
83
+ * `wsEndpoint`: WebSocket 端点(例如 `ws://localhost:9420`)。
84
+ * **`navigate_to`**: 跳转到指定页面(例如 `/pages/index/index`)。
85
+ * **`get_page_data`**: 获取当前页面的数据。
86
+ * **`set_page_data`**: 设置当前页面的数据。
87
+ * **`get_element`**: 获取元素的文本、属性、wxml 或点击元素。
88
+ * **`call_method`**: 调用当前页面实例的方法。
89
+ * **`disconnect`**: 断开自动化连接。
90
+
91
+ ## 常见问题排查
92
+
93
+ * **连接被拒绝 (Connection Refused)**: 确保微信开发者工具正在运行,并且在设置中开启了服务端口。
94
+ * **路径问题**: `projectPath` 请务必使用绝对路径。
package/index.js ADDED
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+
3
+ // 1. Monkey patch console.log to ensure library logs don't corrupt JSON-RPC on stdout
4
+ const originalLog = console.log;
5
+ console.log = function(...args) {
6
+ console.error(...args);
7
+ };
8
+
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+ import automator from "miniprogram-automator";
16
+ import { z } from "zod";
17
+
18
+ // Global state
19
+ let miniProgram = null;
20
+ const DEFAULT_PORT = process.env.WECHAT_PORT || 9420;
21
+ const consoleLogs = []; // Ring buffer for logs
22
+
23
+ function setupListeners(mp) {
24
+ // Clear old logs on new connection
25
+ consoleLogs.length = 0;
26
+
27
+ // Listen for console logs
28
+ mp.on('console', msg => {
29
+ // Keep last 50 logs
30
+ if (consoleLogs.length >= 50) {
31
+ consoleLogs.shift();
32
+ }
33
+ consoleLogs.push({
34
+ type: 'console',
35
+ level: msg.level,
36
+ text: msg.text, // automator msg.text might be a promise or string depending on version, usually string in newer
37
+ args: msg.args, // raw args
38
+ timestamp: Date.now()
39
+ });
40
+ });
41
+
42
+ // Listen for exceptions
43
+ mp.on('exception', err => {
44
+ if (consoleLogs.length >= 50) {
45
+ consoleLogs.shift();
46
+ }
47
+ consoleLogs.push({
48
+ type: 'exception',
49
+ level: 'error',
50
+ text: err.message || JSON.stringify(err),
51
+ timestamp: Date.now()
52
+ });
53
+ });
54
+ }
55
+
56
+ // Tool definitions
57
+ const TOOLS = {
58
+ LAUNCH: "launch",
59
+ CONNECT: "connect",
60
+ CHECK_HEALTH: "check_health",
61
+ NAVIGATE_TO: "navigate_to",
62
+ GET_PAGE_DATA: "get_page_data",
63
+ SET_PAGE_DATA: "set_page_data",
64
+ GET_ELEMENT: "get_element",
65
+ CALL_METHOD: "call_method",
66
+ DISCONNECT: "disconnect"
67
+ };
68
+
69
+ const server = new Server(
70
+ {
71
+ name: "wechat-devtools-mcp",
72
+ version: "1.0.0",
73
+ },
74
+ {
75
+ capabilities: {
76
+ tools: {},
77
+ },
78
+ }
79
+ );
80
+
81
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
82
+ return {
83
+ tools: [
84
+ {
85
+ name: TOOLS.LAUNCH,
86
+ description: "Launch and connect to WeChat Developer Tools. [REQUIRED INITIAL STEP] Use this tool first to start controlling a mini-program. Requires the absolute project path.",
87
+ inputSchema: zodToJsonSchema(
88
+ z.object({
89
+ projectPath: z.string().describe("Absolute path to the mini-program project"),
90
+ cliPath: z.string().optional().describe("Path to the WeChat DevTools CLI executable (optional, will try to auto-detect)"),
91
+ port: z.number().optional().describe("Port for automation (optional)"),
92
+ })
93
+ ),
94
+ },
95
+ {
96
+ name: TOOLS.CONNECT,
97
+ description: "Connect to an already running WeChat Developer Tools instance via WebSocket. Use this if 'launch' fails or you want to attach to an existing session.",
98
+ inputSchema: zodToJsonSchema(
99
+ z.object({
100
+ wsEndpoint: z.string().optional().describe(`WebSocket endpoint (e.g., ws://localhost:9420). Defaults to ws://localhost:${DEFAULT_PORT}`),
101
+ })
102
+ ),
103
+ },
104
+ {
105
+ name: TOOLS.CHECK_HEALTH,
106
+ description: "[CRITICAL] Use this tool to verify if the mini-program is running correctly after ANY code changes. It returns the current page path, network status, and recent console errors.",
107
+ inputSchema: zodToJsonSchema(z.object({})),
108
+ },
109
+ {
110
+ name: TOOLS.NAVIGATE_TO,
111
+ description: "Navigate to a specific page in the mini-program.",
112
+ inputSchema: zodToJsonSchema(
113
+ z.object({
114
+ url: z.string().describe("The URL of the page to navigate to (e.g., /pages/index/index)"),
115
+ })
116
+ ),
117
+ },
118
+ {
119
+ name: TOOLS.GET_PAGE_DATA,
120
+ description: "Get the data of the current page. Useful for verifying state changes after interactions or API calls.",
121
+ inputSchema: zodToJsonSchema(
122
+ z.object({
123
+ path: z.string().optional().describe("The data path to retrieve (optional, returns full data if omitted)"),
124
+ })
125
+ ),
126
+ },
127
+ {
128
+ name: TOOLS.SET_PAGE_DATA,
129
+ description: "Set data on the current page. Use this to mock state or trigger UI updates for testing.",
130
+ inputSchema: zodToJsonSchema(
131
+ z.object({
132
+ data: z.record(z.any()).describe("The data object to set"),
133
+ })
134
+ ),
135
+ },
136
+ {
137
+ name: TOOLS.GET_ELEMENT,
138
+ description: "Get information about an element (text, wxml, attributes, computed style) or interact with it (tap, input, trigger). Use this to inspect UI or perform actions.",
139
+ inputSchema: zodToJsonSchema(
140
+ z.object({
141
+ selector: z.string().describe("The CSS selector of the element"),
142
+ action: z.enum(["text", "wxml", "outerWxml", "attribute", "style", "tap", "input", "trigger"]).optional().default("text").describe("Action to perform: 'text' (content), 'wxml' (structure), 'attribute' (get attr), 'style' (get style), 'tap' (click), 'input' (enter text), 'trigger' (custom event)"),
143
+ attributeName: z.string().optional().describe("Attribute name (required if action is 'attribute')"),
144
+ styleName: z.string().optional().describe("Style name (required if action is 'style')"),
145
+ value: z.string().optional().describe("Value to input (required if action is 'input')"),
146
+ eventName: z.string().optional().describe("Event name to trigger (required if action is 'trigger')"),
147
+ detail: z.record(z.any()).optional().describe("Event detail object (optional for 'trigger')"),
148
+ })
149
+ ),
150
+ },
151
+ {
152
+ name: TOOLS.CALL_METHOD,
153
+ description: "Call a method on the current page.",
154
+ inputSchema: zodToJsonSchema(
155
+ z.object({
156
+ method: z.string().describe("The name of the method to call"),
157
+ args: z.array(z.union([z.string(), z.number(), z.boolean(), z.object({}).passthrough()])).optional().default([]).describe("Arguments to pass to the method"),
158
+ })
159
+ ),
160
+ },
161
+ {
162
+ name: TOOLS.DISCONNECT,
163
+ description: "Disconnect from the mini-program.",
164
+ inputSchema: zodToJsonSchema(z.object({})),
165
+ },
166
+ ],
167
+ };
168
+ });
169
+
170
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
171
+ try {
172
+ const { name, arguments: args } = request.params;
173
+
174
+ switch (name) {
175
+ case TOOLS.LAUNCH: {
176
+ if (miniProgram) {
177
+ return { content: [{ type: "text", text: "Already connected to a Mini Program instance. Disconnect first." }] };
178
+ }
179
+ const { projectPath, cliPath, port } = args;
180
+ const options = { projectPath, cliPath, port };
181
+ // Remove undefined keys
182
+ Object.keys(options).forEach(key => options[key] === undefined && delete options[key]);
183
+
184
+ // Try to connect to existing instance first
185
+ const tryPort = port || DEFAULT_PORT;
186
+ try {
187
+ // Attempt connection (short timeout might be good but connect usually fails fast if port closed)
188
+ miniProgram = await automator.connect({ wsEndpoint: `ws://localhost:${tryPort}` });
189
+ setupListeners(miniProgram);
190
+ return { content: [{ type: "text", text: `Connected to existing Mini Program instance on port ${tryPort}.` }] };
191
+ } catch (e) {
192
+ // If connection fails, launch new instance
193
+ // console.error("Connect failed, launching new instance:", e);
194
+ miniProgram = await automator.launch(options);
195
+ setupListeners(miniProgram);
196
+ return { content: [{ type: "text", text: "Successfully launched and connected to Mini Program." }] };
197
+ }
198
+ }
199
+
200
+ case TOOLS.CONNECT: {
201
+ if (miniProgram) {
202
+ return { content: [{ type: "text", text: "Already connected to a Mini Program instance. Disconnect first." }] };
203
+ }
204
+ let { wsEndpoint } = args;
205
+ if (!wsEndpoint) {
206
+ wsEndpoint = `ws://localhost:${DEFAULT_PORT}`;
207
+ }
208
+ miniProgram = await automator.connect({ wsEndpoint });
209
+ setupListeners(miniProgram);
210
+ return { content: [{ type: "text", text: "Successfully connected to Mini Program." }] };
211
+ }
212
+
213
+ case TOOLS.CHECK_HEALTH: {
214
+ if (!miniProgram) {
215
+ return { content: [{ type: "text", text: JSON.stringify({ connected: false, error: "Not connected" }) }] };
216
+ }
217
+
218
+ // 1. Get Page Path
219
+ let pagePath = "unknown";
220
+ try {
221
+ const page = await miniProgram.currentPage();
222
+ pagePath = page ? page.path : "no_page_found";
223
+ } catch (e) {
224
+ pagePath = `error_getting_path: ${e.message}`;
225
+ }
226
+
227
+ // 2. Get Recent Errors (Console Error or Exceptions)
228
+ // Filter logs where level is 'error' or type is 'exception'
229
+ const recentErrors = consoleLogs
230
+ .filter(log => log.level === 'error' || log.type === 'exception')
231
+ .slice(-5) // Get last 5
232
+ .map(log => `[${new Date(log.timestamp).toISOString().split('T')[1].slice(0,8)}] ${log.text}`);
233
+
234
+ // 3. Network Status (Mock / System Info)
235
+ let networkType = "unknown";
236
+ try {
237
+ const res = await miniProgram.systemInfo();
238
+ // systemInfo in automator might not directly have networkType?
239
+ // Actually miniProgram.systemInfo() returns what wx.getSystemInfo returns.
240
+ // But networkType is usually in wx.getNetworkType.
241
+ // Let's try to call wx.getNetworkType via remote evaluation if possible,
242
+ // but miniProgram.evaluate or callWxMethod might be needed.
243
+ // Automator has callWxMethod? No, page has callMethod (for internal methods).
244
+ // miniProgram has `remote`? No.
245
+ // Actually miniProgram.evaluate() runs code in the app service context.
246
+ const netRes = await miniProgram.evaluate(() => new Promise(resolve => wx.getNetworkType({ success: resolve, fail: () => resolve({ networkType: 'fail' }) })));
247
+ if (netRes) networkType = netRes.networkType;
248
+ } catch (e) {
249
+ networkType = `check_failed: ${e.message}`;
250
+ }
251
+
252
+ return {
253
+ content: [{
254
+ type: "text",
255
+ text: JSON.stringify({
256
+ connected: true,
257
+ pagePath,
258
+ networkType,
259
+ recentConsoleErrors: recentErrors.length > 0 ? recentErrors : ["No recent errors"]
260
+ }, null, 2)
261
+ }]
262
+ };
263
+ }
264
+
265
+ case TOOLS.DISCONNECT: {
266
+ if (!miniProgram) {
267
+ return { content: [{ type: "text", text: "Not connected." }] };
268
+ }
269
+ await miniProgram.disconnect();
270
+ miniProgram = null;
271
+ return { content: [{ type: "text", text: "Disconnected." }] };
272
+ }
273
+
274
+ // For all other commands, check connection first
275
+ default: {
276
+ if (!miniProgram) {
277
+ return { isError: true, content: [{ type: "text", text: "Not connected to Mini Program. Use launch or connect first." }] };
278
+ }
279
+
280
+ // Handle other tools
281
+ switch (name) {
282
+ case TOOLS.NAVIGATE_TO: {
283
+ const { url } = args;
284
+ const page = await miniProgram.reLaunch(url); // using reLaunch to be safe, or navigateTo
285
+ // Note: automator.navigateTo returns the page object
286
+ return { content: [{ type: "text", text: `Navigated to ${url}. Path: ${page.path}` }] };
287
+ }
288
+
289
+ case TOOLS.GET_PAGE_DATA: {
290
+ const page = await miniProgram.currentPage();
291
+ const { path } = args;
292
+ const data = await page.data(path);
293
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
294
+ }
295
+
296
+ case TOOLS.SET_PAGE_DATA: {
297
+ const page = await miniProgram.currentPage();
298
+ const { data } = args;
299
+ await page.setData(data);
300
+ return { content: [{ type: "text", text: "Data set successfully." }] };
301
+ }
302
+
303
+ case TOOLS.GET_ELEMENT: {
304
+ const page = await miniProgram.currentPage();
305
+ const { selector, action, attributeName, styleName, value, eventName, detail } = args;
306
+ const element = await page.$(selector);
307
+
308
+ if (!element) {
309
+ return { isError: true, content: [{ type: "text", text: `Element not found: ${selector}` }] };
310
+ }
311
+
312
+ let result;
313
+ if (action === "text") result = await element.text();
314
+ else if (action === "wxml") result = await element.wxml();
315
+ else if (action === "outerWxml") result = await element.outerWxml();
316
+ else if (action === "attribute") result = await element.attribute(attributeName);
317
+ else if (action === "style") result = await element.style(styleName);
318
+ else if (action === "tap") {
319
+ await element.tap();
320
+ result = "Tapped element";
321
+ }
322
+ else if (action === "input") {
323
+ await element.input(value || "");
324
+ result = `Input value "${value}" set`;
325
+ }
326
+ else if (action === "trigger") {
327
+ await element.trigger(eventName, detail || {});
328
+ result = `Triggered event "${eventName}"`;
329
+ }
330
+
331
+ return { content: [{ type: "text", text: String(result) }] };
332
+ }
333
+
334
+ case TOOLS.CALL_METHOD: {
335
+ const page = await miniProgram.currentPage();
336
+ const { method, args: methodArgs } = args;
337
+ const result = await page.callMethod(method, ...methodArgs);
338
+ return { content: [{ type: "text", text: result === undefined ? "undefined" : JSON.stringify(result, null, 2) }] };
339
+ }
340
+
341
+ default:
342
+ return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] };
343
+ }
344
+ }
345
+ }
346
+ } catch (error) {
347
+ return {
348
+ isError: true,
349
+ content: [{ type: "text", text: `Error: ${error.message}` }],
350
+ };
351
+ }
352
+ });
353
+
354
+ // Helper for Zod to JSON Schema since we can't easily import zod-to-json-schema in this environment without proper setup or it might conflict
355
+ // Actually, I can use a simplified helper or just rely on manual schema construction if needed, but the library is installed.
356
+ // Let's try to import it properly.
357
+ import { zodToJsonSchema } from "zod-to-json-schema";
358
+
359
+ async function main() {
360
+ const transport = new StdioServerTransport();
361
+ await server.connect(transport);
362
+ console.error("WeChat DevTools MCP Server running on stdio");
363
+ }
364
+
365
+ main().catch((error) => {
366
+ console.error("Fatal error:", error);
367
+ process.exit(1);
368
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "wechat-dev-mcp",
3
+ "version": "1.0.1",
4
+ "description": "A Model Context Protocol (MCP) server for controlling WeChat Developer Tools",
5
+ "bin": {
6
+ "wechat-dev-mcp": "./index.js"
7
+ },
8
+ "files": [
9
+ "index.js",
10
+ "README.md",
11
+ "README_zh-CN.md"
12
+ ],
13
+ "type": "module",
14
+ "main": "index.js",
15
+ "scripts": {
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "mcp-server",
21
+ "wechat",
22
+ "miniprogram",
23
+ "devtools",
24
+ "automation",
25
+ "miniprogram-automator"
26
+ ],
27
+ "author": "CuiJiawei",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.25.3",
31
+ "miniprogram-automator": "^0.12.1",
32
+ "zod": "^3.23.0",
33
+ "zod-to-json-schema": "^3.25.1"
34
+ }
35
+ }