batter-pixso-mcp 0.1.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.
@@ -0,0 +1,310 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ /* 贴近 Figma/Pixso 原生插件:白底、Inter 11px、细边框圆角控件、语义色状态 */
7
+ * {
8
+ box-sizing: border-box;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+ body {
13
+ font: 11px/1.5 Inter, "PingFang SC", "Microsoft YaHei", sans-serif;
14
+ background: #ffffff;
15
+ color: #1e1e1e;
16
+ padding: 8px 12px;
17
+ overflow: hidden;
18
+ user-select: none;
19
+ }
20
+ .row {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 6px;
24
+ white-space: nowrap;
25
+ }
26
+ .row + .row {
27
+ margin-top: 4px;
28
+ }
29
+ .grow {
30
+ flex: 1;
31
+ min-width: 0;
32
+ overflow: hidden;
33
+ text-overflow: ellipsis;
34
+ }
35
+ /* 连接状态行 */
36
+ .dot {
37
+ width: 8px;
38
+ height: 8px;
39
+ border-radius: 50%;
40
+ background: #b3b3b3;
41
+ flex-shrink: 0;
42
+ animation: pulse 1.6s ease-in-out infinite;
43
+ }
44
+ @keyframes pulse {
45
+ 0%, 100% { opacity: 1; }
46
+ 50% { opacity: 0.35; }
47
+ }
48
+ .dot.ok {
49
+ background: #14ae5c;
50
+ animation: none;
51
+ }
52
+ .dot.err {
53
+ background: #f24822;
54
+ animation: none;
55
+ }
56
+ #conn {
57
+ font-weight: 500;
58
+ color: #1e1e1e;
59
+ }
60
+ /* 端口输入 + 重连:Figma 风格次控件 */
61
+ #port {
62
+ width: 42px;
63
+ padding: 1px 4px;
64
+ font: inherit;
65
+ color: #1e1e1e;
66
+ background: #ffffff;
67
+ border: 1px solid #e6e6e6;
68
+ border-radius: 5px;
69
+ text-align: center;
70
+ }
71
+ #port:hover { border-color: #b3b3b3; }
72
+ #port:focus {
73
+ outline: none;
74
+ border-color: #0d99ff;
75
+ box-shadow: 0 0 0 1px #0d99ff;
76
+ }
77
+ #retry {
78
+ padding: 1px 10px;
79
+ font: inherit;
80
+ font-weight: 500;
81
+ color: #1e1e1e;
82
+ background: #ffffff;
83
+ border: 1px solid #e6e6e6;
84
+ border-radius: 5px;
85
+ cursor: pointer;
86
+ }
87
+ #retry:hover {
88
+ background: #f5f5f5;
89
+ border-color: #b3b3b3;
90
+ }
91
+ #retry:active { background: #ebebeb; }
92
+ /* 执行状态行:次级灰,忙碌时蓝色呼吸 */
93
+ #exec {
94
+ color: #7b7b7b;
95
+ }
96
+ #exec::before {
97
+ content: "▸ ";
98
+ color: #b3b3b3;
99
+ }
100
+ #exec.busy {
101
+ color: #0d99ff;
102
+ animation: pulse 1.2s ease-in-out infinite;
103
+ }
104
+ #exec.busy::before { color: #0d99ff; }
105
+ #exec.ok { color: #14ae5c; }
106
+ #exec.ok::before { color: #14ae5c; }
107
+ #exec.err { color: #f24822; }
108
+ #exec.err::before { color: #f24822; }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div class="row">
113
+ <span class="dot" id="dot"></span>
114
+ <span id="conn" class="grow">连接中…</span>
115
+ <input id="port" inputmode="numeric" spellcheck="false" title="服务端口" />
116
+ <button id="retry" type="button">重连</button>
117
+ </div>
118
+ <div class="row"><span id="exec" class="grow">空闲</span></div>
119
+ <script>
120
+ "use strict";
121
+ (() => {
122
+ // src/ui.ts
123
+ var DEFAULT_PORT = "3210";
124
+ var dotEl = document.getElementById("dot");
125
+ var connEl = document.getElementById("conn");
126
+ var execEl = document.getElementById("exec");
127
+ var portInput = document.getElementById("port");
128
+ var retryBtn = document.getElementById("retry");
129
+ var serverBase = `http://127.0.0.1:${DEFAULT_PORT}`;
130
+ function setConn(text, cls) {
131
+ connEl.textContent = text;
132
+ dotEl.className = `dot${cls ? ` ${cls}` : ""}`;
133
+ }
134
+ function setExec(text, cls) {
135
+ execEl.textContent = text;
136
+ execEl.className = `grow${cls ? ` ${cls}` : ""}`;
137
+ }
138
+ var sessionId = null;
139
+ var lastContext = null;
140
+ async function ensureSession() {
141
+ if (sessionId) return sessionId;
142
+ const res = await fetch(`${serverBase}/sessions/register`, {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify(lastContext ?? {})
146
+ });
147
+ if (!res.ok) throw new Error(`register failed: HTTP ${res.status}`);
148
+ const data = await res.json();
149
+ sessionId = data.sessionId;
150
+ return sessionId;
151
+ }
152
+ function sleep(ms) {
153
+ const { promise, resolve } = Promise.withResolvers();
154
+ setTimeout(resolve, ms);
155
+ return promise;
156
+ }
157
+ function report(payload) {
158
+ lastContext = payload;
159
+ void (async () => {
160
+ try {
161
+ let sid = await ensureSession();
162
+ let res = await postContext(sid, payload);
163
+ if (res.status === 404) {
164
+ sessionId = null;
165
+ sid = await ensureSession();
166
+ res = await postContext(sid, payload);
167
+ }
168
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
169
+ setConn(`\u5DF2\u8FDE\u63A5 \xB7 \u9009\u533A ${payload.selection?.length ?? 0}`, "ok");
170
+ } catch {
171
+ setConn("\u540C\u6B65\u5931\u8D25", "err");
172
+ }
173
+ })();
174
+ }
175
+ function postContext(sid, payload) {
176
+ return fetch(`${serverBase}/context?session=${sid}`, {
177
+ method: "POST",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify(payload)
180
+ });
181
+ }
182
+ function checkHealth() {
183
+ fetch(`${serverBase}/health`).then(() => setConn("\u5DF2\u8FDE\u63A5", "ok")).catch(() => setConn("\u8FDE\u63A5\u5931\u8D25", "err"));
184
+ }
185
+ var pendingCommands = /* @__PURE__ */ new Map();
186
+ function toBytes(raw) {
187
+ return new Uint8Array(raw);
188
+ }
189
+ async function uploadAsset(format, bytes) {
190
+ const res = await fetch(`${serverBase}/assets?format=${format}`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/octet-stream" },
193
+ body: bytes
194
+ });
195
+ if (!res.ok) throw new Error(`asset upload failed: HTTP ${res.status}`);
196
+ const data = await res.json();
197
+ if (!data.url) throw new Error("asset upload failed: \u54CD\u5E94\u7F3A\u5C11 url \u5B57\u6BB5");
198
+ return data.url;
199
+ }
200
+ async function handleCommand(command) {
201
+ setExec(`\u6267\u884C\u4E2D ${command.type}`, "busy");
202
+ const reply = await new Promise((resolve) => {
203
+ pendingCommands.set(command.id, resolve);
204
+ parent.postMessage({ pluginMessage: { type: "command", command } }, "*");
205
+ });
206
+ if (!reply.ok) {
207
+ await postResult(command.id, false, reply.error ?? "unknown error");
208
+ setExec(`\u5931\u8D25 ${command.type}`, "err");
209
+ return;
210
+ }
211
+ let result = reply.result;
212
+ const assets = [...reply.assets ?? []].sort((a, b) => b.key.length - a.key.length);
213
+ for (const asset of assets) {
214
+ const url = await uploadAsset(asset.format, toBytes(asset.bytes));
215
+ const text = JSON.stringify(result);
216
+ const quoted = JSON.stringify(asset.key);
217
+ result = JSON.parse(text.split(quoted).join(JSON.stringify(url)));
218
+ }
219
+ await postResult(command.id, true, result);
220
+ setExec(`\u5B8C\u6210 ${command.type}`, "ok");
221
+ }
222
+ async function postResult(id, ok, payload) {
223
+ const body = ok ? { ok: true, result: payload } : { ok: false, error: String(payload) };
224
+ await fetch(`${serverBase}/commands/${id}/result`, {
225
+ method: "POST",
226
+ headers: { "Content-Type": "application/json" },
227
+ body: JSON.stringify(body)
228
+ });
229
+ }
230
+ var currentES = null;
231
+ var channelMode = null;
232
+ async function startChannel() {
233
+ for (; ; ) {
234
+ try {
235
+ const sid = await ensureSession();
236
+ const es = new EventSource(`${serverBase}/commands/stream?session=${sid}`);
237
+ currentES = es;
238
+ channelMode = "sse";
239
+ es.onmessage = (e) => {
240
+ const { command } = JSON.parse(e.data);
241
+ void handleCommand(command);
242
+ };
243
+ es.onerror = () => {
244
+ if (es.readyState === EventSource.CLOSED) {
245
+ currentES = null;
246
+ channelMode = "poll";
247
+ void pollLoop();
248
+ }
249
+ };
250
+ return;
251
+ } catch {
252
+ await sleep(2e3);
253
+ }
254
+ }
255
+ }
256
+ async function pollLoop() {
257
+ for (; ; ) {
258
+ try {
259
+ const sid = await ensureSession();
260
+ const res = await fetch(`${serverBase}/commands/next?session=${sid}`);
261
+ if (res.status === 404) {
262
+ sessionId = null;
263
+ await sleep(500);
264
+ continue;
265
+ }
266
+ if (res.status === 204) continue;
267
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
268
+ const { command } = await res.json();
269
+ await handleCommand(command);
270
+ } catch {
271
+ await sleep(2e3);
272
+ }
273
+ }
274
+ }
275
+ function reconnect() {
276
+ const port = portInput.value.trim() || DEFAULT_PORT;
277
+ serverBase = `http://127.0.0.1:${port}`;
278
+ sessionId = null;
279
+ setConn("\u8FDE\u63A5\u4E2D\u2026");
280
+ if (channelMode === "sse") {
281
+ currentES?.close();
282
+ currentES = null;
283
+ channelMode = null;
284
+ void startChannel();
285
+ }
286
+ checkHealth();
287
+ parent.postMessage({ pluginMessage: { type: "pull" } }, "*");
288
+ }
289
+ retryBtn.addEventListener("click", reconnect);
290
+ window.onmessage = (event) => {
291
+ const msg = event.data?.pluginMessage;
292
+ if (msg?.type === "context") {
293
+ report(msg.payload);
294
+ return;
295
+ }
296
+ if (msg?.type === "command-result") {
297
+ const waiter = pendingCommands.get(msg.id);
298
+ pendingCommands.delete(msg.id);
299
+ waiter?.(msg);
300
+ }
301
+ };
302
+ portInput.value = DEFAULT_PORT;
303
+ portInput.addEventListener("change", reconnect);
304
+ parent.postMessage({ pluginMessage: { type: "pull" } }, "*");
305
+ checkHealth();
306
+ void startChannel();
307
+ })();
308
+ </script>
309
+ </body>
310
+ </html>
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "batter-pixso-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP bridge: Pixso plugin -> local MCP server -> Pixso remote MCP",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "exports": {
10
+ "./protocol": "./src/protocol.ts"
11
+ },
12
+ "bin": {
13
+ "batter-pixso-mcp": "dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "npm run typecheck && node build.mjs",
20
+ "prepack": "node ../../scripts/sync-plugin-dist.mjs",
21
+ "typecheck": "tsc --noEmit",
22
+ "dev": "tsx watch src/index.ts",
23
+ "start": "node dist/index.js"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.17.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.10.0",
30
+ "esbuild": "^0.24.0",
31
+ "tsx": "^4.19.0",
32
+ "typescript": "^5.7.0"
33
+ }
34
+ }