@jacob-z/zz-ui-brigde-mcp 1.0.0

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.
Files changed (3) hide show
  1. package/README.md +119 -0
  2. package/dist/index.js +361 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @zz-mcp/ui-diff-mcp
2
+
3
+ AI 驱动的 MasterGo 设计稿节点查询 MCP 服务。将 MasterGo 插件中的节点数据通过 MCP protocol 暴露给 Claude Code 等 AI 工具。
4
+
5
+ ## 功能
6
+
7
+ - **实时选中快照**:MasterGo 中选中任意节点,AI 可立即获取其属性
8
+ - **按需节点查询**:通过 nodeId 精确查询任意节点的详细信息
9
+ - **双通道通信**:MCP stdio 通道 + HTTP 本地服务(port 18765)
10
+
11
+ ## 可用函数
12
+
13
+ ### `get_selected_node`
14
+
15
+ 获取 MasterGo 当前选中节点的属性快照。无需等待,直接返回缓存数据。
16
+
17
+ **参数**:无
18
+
19
+ **返回值示例**:
20
+
21
+ ```json
22
+ {
23
+ "selected": {
24
+ "id": "11:809",
25
+ "name": "容器 1117",
26
+ "type": "FRAME",
27
+ "x": 488,
28
+ "y": 24,
29
+ "width": 236,
30
+ "height": 232,
31
+ "fills": [{ "type": "SOLID", "color": { "r": 0.91, "g": 0.79, "b": 0.79, "a": 1 } }],
32
+ "opacity": 1,
33
+ "childrenIds": ["11:0010", "11:0019", "11:0028", "11:0037"]
34
+ },
35
+ "freshAt": 1778263424580
36
+ }
37
+ ```
38
+
39
+ ### `get_node_by_id`
40
+
41
+ 按 nodeId 精确查询 MasterGo 节点属性。同步阻塞等待插件响应,超时 25 秒。
42
+
43
+ **参数**:
44
+ | 字段 | 类型 | 必填 | 说明 |
45
+ |------|------|------|------|
46
+ | nodeId | string | 是 | MasterGo 节点 ID |
47
+
48
+ **返回值**:节点属性 JSON,包含 id、name、type、x、y、width、height、opacity、fills、fontSize、characters、fontName 等字段
49
+
50
+ ## MCP 配置
51
+
52
+ ### Claude Code
53
+
54
+ 在 `.claude/settings.json` 中添加:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "ui-diff-mcp": {
60
+ "command": "npx",
61
+ "args": ["-y", "@zz-mcp/ui-diff-mcp"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ 重启 Claude Code 后,在对话中执行 `/mcp` 确认状态为 `connected`。
68
+
69
+ ### 本地开发(源码运行)
70
+
71
+ ```bash
72
+ # 进入包目录
73
+ cd packages/ui-diff-mcp
74
+
75
+ # 源码运行(需 tsx)
76
+ pnpm mcp:server
77
+
78
+ # 或构建后运行
79
+ pnpm build
80
+ node dist/index.js
81
+ ```
82
+
83
+ ## 架构
84
+
85
+ ```
86
+ Claude Code (MCP Client)
87
+ |
88
+ | stdio protocol
89
+ v
90
+ MCP Server (@zz-mcp/ui-diff-mcp)
91
+ |
92
+ | HTTP localhost:18765
93
+ v
94
+ UI iframe (mcp-bridge.ts)
95
+ |
96
+ | postMessage
97
+ v
98
+ MasterGo Sandbox (lib/main.ts)
99
+ |
100
+ v
101
+ MasterGo Plugin API (mg.*)
102
+ ```
103
+
104
+ ## 配套插件
105
+
106
+ 本 MCP 服务需要配合 MasterGo 插件使用。插件负责:
107
+
108
+ - 监听选择变化,推送节点快照
109
+ - 响应 `GET_NODE_INFO` 消息,查询指定节点
110
+
111
+ 插件发布地址:MasterGo 插件广场(待补充)
112
+
113
+ ## 环境要求
114
+
115
+ - Node.js >= 18.0.0
116
+
117
+ ## 版本
118
+
119
+ 当前版本:1.0.0
package/dist/index.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ import * as http from "node:http";
3
+ import { v4 } from "uuid";
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { ListToolsRequestSchema, CallToolRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
7
+ const PENDING_TTL_MS = 3e4;
8
+ class PendingRequestManager {
9
+ pending = /* @__PURE__ */ new Map();
10
+ queue = [];
11
+ create(nodeId, fields) {
12
+ return new Promise((resolve, reject) => {
13
+ const requestId = v4();
14
+ const timeoutHandle = setTimeout(() => {
15
+ this.reject(requestId, new Error("Timeout: plugin did not respond within 30s"));
16
+ }, PENDING_TTL_MS);
17
+ this.pending.set(requestId, { requestId, nodeId, fields, resolve, reject, timeoutHandle });
18
+ this.queue.push(requestId);
19
+ });
20
+ }
21
+ resolve(requestId, data) {
22
+ const req = this.pending.get(requestId);
23
+ if (!req)
24
+ return;
25
+ clearTimeout(req.timeoutHandle);
26
+ this.pending.delete(requestId);
27
+ this.removeFromQueue(requestId);
28
+ req.resolve(data);
29
+ }
30
+ reject(requestId, reason) {
31
+ const req = this.pending.get(requestId);
32
+ if (!req)
33
+ return;
34
+ clearTimeout(req.timeoutHandle);
35
+ this.pending.delete(requestId);
36
+ this.removeFromQueue(requestId);
37
+ req.reject(reason);
38
+ }
39
+ getPending() {
40
+ const id = this.queue[0];
41
+ if (!id)
42
+ return void 0;
43
+ const req = this.pending.get(id);
44
+ if (!req)
45
+ return void 0;
46
+ return { requestId: req.requestId, nodeId: req.nodeId, fields: req.fields };
47
+ }
48
+ removeFromQueue(requestId) {
49
+ const idx = this.queue.indexOf(requestId);
50
+ if (idx !== -1)
51
+ this.queue.splice(idx, 1);
52
+ }
53
+ }
54
+ const pendingRequests = new PendingRequestManager();
55
+ class SelectionCacheManager {
56
+ snapshot = { selected: null, reason: "no_plugin_connected" };
57
+ lastSeq = -1;
58
+ update(data, seq) {
59
+ if (seq <= this.lastSeq)
60
+ return;
61
+ this.lastSeq = seq;
62
+ if (data === null) {
63
+ this.snapshot = { selected: null, reason: "no_selection", freshAt: Date.now() };
64
+ } else {
65
+ this.snapshot = { selected: data, freshAt: Date.now() };
66
+ }
67
+ }
68
+ get() {
69
+ return this.snapshot;
70
+ }
71
+ }
72
+ const selectionCache = new SelectionCacheManager();
73
+ const PORT = 18765;
74
+ const LONG_POLL_TIMEOUT_MS = 25e3;
75
+ const CORS_HEADERS = {
76
+ "Access-Control-Allow-Origin": "*",
77
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
78
+ "Access-Control-Allow-Headers": "Content-Type",
79
+ "Access-Control-Allow-Private-Network": "true"
80
+ };
81
+ function setCorsHeaders(res) {
82
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
83
+ res.setHeader(key, value);
84
+ }
85
+ }
86
+ function readBody(req) {
87
+ return new Promise((resolve) => {
88
+ let body = "";
89
+ req.on("data", (chunk) => {
90
+ body += chunk.toString();
91
+ });
92
+ req.on("end", () => resolve(body));
93
+ });
94
+ }
95
+ function parseJson(raw) {
96
+ try {
97
+ return JSON.parse(raw);
98
+ } catch {
99
+ return void 0;
100
+ }
101
+ }
102
+ function isNodeOrError(val) {
103
+ if (typeof val !== "object" || val === null)
104
+ return false;
105
+ const obj = val;
106
+ if (typeof obj.error === "string" && typeof obj.nodeId === "string")
107
+ return true;
108
+ return typeof obj.id === "string";
109
+ }
110
+ function isNodeQueryResponse(val) {
111
+ if (typeof val !== "object" || val === null)
112
+ return false;
113
+ const obj = val;
114
+ return typeof obj.requestId === "string" && isNodeOrError(obj.data);
115
+ }
116
+ function isSelectionBody(val) {
117
+ if (typeof val !== "object" || val === null)
118
+ return false;
119
+ const obj = val;
120
+ return typeof obj.seq === "number" && "data" in obj;
121
+ }
122
+ async function handleRequest(req, res) {
123
+ setCorsHeaders(res);
124
+ if (req.method === "OPTIONS") {
125
+ res.writeHead(200);
126
+ res.end();
127
+ return;
128
+ }
129
+ const url = req.url ?? "/";
130
+ if (url === "/health" && req.method === "GET") {
131
+ res.writeHead(200, { "Content-Type": "application/json" });
132
+ res.end(JSON.stringify({ status: "ok", pid: process.pid }));
133
+ return;
134
+ }
135
+ if (url === "/selection" && req.method === "POST") {
136
+ const raw = await readBody(req);
137
+ const body = parseJson(raw);
138
+ if (isSelectionBody(body)) {
139
+ selectionCache.update(body.data, body.seq);
140
+ res.writeHead(200, { "Content-Type": "application/json" });
141
+ res.end(JSON.stringify({ ok: true }));
142
+ } else {
143
+ res.writeHead(400, { "Content-Type": "application/json" });
144
+ res.end(JSON.stringify({ error: "invalid body" }));
145
+ }
146
+ return;
147
+ }
148
+ if (url === "/pending-requests" && req.method === "GET") {
149
+ const pending = pendingRequests.getPending();
150
+ if (pending) {
151
+ res.writeHead(200, { "Content-Type": "application/json" });
152
+ res.end(JSON.stringify(pending));
153
+ return;
154
+ }
155
+ let resolved = false;
156
+ const timer = setTimeout(() => {
157
+ if (!resolved) {
158
+ resolved = true;
159
+ res.writeHead(204);
160
+ res.end();
161
+ }
162
+ }, LONG_POLL_TIMEOUT_MS);
163
+ const poll = setInterval(() => {
164
+ if (resolved) {
165
+ clearInterval(poll);
166
+ return;
167
+ }
168
+ const p = pendingRequests.getPending();
169
+ if (p) {
170
+ resolved = true;
171
+ clearInterval(poll);
172
+ clearTimeout(timer);
173
+ res.writeHead(200, { "Content-Type": "application/json" });
174
+ res.end(JSON.stringify(p));
175
+ }
176
+ }, 200);
177
+ req.on("close", () => {
178
+ if (!resolved) {
179
+ resolved = true;
180
+ clearInterval(poll);
181
+ clearTimeout(timer);
182
+ }
183
+ });
184
+ return;
185
+ }
186
+ if (url === "/node-result" && req.method === "POST") {
187
+ const raw = await readBody(req);
188
+ const body = parseJson(raw);
189
+ if (isNodeQueryResponse(body)) {
190
+ pendingRequests.resolve(body.requestId, body.data);
191
+ res.writeHead(200, { "Content-Type": "application/json" });
192
+ res.end(JSON.stringify({ ok: true }));
193
+ } else {
194
+ res.writeHead(400, { "Content-Type": "application/json" });
195
+ res.end(JSON.stringify({ error: "invalid body" }));
196
+ }
197
+ return;
198
+ }
199
+ res.writeHead(404, { "Content-Type": "application/json" });
200
+ res.end(JSON.stringify({ error: "not found" }));
201
+ }
202
+ function killStaleServer() {
203
+ return new Promise((resolve) => {
204
+ const req = http.get(`http://localhost:${PORT}/health`, (res) => {
205
+ let body = "";
206
+ res.on("data", (c) => {
207
+ body += c.toString();
208
+ });
209
+ res.on("end", () => {
210
+ try {
211
+ const parsed = JSON.parse(body);
212
+ if (typeof parsed.pid === "number" && parsed.pid !== process.pid) {
213
+ console.error(`[ai-tools] Killing stale server process (pid=${parsed.pid})`);
214
+ try {
215
+ process.kill(parsed.pid, "SIGTERM");
216
+ } catch {
217
+ }
218
+ }
219
+ } catch {
220
+ }
221
+ setTimeout(resolve, 500);
222
+ });
223
+ });
224
+ req.on("error", () => setTimeout(resolve, 200));
225
+ });
226
+ }
227
+ function createHttpServer(resolve, reject, retries = 2) {
228
+ const server = http.createServer((req, res) => {
229
+ handleRequest(req, res).catch((err) => {
230
+ const msg = err instanceof Error ? err.message : String(err);
231
+ console.error("[ai-tools] Request handler error:", msg);
232
+ if (!res.headersSent) {
233
+ res.writeHead(500);
234
+ res.end();
235
+ }
236
+ });
237
+ });
238
+ server.on("error", (err) => {
239
+ if (err.code === "EADDRINUSE" && retries > 0) {
240
+ killStaleServer().then(() => createHttpServer(resolve, reject, retries - 1));
241
+ } else {
242
+ reject(err);
243
+ }
244
+ });
245
+ server.listen(PORT, () => {
246
+ console.error(`[ai-tools] HTTP server listening on port ${PORT}`);
247
+ resolve();
248
+ });
249
+ }
250
+ function startHttpServer() {
251
+ return new Promise((resolve, reject) => {
252
+ createHttpServer(resolve, reject);
253
+ });
254
+ }
255
+ const BASE_FIELDS = /* @__PURE__ */ new Set(["id", "name", "type", "childrenIds"]);
256
+ function filterFields(data, fields) {
257
+ if (!fields || fields.length === 0)
258
+ return data;
259
+ const result = {};
260
+ const fieldSet = /* @__PURE__ */ new Set([...BASE_FIELDS, ...fields]);
261
+ for (const [key, value] of Object.entries(data)) {
262
+ if (fieldSet.has(key))
263
+ result[key] = value;
264
+ }
265
+ return result;
266
+ }
267
+ async function startMcpServer() {
268
+ const server = new Server(
269
+ { name: "ui-differ-ai-tools", version: "0.2.0" },
270
+ { capabilities: { tools: {} } }
271
+ );
272
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
273
+ tools: [
274
+ {
275
+ name: "get_selected_node",
276
+ description: "获取 MasterGo 当前选中节点的属性(快照语义,无需 round-trip)",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ fields: {
281
+ type: "array",
282
+ items: { type: "string" },
283
+ description: "要获取的字段名列表。不传则返回全部字段。常用场景见 skill 指导。始终包含基础字段:id, name, type, childrenIds。"
284
+ }
285
+ }
286
+ }
287
+ },
288
+ {
289
+ name: "get_node_by_id",
290
+ description: "按 nodeId 从 MasterGo 插件查询节点属性(同步阻塞,等待插件响应)",
291
+ inputSchema: {
292
+ type: "object",
293
+ properties: {
294
+ nodeId: {
295
+ type: "string",
296
+ description: "MasterGo 节点 ID"
297
+ },
298
+ fields: {
299
+ type: "array",
300
+ items: { type: "string" },
301
+ description: "要获取的字段名列表。不传则返回全部字段。常用场景见 skill 指导。始终包含基础字段:id, name, type, childrenIds。"
302
+ }
303
+ },
304
+ required: ["nodeId"]
305
+ }
306
+ }
307
+ ]
308
+ }));
309
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
310
+ const { name, arguments: args } = request.params;
311
+ const rawFields = args ? args.fields : void 0;
312
+ const fields = Array.isArray(rawFields) ? rawFields : void 0;
313
+ if (name === "get_selected_node") {
314
+ const snapshot = selectionCache.get();
315
+ const selected = snapshot.selected;
316
+ const filtered = selected ? { ...snapshot, selected: filterFields(selected, fields) } : snapshot;
317
+ return {
318
+ content: [
319
+ {
320
+ type: "text",
321
+ text: JSON.stringify(filtered, null, 2)
322
+ }
323
+ ]
324
+ };
325
+ }
326
+ if (name === "get_node_by_id") {
327
+ if (!args || typeof args.nodeId !== "string") {
328
+ throw new McpError(ErrorCode.InvalidParams, "nodeId is required and must be a string");
329
+ }
330
+ const nodeId = args.nodeId;
331
+ try {
332
+ const result = await pendingRequests.create(nodeId, fields);
333
+ const filtered = filterFields(result, fields);
334
+ return {
335
+ content: [
336
+ {
337
+ type: "text",
338
+ text: JSON.stringify(filtered, null, 2)
339
+ }
340
+ ]
341
+ };
342
+ } catch (err) {
343
+ const msg = err instanceof Error ? err.message : String(err);
344
+ throw new McpError(ErrorCode.InternalError, msg);
345
+ }
346
+ }
347
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
348
+ });
349
+ const transport = new StdioServerTransport();
350
+ await server.connect(transport);
351
+ console.error("[ai-tools] MCP server connected via stdio");
352
+ }
353
+ async function main() {
354
+ await startHttpServer();
355
+ await startMcpServer();
356
+ }
357
+ main().catch((err) => {
358
+ const msg = err instanceof Error ? err.message : String(err);
359
+ console.error("[ai-tools] Fatal error:", msg);
360
+ process.exit(1);
361
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@jacob-z/zz-ui-brigde-mcp",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "zz-ui-brigde-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "scripts": {
16
+ "build": "vite build",
17
+ "prepublishOnly": "pnpm build",
18
+ "mcp:server": "tsx src/index.ts"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.11.0",
22
+ "uuid": "^11.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "vite": "^6.4.1"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }