@eyejack-creator/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.
- package/README.md +42 -0
- package/dist/index.js +962 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @eyejack-creator/mcp
|
|
2
|
+
|
|
3
|
+
EyeJack Creator MCP server — connect **Claude Code / Codex / Cursor** to a live EyeJack editing session. Phase 3 of the agentic roadmap: **read-only** (scene inspection); writes, screenshots, and live sync arrive in later phases.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Claude Code ──stdio──► @eyejack/mcp ──► @eyejack/creator-api ──► AppSync (agent session token)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Connect
|
|
10
|
+
|
|
11
|
+
1. Open your artwork in the EyeJack Creator and press **Connect AI** — it mints a short-lived session token scoped to *that artwork only* and shows the exact command:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add eyejack \
|
|
15
|
+
--env EYEJACK_SESSION_TOKEN=<token> \
|
|
16
|
+
--env EYEJACK_STAGE=dev \
|
|
17
|
+
-- npx -y @eyejack-creator/mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Other MCP clients consume the same `{command, args, env}` triple — Cursor (`.cursor/mcp.json` or Settings → MCP), Claude Desktop (`claude_desktop_config.json`), Codex (`~/.codex/config.toml` `mcp_servers`).
|
|
21
|
+
|
|
22
|
+
2. Ask away: *"What's in my scene?" · "Which assets aren't placed yet?" · "How is the layout arranged?"*
|
|
23
|
+
|
|
24
|
+
The token lives as long as your editor tab (the tab heartbeats it; closing the tab lets it lapse within minutes). Pressing Connect AI anywhere supersedes the previous session — one active AI session per account.
|
|
25
|
+
|
|
26
|
+
## Tools (v0)
|
|
27
|
+
|
|
28
|
+
| Tool | Returns |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `get_scene` | Full structured snapshot: metadata, parsed scene document (unity-space, degrees), `sceneRevision`, referenced files |
|
|
31
|
+
| `list_assets` | The artwork's asset files with `usedInScene` resolution |
|
|
32
|
+
| `get_session_status` | Session scope, active state, expiry |
|
|
33
|
+
|
|
34
|
+
Tool descriptions embed the scene-format contract so models handle coordinates/conventions correctly without external docs.
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm --filter @eyejack-creator/mcp build
|
|
40
|
+
# End-to-end smoke (spawns the server over stdio like a real client):
|
|
41
|
+
EYEJACK_SESSION_TOKEN=<token> node examples/smoke-client.mjs
|
|
42
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// ../creator-api/dist/transport.js
|
|
8
|
+
var GraphQLRequestError = class extends Error {
|
|
9
|
+
constructor(message, errors, data) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.errors = errors;
|
|
12
|
+
this.data = data;
|
|
13
|
+
this.name = "GraphQLRequestError";
|
|
14
|
+
}
|
|
15
|
+
/** First AppSync errorType, e.g. 'UnauthorizedException' or 'SceneRevisionConflict'. */
|
|
16
|
+
get errorType() {
|
|
17
|
+
return this.errors[0]?.errorType;
|
|
18
|
+
}
|
|
19
|
+
/** Payload attached via $util.error's data/errorInfo args (may be absent). */
|
|
20
|
+
get errorData() {
|
|
21
|
+
const first = this.errors[0];
|
|
22
|
+
return first?.data ?? first?.errorInfo ?? void 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var TransportError = class extends Error {
|
|
26
|
+
constructor(message, status, bodyText) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.bodyText = bodyText;
|
|
30
|
+
this.name = "TransportError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var GraphQLClient = class {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.endpoint = options.endpoint;
|
|
36
|
+
this.auth = options.auth;
|
|
37
|
+
const impl = options.fetchImpl ?? globalThis.fetch;
|
|
38
|
+
if (!impl) {
|
|
39
|
+
throw new Error("GraphQLClient: no fetch available \u2014 pass fetchImpl (Node < 18?)");
|
|
40
|
+
}
|
|
41
|
+
this.fetchImpl = impl.bind(globalThis);
|
|
42
|
+
}
|
|
43
|
+
/** Execute a query/mutation and return `data` (throws GraphQLRequestError / TransportError). */
|
|
44
|
+
async execute(query, variables) {
|
|
45
|
+
const body = JSON.stringify(variables === void 0 ? { query } : { query, variables });
|
|
46
|
+
const baseHeaders = { "Content-Type": "application/json" };
|
|
47
|
+
const authHeaders = await this.auth.authorize({
|
|
48
|
+
url: this.endpoint,
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: baseHeaders,
|
|
51
|
+
body
|
|
52
|
+
});
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = await this.fetchImpl(this.endpoint, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { ...baseHeaders, ...authHeaders },
|
|
58
|
+
body
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new TransportError(`Network error calling ${this.endpoint}: ${err.message}`);
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const text = await response.text().catch(() => "");
|
|
65
|
+
throw new TransportError(`HTTP ${response.status} from GraphQL endpoint`, response.status, text);
|
|
66
|
+
}
|
|
67
|
+
const json = await response.json();
|
|
68
|
+
if (json.errors && json.errors.length > 0) {
|
|
69
|
+
const first = json.errors[0];
|
|
70
|
+
throw new GraphQLRequestError(first?.message ?? "GraphQL request failed", json.errors, json.data);
|
|
71
|
+
}
|
|
72
|
+
if (json.data === void 0 || json.data === null) {
|
|
73
|
+
throw new GraphQLRequestError("GraphQL response contained no data", []);
|
|
74
|
+
}
|
|
75
|
+
return json.data;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ../creator-api/dist/realtime.js
|
|
80
|
+
var KA_GRACE_MS = 5 * 6e4;
|
|
81
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
82
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
83
|
+
var AppSyncRealtimeClient = class {
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.ws = null;
|
|
86
|
+
this.connecting = null;
|
|
87
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
88
|
+
this.nextId = 1;
|
|
89
|
+
this.kaTimeoutMs = KA_GRACE_MS;
|
|
90
|
+
this.kaTimer = null;
|
|
91
|
+
this.reconnectAttempt = 0;
|
|
92
|
+
this.closedByUser = false;
|
|
93
|
+
const url = new URL(options.graphqlEndpoint);
|
|
94
|
+
this.httpHost = url.host;
|
|
95
|
+
this.realtimeUrl = `wss://${url.host.replace("appsync-api", "appsync-realtime-api")}${url.pathname}`;
|
|
96
|
+
this.auth = options.auth;
|
|
97
|
+
const impl = options.WebSocketImpl ?? globalThis.WebSocket;
|
|
98
|
+
if (!impl)
|
|
99
|
+
throw new Error("AppSyncRealtimeClient: no WebSocket available \u2014 pass WebSocketImpl");
|
|
100
|
+
this.WebSocketImpl = impl;
|
|
101
|
+
this.onTransportEvent = options.onTransportEvent ?? (() => {
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
subscribe(query, variables, handlers) {
|
|
105
|
+
const id = String(this.nextId++);
|
|
106
|
+
const sub = { id, query, variables, handlers, started: false };
|
|
107
|
+
this.subs.set(id, sub);
|
|
108
|
+
this.ensureConnected().then(() => this.startSub(sub)).catch((err) => handlers.error?.(err));
|
|
109
|
+
return {
|
|
110
|
+
unsubscribe: () => {
|
|
111
|
+
this.subs.delete(id);
|
|
112
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
113
|
+
this.ws.send(JSON.stringify({ id, type: "stop" }));
|
|
114
|
+
}
|
|
115
|
+
if (this.subs.size === 0)
|
|
116
|
+
this.teardown("idle");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Close the socket and stop reconnecting (subscriptions are NOT resumed). */
|
|
121
|
+
close() {
|
|
122
|
+
this.closedByUser = true;
|
|
123
|
+
this.teardown("closed-by-user");
|
|
124
|
+
}
|
|
125
|
+
async authHeader() {
|
|
126
|
+
const headers = await this.auth.authorize({
|
|
127
|
+
url: `https://${this.httpHost}/graphql`,
|
|
128
|
+
method: "WS",
|
|
129
|
+
headers: {},
|
|
130
|
+
body: "{}"
|
|
131
|
+
});
|
|
132
|
+
return { host: this.httpHost, ...headers };
|
|
133
|
+
}
|
|
134
|
+
b64(obj) {
|
|
135
|
+
const json = JSON.stringify(obj);
|
|
136
|
+
const btoaFn = globalThis.btoa;
|
|
137
|
+
if (btoaFn)
|
|
138
|
+
return btoaFn(unescape(encodeURIComponent(json)));
|
|
139
|
+
return Buffer.from(json, "utf8").toString("base64");
|
|
140
|
+
}
|
|
141
|
+
ensureConnected() {
|
|
142
|
+
if (this.ws && this.ws.readyState === 1)
|
|
143
|
+
return Promise.resolve();
|
|
144
|
+
if (this.connecting)
|
|
145
|
+
return this.connecting;
|
|
146
|
+
this.closedByUser = false;
|
|
147
|
+
this.connecting = new Promise((resolve, reject) => {
|
|
148
|
+
(async () => {
|
|
149
|
+
const auth = await this.authHeader();
|
|
150
|
+
const url = `${this.realtimeUrl}?header=${this.b64(auth)}&payload=${this.b64({})}`;
|
|
151
|
+
const ws = new this.WebSocketImpl(url, ["graphql-ws"]);
|
|
152
|
+
this.ws = ws;
|
|
153
|
+
const fail = (err) => {
|
|
154
|
+
this.connecting = null;
|
|
155
|
+
reject(err);
|
|
156
|
+
};
|
|
157
|
+
ws.onopen = () => ws.send(JSON.stringify({ type: "connection_init" }));
|
|
158
|
+
ws.onerror = (e) => {
|
|
159
|
+
this.onTransportEvent("socket-error", e);
|
|
160
|
+
fail(new Error("AppSync realtime socket error"));
|
|
161
|
+
};
|
|
162
|
+
ws.onclose = () => this.handleClose();
|
|
163
|
+
ws.onmessage = (event) => {
|
|
164
|
+
const msg = JSON.parse(String(event.data));
|
|
165
|
+
switch (msg.type) {
|
|
166
|
+
case "connection_ack": {
|
|
167
|
+
this.kaTimeoutMs = msg.payload?.connectionTimeoutMs ?? KA_GRACE_MS;
|
|
168
|
+
this.resetKaWatchdog();
|
|
169
|
+
this.reconnectAttempt = 0;
|
|
170
|
+
this.connecting = null;
|
|
171
|
+
resolve();
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "ka":
|
|
175
|
+
this.resetKaWatchdog();
|
|
176
|
+
break;
|
|
177
|
+
case "start_ack":
|
|
178
|
+
break;
|
|
179
|
+
case "data": {
|
|
180
|
+
const sub = msg.id ? this.subs.get(msg.id) : void 0;
|
|
181
|
+
const data = msg.payload?.data;
|
|
182
|
+
if (sub && data !== void 0)
|
|
183
|
+
sub.handlers.next(data);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "error":
|
|
187
|
+
case "connection_error": {
|
|
188
|
+
const sub = msg.id ? this.subs.get(msg.id) : void 0;
|
|
189
|
+
this.onTransportEvent("protocol-error", msg.payload);
|
|
190
|
+
if (sub)
|
|
191
|
+
sub.handlers.error?.(msg.payload);
|
|
192
|
+
else
|
|
193
|
+
fail(new Error(`AppSync realtime error: ${JSON.stringify(msg.payload)}`));
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "complete":
|
|
197
|
+
if (msg.id)
|
|
198
|
+
this.subs.delete(msg.id);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
})().catch((err) => {
|
|
203
|
+
this.connecting = null;
|
|
204
|
+
reject(err);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
return this.connecting;
|
|
208
|
+
}
|
|
209
|
+
async startSub(sub) {
|
|
210
|
+
if (!this.ws || this.ws.readyState !== 1 || sub.started)
|
|
211
|
+
return;
|
|
212
|
+
const auth = await this.authHeader();
|
|
213
|
+
if (!this.subs.has(sub.id))
|
|
214
|
+
return;
|
|
215
|
+
sub.started = true;
|
|
216
|
+
this.ws.send(JSON.stringify({
|
|
217
|
+
id: sub.id,
|
|
218
|
+
type: "start",
|
|
219
|
+
payload: {
|
|
220
|
+
data: JSON.stringify({ query: sub.query, variables: sub.variables ?? {} }),
|
|
221
|
+
extensions: { authorization: auth }
|
|
222
|
+
}
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
resetKaWatchdog() {
|
|
226
|
+
if (this.kaTimer)
|
|
227
|
+
clearTimeout(this.kaTimer);
|
|
228
|
+
this.kaTimer = setTimeout(() => {
|
|
229
|
+
this.onTransportEvent("ka-timeout");
|
|
230
|
+
try {
|
|
231
|
+
this.ws?.close();
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}, this.kaTimeoutMs);
|
|
235
|
+
}
|
|
236
|
+
handleClose() {
|
|
237
|
+
if (this.kaTimer)
|
|
238
|
+
clearTimeout(this.kaTimer);
|
|
239
|
+
this.kaTimer = null;
|
|
240
|
+
this.ws = null;
|
|
241
|
+
this.connecting = null;
|
|
242
|
+
for (const sub of this.subs.values())
|
|
243
|
+
sub.started = false;
|
|
244
|
+
if (this.closedByUser || this.subs.size === 0)
|
|
245
|
+
return;
|
|
246
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS);
|
|
247
|
+
this.reconnectAttempt += 1;
|
|
248
|
+
this.onTransportEvent("reconnecting", { attempt: this.reconnectAttempt, delayMs: delay });
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
if (this.subs.size === 0)
|
|
251
|
+
return;
|
|
252
|
+
this.ensureConnected().then(() => {
|
|
253
|
+
for (const sub of this.subs.values())
|
|
254
|
+
void this.startSub(sub);
|
|
255
|
+
}).catch((err) => this.onTransportEvent("reconnect-failed", err));
|
|
256
|
+
}, delay);
|
|
257
|
+
}
|
|
258
|
+
teardown(reason) {
|
|
259
|
+
this.onTransportEvent("teardown", reason);
|
|
260
|
+
if (this.kaTimer)
|
|
261
|
+
clearTimeout(this.kaTimer);
|
|
262
|
+
this.kaTimer = null;
|
|
263
|
+
const ws = this.ws;
|
|
264
|
+
this.ws = null;
|
|
265
|
+
this.connecting = null;
|
|
266
|
+
try {
|
|
267
|
+
ws?.close();
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// ../creator-api/dist/stages.js
|
|
274
|
+
var STAGES = {
|
|
275
|
+
dev: {
|
|
276
|
+
graphqlEndpoint: "https://4mtyioftuncyll6x4xlsaafjt4.appsync-api.us-east-1.amazonaws.com/graphql",
|
|
277
|
+
region: "us-east-1"
|
|
278
|
+
},
|
|
279
|
+
prd: {
|
|
280
|
+
graphqlEndpoint: "https://xarmlusdjfdzjkab4ptvf5yqye.appsync-api.us-east-1.amazonaws.com/graphql",
|
|
281
|
+
region: "us-east-1"
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// ../creator-api/dist/domains/agent.js
|
|
286
|
+
var CREATE_AGENT_SESSION = `
|
|
287
|
+
mutation CreateAgentSession($artworkId: ID!) {
|
|
288
|
+
createAgentSession(artworkId: $artworkId) {
|
|
289
|
+
sessionId
|
|
290
|
+
token
|
|
291
|
+
artworkId
|
|
292
|
+
expiresAt
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
var AGENT_SESSION_HEARTBEAT = `
|
|
297
|
+
mutation AgentSessionHeartbeat($sessionId: ID!) {
|
|
298
|
+
agentSessionHeartbeat(sessionId: $sessionId) {
|
|
299
|
+
sessionId
|
|
300
|
+
artworkId
|
|
301
|
+
status
|
|
302
|
+
expiresAt
|
|
303
|
+
lastHeartbeat
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
var END_AGENT_SESSION = `
|
|
308
|
+
mutation EndAgentSession($sessionId: ID!) {
|
|
309
|
+
endAgentSession(sessionId: $sessionId) {
|
|
310
|
+
sessionId
|
|
311
|
+
artworkId
|
|
312
|
+
status
|
|
313
|
+
expiresAt
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
var AGENT_GET_SESSION = `
|
|
318
|
+
query AgentGetSession {
|
|
319
|
+
agentGetSession {
|
|
320
|
+
sessionId
|
|
321
|
+
artworkId
|
|
322
|
+
userid
|
|
323
|
+
status
|
|
324
|
+
expiresAt
|
|
325
|
+
lastHeartbeat
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
`;
|
|
329
|
+
var AGENT_GET_SCENE = `
|
|
330
|
+
query AgentGetScene {
|
|
331
|
+
agentGetScene {
|
|
332
|
+
artworkId
|
|
333
|
+
name
|
|
334
|
+
status
|
|
335
|
+
sceneConfig
|
|
336
|
+
sceneRevision
|
|
337
|
+
modified
|
|
338
|
+
files {
|
|
339
|
+
type
|
|
340
|
+
path
|
|
341
|
+
checksum
|
|
342
|
+
fileID
|
|
343
|
+
size
|
|
344
|
+
width
|
|
345
|
+
height
|
|
346
|
+
pz
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
var AgentApi = class {
|
|
352
|
+
constructor(client2) {
|
|
353
|
+
this.client = client2;
|
|
354
|
+
}
|
|
355
|
+
// --- Browser-side (Cognito) session lifecycle ---
|
|
356
|
+
async createSession(artworkId) {
|
|
357
|
+
const data = await this.client.execute(CREATE_AGENT_SESSION, { artworkId });
|
|
358
|
+
return data.createAgentSession;
|
|
359
|
+
}
|
|
360
|
+
async heartbeat(sessionId) {
|
|
361
|
+
const data = await this.client.execute(AGENT_SESSION_HEARTBEAT, { sessionId });
|
|
362
|
+
return data.agentSessionHeartbeat;
|
|
363
|
+
}
|
|
364
|
+
async endSession(sessionId) {
|
|
365
|
+
const data = await this.client.execute(END_AGENT_SESSION, { sessionId });
|
|
366
|
+
return data.endAgentSession;
|
|
367
|
+
}
|
|
368
|
+
// --- Agent-side (session token) scoped reads ---
|
|
369
|
+
async getSession() {
|
|
370
|
+
const data = await this.client.execute(AGENT_GET_SESSION);
|
|
371
|
+
return data.agentGetSession;
|
|
372
|
+
}
|
|
373
|
+
async getScene() {
|
|
374
|
+
const data = await this.client.execute(AGENT_GET_SCENE);
|
|
375
|
+
return data.agentGetScene;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// ../creator-api/dist/documents/artworks.js
|
|
380
|
+
var ARTWORK_FIELDS = `
|
|
381
|
+
id
|
|
382
|
+
userid
|
|
383
|
+
artworkType
|
|
384
|
+
name
|
|
385
|
+
description
|
|
386
|
+
status
|
|
387
|
+
files {
|
|
388
|
+
type
|
|
389
|
+
path
|
|
390
|
+
checksum
|
|
391
|
+
fileID
|
|
392
|
+
size
|
|
393
|
+
width
|
|
394
|
+
height
|
|
395
|
+
pz
|
|
396
|
+
}
|
|
397
|
+
created
|
|
398
|
+
modified
|
|
399
|
+
downloadsCount
|
|
400
|
+
viewsCount
|
|
401
|
+
enabled
|
|
402
|
+
publishEnabled
|
|
403
|
+
redirectLaunchV3
|
|
404
|
+
ctaLinkUrl
|
|
405
|
+
ctaLinkText
|
|
406
|
+
creatorVersion
|
|
407
|
+
animationHasAlpha
|
|
408
|
+
animationVideoOrientation
|
|
409
|
+
sceneConfig
|
|
410
|
+
sceneRevision
|
|
411
|
+
`;
|
|
412
|
+
var GET_ARTWORK = `
|
|
413
|
+
query GetArtwork($id: ID!) {
|
|
414
|
+
getArtwork(id: $id) {
|
|
415
|
+
${ARTWORK_FIELDS}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
`;
|
|
419
|
+
var GET_ARTWORKS = `
|
|
420
|
+
query GetArtworks($ids: [ID]) {
|
|
421
|
+
getArtworks(ids: $ids) {
|
|
422
|
+
${ARTWORK_FIELDS}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
`;
|
|
426
|
+
var GET_ARTWORKS_BY_USERID = `
|
|
427
|
+
query GetArtworksByUserid($userid: ID!, $after: String) {
|
|
428
|
+
getArtworksByUserid(userid: $userid, after: $after) {
|
|
429
|
+
nextToken
|
|
430
|
+
items {
|
|
431
|
+
${ARTWORK_FIELDS}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
`;
|
|
436
|
+
var CREATE_ARTWORK = `
|
|
437
|
+
mutation CreateArtwork($input: CreateArtworkInput!) {
|
|
438
|
+
createArtwork(input: $input) {
|
|
439
|
+
id
|
|
440
|
+
status
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
`;
|
|
444
|
+
var UPDATE_ARTWORK = `
|
|
445
|
+
mutation UpdateArtwork($input: UpdateArtworkInput!) {
|
|
446
|
+
updateArtwork(input: $input) {
|
|
447
|
+
id
|
|
448
|
+
userid
|
|
449
|
+
name
|
|
450
|
+
description
|
|
451
|
+
status
|
|
452
|
+
creatorVersion
|
|
453
|
+
animationHasAlpha
|
|
454
|
+
animationVideoOrientation
|
|
455
|
+
redirectLaunchV3
|
|
456
|
+
modified
|
|
457
|
+
sceneRevision
|
|
458
|
+
sceneConfig
|
|
459
|
+
files {
|
|
460
|
+
type
|
|
461
|
+
path
|
|
462
|
+
checksum
|
|
463
|
+
fileID
|
|
464
|
+
size
|
|
465
|
+
width
|
|
466
|
+
height
|
|
467
|
+
pz
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
`;
|
|
472
|
+
var UPDATE_ARTWORK_SCENE = `
|
|
473
|
+
mutation UpdateArtworkScene($input: UpdateArtworkSceneInput!) {
|
|
474
|
+
updateArtworkScene(input: $input) {
|
|
475
|
+
id
|
|
476
|
+
userid
|
|
477
|
+
sceneConfig
|
|
478
|
+
sceneRevision
|
|
479
|
+
modified
|
|
480
|
+
ops
|
|
481
|
+
originId
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
`;
|
|
485
|
+
var ON_UPDATE_ARTWORK = `
|
|
486
|
+
subscription OnUpdateArtwork($userid: ID!) {
|
|
487
|
+
onUpdateArtwork(userid: $userid) {
|
|
488
|
+
id
|
|
489
|
+
userid
|
|
490
|
+
name
|
|
491
|
+
description
|
|
492
|
+
status
|
|
493
|
+
publishEnabled
|
|
494
|
+
viewsCount
|
|
495
|
+
downloadsCount
|
|
496
|
+
modified
|
|
497
|
+
sceneConfig
|
|
498
|
+
sceneRevision
|
|
499
|
+
files {
|
|
500
|
+
type
|
|
501
|
+
path
|
|
502
|
+
checksum
|
|
503
|
+
fileID
|
|
504
|
+
size
|
|
505
|
+
width
|
|
506
|
+
height
|
|
507
|
+
pz
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
var ON_ARTWORK_SCENE = `
|
|
513
|
+
subscription OnArtworkScene($id: ID!) {
|
|
514
|
+
onArtworkScene(id: $id) {
|
|
515
|
+
id
|
|
516
|
+
userid
|
|
517
|
+
sceneConfig
|
|
518
|
+
sceneRevision
|
|
519
|
+
modified
|
|
520
|
+
ops
|
|
521
|
+
originId
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
`;
|
|
525
|
+
var PUBLISH_ARTWORK = `
|
|
526
|
+
mutation PublishArtwork($id: ID!) {
|
|
527
|
+
publishArtwork(id: $id) {
|
|
528
|
+
id
|
|
529
|
+
status
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
`;
|
|
533
|
+
var UNPUBLISH_ARTWORK = `
|
|
534
|
+
mutation UnpublishArtwork($id: ID!) {
|
|
535
|
+
unpublishArtwork(id: $id) {
|
|
536
|
+
id
|
|
537
|
+
status
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
`;
|
|
541
|
+
var DELETE_ARTWORK = `
|
|
542
|
+
mutation DeleteArtwork($input: DeleteArtworkInput!) {
|
|
543
|
+
deleteArtwork(input: $input) {
|
|
544
|
+
id
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
`;
|
|
548
|
+
|
|
549
|
+
// ../creator-api/dist/domains/artworks.js
|
|
550
|
+
var ArtworksApi = class {
|
|
551
|
+
constructor(client2, realtime) {
|
|
552
|
+
this.client = client2;
|
|
553
|
+
this.realtime = realtime;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Live artwork-record updates for a user (fattened onUpdateArtwork) over the
|
|
557
|
+
* SDK's realtime client. Payload fields are null for writers whose mutation
|
|
558
|
+
* selection didn't include them (e.g. creator-electron) — guard accordingly.
|
|
559
|
+
*/
|
|
560
|
+
watchUser(userid, handlers) {
|
|
561
|
+
if (!this.realtime)
|
|
562
|
+
throw new Error("ArtworksApi.watchUser: realtime client not configured");
|
|
563
|
+
return this.realtime().subscribe(ON_UPDATE_ARTWORK, { userid }, {
|
|
564
|
+
next: (data) => {
|
|
565
|
+
if (data?.onUpdateArtwork)
|
|
566
|
+
handlers.next(data.onUpdateArtwork);
|
|
567
|
+
},
|
|
568
|
+
error: handlers.error
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
async get(id) {
|
|
572
|
+
const data = await this.client.execute(GET_ARTWORK, { id });
|
|
573
|
+
return data.getArtwork;
|
|
574
|
+
}
|
|
575
|
+
/** Batch fetch by ids (DynamoDB BatchGetItem under the hood). Missing ids are dropped. */
|
|
576
|
+
async getMany(ids) {
|
|
577
|
+
if (ids.length === 0)
|
|
578
|
+
return [];
|
|
579
|
+
const data = await this.client.execute(GET_ARTWORKS, {
|
|
580
|
+
ids
|
|
581
|
+
});
|
|
582
|
+
return (data.getArtworks || []).filter((a) => !!a);
|
|
583
|
+
}
|
|
584
|
+
async listByUser(userid, after) {
|
|
585
|
+
const data = await this.client.execute(GET_ARTWORKS_BY_USERID, { userid, after: after ?? null });
|
|
586
|
+
return data.getArtworksByUserid;
|
|
587
|
+
}
|
|
588
|
+
async create(input) {
|
|
589
|
+
const data = await this.client.execute(CREATE_ARTWORK, { input });
|
|
590
|
+
return data.createArtwork;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Legacy whole-record update — the same mutation the editor has always used.
|
|
594
|
+
* The server bumps sceneRevision whenever input contains a non-null
|
|
595
|
+
* sceneConfig. Fields absent from the input are left untouched; fields
|
|
596
|
+
* explicitly null are REMOVED from the record (VTL semantics) — mirror the
|
|
597
|
+
* legacy payload shape when swapping call sites.
|
|
598
|
+
*/
|
|
599
|
+
async update(input) {
|
|
600
|
+
const data = await this.client.execute(UPDATE_ARTWORK, {
|
|
601
|
+
input
|
|
602
|
+
});
|
|
603
|
+
return data.updateArtwork;
|
|
604
|
+
}
|
|
605
|
+
async publish(id) {
|
|
606
|
+
const data = await this.client.execute(PUBLISH_ARTWORK, { id });
|
|
607
|
+
return data.publishArtwork;
|
|
608
|
+
}
|
|
609
|
+
async unpublish(id) {
|
|
610
|
+
const data = await this.client.execute(UNPUBLISH_ARTWORK, { id });
|
|
611
|
+
return data.unpublishArtwork;
|
|
612
|
+
}
|
|
613
|
+
/** Soft delete (status='deleted') via the backend's delete pipeline. */
|
|
614
|
+
async remove(id) {
|
|
615
|
+
const data = await this.client.execute(DELETE_ARTWORK, {
|
|
616
|
+
input: { id }
|
|
617
|
+
});
|
|
618
|
+
return data.deleteArtwork;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// ../creator-api/dist/domains/scene.js
|
|
623
|
+
var MAX_DOC_BYTES = 3e5;
|
|
624
|
+
function parseSceneConfig(raw) {
|
|
625
|
+
if (!raw)
|
|
626
|
+
return null;
|
|
627
|
+
let parsed;
|
|
628
|
+
try {
|
|
629
|
+
parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
630
|
+
} catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
634
|
+
return null;
|
|
635
|
+
const doc = parsed;
|
|
636
|
+
if (typeof doc.version !== "number")
|
|
637
|
+
return null;
|
|
638
|
+
if (!Array.isArray(doc.assets) || !Array.isArray(doc.scenes))
|
|
639
|
+
return null;
|
|
640
|
+
return parsed;
|
|
641
|
+
}
|
|
642
|
+
function stringifySceneDocument(doc) {
|
|
643
|
+
return JSON.stringify(doc);
|
|
644
|
+
}
|
|
645
|
+
function validateSceneDocument(doc, files) {
|
|
646
|
+
const issues = [];
|
|
647
|
+
if (typeof doc.version !== "number") {
|
|
648
|
+
issues.push({ level: "error", message: "version must be a number" });
|
|
649
|
+
}
|
|
650
|
+
if (!Array.isArray(doc.scenes) || doc.scenes.length === 0) {
|
|
651
|
+
issues.push({ level: "error", message: "scenes must be a non-empty array (only scenes[0] renders)" });
|
|
652
|
+
}
|
|
653
|
+
if (!Array.isArray(doc.assets)) {
|
|
654
|
+
issues.push({ level: "error", message: "assets must be an array" });
|
|
655
|
+
}
|
|
656
|
+
const root = doc.scenes?.[0];
|
|
657
|
+
if (root) {
|
|
658
|
+
if (!Array.isArray(root.children)) {
|
|
659
|
+
issues.push({ level: "error", message: "scenes[0].children must be an array" });
|
|
660
|
+
} else {
|
|
661
|
+
const fileIds = new Set((files ?? []).map((f) => f.fileID).filter(Boolean));
|
|
662
|
+
const assetIds = new Set((doc.assets ?? []).map((a) => a.assetID));
|
|
663
|
+
root.children.forEach((child, i) => {
|
|
664
|
+
for (const key of ["position", "rotation", "scale"]) {
|
|
665
|
+
const v = child[key];
|
|
666
|
+
if (!Array.isArray(v) || v.length !== 3 || v.some((n) => typeof n !== "number" || !Number.isFinite(n))) {
|
|
667
|
+
issues.push({ level: "error", message: `children[${i}].${key} must be [x,y,z] finite numbers` });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!child.sceneID) {
|
|
671
|
+
issues.push({ level: "error", message: `children[${i}].sceneID missing` });
|
|
672
|
+
}
|
|
673
|
+
if (!child.assetID) {
|
|
674
|
+
issues.push({ level: "error", message: `children[${i}].assetID missing` });
|
|
675
|
+
} else if (files && !fileIds.has(child.assetID) && !assetIds.has(child.assetID)) {
|
|
676
|
+
issues.push({
|
|
677
|
+
level: "warning",
|
|
678
|
+
message: `children[${i}].assetID "${child.assetID}" matches no files[].fileID or assets[].assetID \u2014 the viewer will skip it`
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const bytes = new TextEncoder().encode(stringifySceneDocument(doc)).length;
|
|
685
|
+
if (bytes > MAX_DOC_BYTES) {
|
|
686
|
+
issues.push({
|
|
687
|
+
level: "error",
|
|
688
|
+
message: `document is ${bytes} bytes; max ${MAX_DOC_BYTES} (DynamoDB item headroom)`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return issues;
|
|
692
|
+
}
|
|
693
|
+
var SceneApi = class {
|
|
694
|
+
constructor(client2, artworks, realtime) {
|
|
695
|
+
this.client = client2;
|
|
696
|
+
this.artworks = artworks;
|
|
697
|
+
this.realtime = realtime;
|
|
698
|
+
}
|
|
699
|
+
/** Live scene channel for one artwork (fat onArtworkScene) over the SDK's realtime client. */
|
|
700
|
+
watch(artworkId, handlers) {
|
|
701
|
+
if (!this.realtime)
|
|
702
|
+
throw new Error("SceneApi.watch: realtime client not configured");
|
|
703
|
+
return this.realtime().subscribe(ON_ARTWORK_SCENE, { id: artworkId }, {
|
|
704
|
+
next: (data) => {
|
|
705
|
+
if (data?.onArtworkScene)
|
|
706
|
+
handlers.next(data.onArtworkScene);
|
|
707
|
+
},
|
|
708
|
+
error: handlers.error
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/** Read the structured snapshot (doc + revision + files) for an artwork. */
|
|
712
|
+
async get(artworkId) {
|
|
713
|
+
const artwork = await this.artworks.get(artworkId);
|
|
714
|
+
if (!artwork)
|
|
715
|
+
return null;
|
|
716
|
+
return {
|
|
717
|
+
artworkId: artwork.id,
|
|
718
|
+
name: artwork.name ?? null,
|
|
719
|
+
doc: parseSceneConfig(artwork.sceneConfig),
|
|
720
|
+
raw: artwork.sceneConfig ?? null,
|
|
721
|
+
revision: artwork.sceneRevision ?? 0,
|
|
722
|
+
files: artwork.files ?? [],
|
|
723
|
+
artwork
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Revision-checked save (updateArtworkScene). The write is atomic: it lands
|
|
728
|
+
* only if the caller owns the artwork AND the stored revision equals
|
|
729
|
+
* expectedRevision; otherwise the SDK re-reads and returns a typed conflict
|
|
730
|
+
* for the caller to rebase on. Fans out to onArtworkScene subscribers.
|
|
731
|
+
*/
|
|
732
|
+
async save(params) {
|
|
733
|
+
const raw = typeof params.doc === "string" ? params.doc : stringifySceneDocument(params.doc);
|
|
734
|
+
if (typeof params.doc !== "string") {
|
|
735
|
+
const errors = validateSceneDocument(params.doc, params.validateAgainstFiles).filter((i) => i.level === "error");
|
|
736
|
+
if (errors.length > 0) {
|
|
737
|
+
throw new Error(`Refusing to save invalid scene document: ${errors.map((i) => i.message).join("; ")}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const data = await this.client.execute(UPDATE_ARTWORK_SCENE, {
|
|
742
|
+
input: {
|
|
743
|
+
id: params.artworkId,
|
|
744
|
+
expectedRevision: params.expectedRevision,
|
|
745
|
+
sceneConfig: raw,
|
|
746
|
+
ops: params.ops ?? null,
|
|
747
|
+
originId: params.originId ?? null
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
const payload = data.updateArtworkScene;
|
|
751
|
+
return {
|
|
752
|
+
conflict: false,
|
|
753
|
+
revision: payload.sceneRevision,
|
|
754
|
+
modified: payload.modified ?? null,
|
|
755
|
+
payload
|
|
756
|
+
};
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (err instanceof GraphQLRequestError && err.errorType === "SceneRevisionConflict") {
|
|
759
|
+
const current = await this.artworks.get(params.artworkId).catch(() => null);
|
|
760
|
+
if (!current)
|
|
761
|
+
return { conflict: true, exists: false, currentRevision: null };
|
|
762
|
+
return { conflict: true, exists: true, currentRevision: current.sceneRevision ?? 0 };
|
|
763
|
+
}
|
|
764
|
+
throw err;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Save a scene document through the LEGACY path (updateArtwork — no conflict
|
|
769
|
+
* check; the server still bumps sceneRevision). Kept for un-migrated flows;
|
|
770
|
+
* prefer save() everywhere new.
|
|
771
|
+
*/
|
|
772
|
+
async saveLegacy(artworkId, doc, options) {
|
|
773
|
+
const raw = typeof doc === "string" ? doc : stringifySceneDocument(doc);
|
|
774
|
+
if (typeof doc !== "string") {
|
|
775
|
+
const issues = validateSceneDocument(doc, options?.validateAgainstFiles).filter((i) => i.level === "error");
|
|
776
|
+
if (issues.length > 0) {
|
|
777
|
+
throw new Error(`Refusing to save invalid scene document: ${issues.map((i) => i.message).join("; ")}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return this.artworks.update({ id: artworkId, sceneConfig: raw });
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// ../creator-api/dist/client.js
|
|
785
|
+
function createCreatorClient(options) {
|
|
786
|
+
const endpoint = options.endpoint ?? (options.stage ? STAGES[options.stage].graphqlEndpoint : void 0);
|
|
787
|
+
if (!endpoint) {
|
|
788
|
+
throw new Error("createCreatorClient: provide either `stage` or `endpoint`");
|
|
789
|
+
}
|
|
790
|
+
const graphql = new GraphQLClient({ endpoint, auth: options.auth, fetchImpl: options.fetchImpl });
|
|
791
|
+
let realtimeInstance = null;
|
|
792
|
+
const getRealtime = () => {
|
|
793
|
+
if (!realtimeInstance) {
|
|
794
|
+
realtimeInstance = new AppSyncRealtimeClient({
|
|
795
|
+
graphqlEndpoint: endpoint,
|
|
796
|
+
auth: options.auth,
|
|
797
|
+
WebSocketImpl: options.WebSocketImpl,
|
|
798
|
+
onTransportEvent: options.onRealtimeEvent
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return realtimeInstance;
|
|
802
|
+
};
|
|
803
|
+
const agent = new AgentApi(graphql);
|
|
804
|
+
const artworks = new ArtworksApi(graphql, getRealtime);
|
|
805
|
+
const scene = new SceneApi(graphql, artworks, getRealtime);
|
|
806
|
+
return {
|
|
807
|
+
agent,
|
|
808
|
+
artworks,
|
|
809
|
+
scene,
|
|
810
|
+
graphql,
|
|
811
|
+
endpoint,
|
|
812
|
+
get realtime() {
|
|
813
|
+
return getRealtime();
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ../creator-api/dist/auth.js
|
|
819
|
+
var StaticTokenProvider = class {
|
|
820
|
+
constructor(token, headerName = "Authorization") {
|
|
821
|
+
this.token = token;
|
|
822
|
+
this.headerName = headerName;
|
|
823
|
+
}
|
|
824
|
+
async authorize() {
|
|
825
|
+
return { [this.headerName]: this.token };
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// src/config.ts
|
|
830
|
+
function loadConfig() {
|
|
831
|
+
const token = process.env.EYEJACK_SESSION_TOKEN?.trim();
|
|
832
|
+
if (!token) {
|
|
833
|
+
console.error(
|
|
834
|
+
'EYEJACK_SESSION_TOKEN is not set.\nOpen your artwork in the EyeJack Creator, press "Connect AI", and use the command it shows.'
|
|
835
|
+
);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
const stage = process.env.EYEJACK_STAGE?.trim() || "dev";
|
|
839
|
+
const endpoint = process.env.EYEJACK_GRAPHQL_ENDPOINT?.trim() || (STAGES[stage] ? STAGES[stage].graphqlEndpoint : void 0);
|
|
840
|
+
if (!endpoint) {
|
|
841
|
+
console.error(`Unknown EYEJACK_STAGE "${stage}" and no EYEJACK_GRAPHQL_ENDPOINT set.`);
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
return { token, endpoint, stage };
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/tools.ts
|
|
848
|
+
var SCENE_CONTRACT = `Scene document conventions (the stored EyeJack format):
|
|
849
|
+
- Coordinate space is unity-style left-handed; rotations are in DEGREES.
|
|
850
|
+
- Only scenes[0] is rendered ("root"); its children[] are the panels/models.
|
|
851
|
+
- Each child: { sceneID (stable uuid), assetID, position [x,y,z], rotation [x,y,z], scale [x,y,z] }.
|
|
852
|
+
- A child's assetID must equal a files[].fileID (the asset it displays).
|
|
853
|
+
- files[] entries carry type (image | video | video-alpha | glb | image-target | cover), path (CDN url), width, height.
|
|
854
|
+
- Assets are provisioned by the human in the browser for now \u2014 there is no upload tool yet.
|
|
855
|
+
- sceneRevision is the concurrency counter; every write bumps it (writes arrive in a later phase).`;
|
|
856
|
+
var REJECTED_HINT = 'Session token rejected (expired, ended, or superseded). Ask the user to press "Connect AI" in the EyeJack editor again and update EYEJACK_SESSION_TOKEN.';
|
|
857
|
+
function errorText(err) {
|
|
858
|
+
if (err instanceof TransportError && (err.status === 401 || err.status === 403)) {
|
|
859
|
+
return REJECTED_HINT;
|
|
860
|
+
}
|
|
861
|
+
if (err instanceof GraphQLRequestError) {
|
|
862
|
+
if ((err.errorType || "").includes("Unauthorized")) {
|
|
863
|
+
return REJECTED_HINT;
|
|
864
|
+
}
|
|
865
|
+
return `EyeJack API error: ${err.errorType || err.message}`;
|
|
866
|
+
}
|
|
867
|
+
return `Error: ${err.message}`;
|
|
868
|
+
}
|
|
869
|
+
var asText = (value) => ({
|
|
870
|
+
content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }]
|
|
871
|
+
});
|
|
872
|
+
var asError = (err) => ({
|
|
873
|
+
content: [{ type: "text", text: errorText(err) }],
|
|
874
|
+
isError: true
|
|
875
|
+
});
|
|
876
|
+
function registerTools(server2, client2) {
|
|
877
|
+
server2.tool(
|
|
878
|
+
"get_scene",
|
|
879
|
+
`Read the full structured snapshot of the connected EyeJack artwork: metadata, the parsed scene document, the current sceneRevision, and the asset files it references. This session's token is scoped to exactly one artwork \u2014 there is nothing to select. ${SCENE_CONTRACT}`,
|
|
880
|
+
async () => {
|
|
881
|
+
try {
|
|
882
|
+
const record = await client2.agent.getScene();
|
|
883
|
+
const doc = parseSceneConfig(record.sceneConfig);
|
|
884
|
+
return asText({
|
|
885
|
+
artworkId: record.artworkId,
|
|
886
|
+
name: record.name,
|
|
887
|
+
status: record.status,
|
|
888
|
+
sceneRevision: record.sceneRevision ?? 0,
|
|
889
|
+
modified: record.modified,
|
|
890
|
+
scene: doc,
|
|
891
|
+
sceneIsEmpty: !doc || !doc.scenes?.[0]?.children?.length,
|
|
892
|
+
files: (record.files || []).map((f) => ({
|
|
893
|
+
fileID: f.fileID,
|
|
894
|
+
type: f.type,
|
|
895
|
+
path: f.path,
|
|
896
|
+
width: f.width,
|
|
897
|
+
height: f.height
|
|
898
|
+
}))
|
|
899
|
+
});
|
|
900
|
+
} catch (err) {
|
|
901
|
+
return asError(err);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
);
|
|
905
|
+
server2.tool(
|
|
906
|
+
"list_assets",
|
|
907
|
+
"List the asset files (images, videos, 3D models) available on the connected artwork \u2014 the palette the scene's panels can reference via assetID == fileID. Read-only.",
|
|
908
|
+
async () => {
|
|
909
|
+
try {
|
|
910
|
+
const record = await client2.agent.getScene();
|
|
911
|
+
const doc = parseSceneConfig(record.sceneConfig);
|
|
912
|
+
const usedIds = new Set((doc?.scenes?.[0]?.children || []).map((c) => c.assetID));
|
|
913
|
+
return asText(
|
|
914
|
+
(record.files || []).map((f) => ({
|
|
915
|
+
fileID: f.fileID,
|
|
916
|
+
type: f.type,
|
|
917
|
+
width: f.width,
|
|
918
|
+
height: f.height,
|
|
919
|
+
path: f.path,
|
|
920
|
+
usedInScene: f.fileID ? usedIds.has(f.fileID) : false
|
|
921
|
+
}))
|
|
922
|
+
);
|
|
923
|
+
} catch (err) {
|
|
924
|
+
return asError(err);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
server2.tool(
|
|
929
|
+
"get_session_status",
|
|
930
|
+
"Inspect the agent session itself: which artwork it is scoped to, whether it is active, and when it expires (the editor tab heartbeats it while open; closing the tab lets it lapse).",
|
|
931
|
+
async () => {
|
|
932
|
+
try {
|
|
933
|
+
const session = await client2.agent.getSession();
|
|
934
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
935
|
+
return asText({
|
|
936
|
+
artworkId: session.artworkId,
|
|
937
|
+
status: session.status,
|
|
938
|
+
expiresAt: session.expiresAt,
|
|
939
|
+
expiresInSeconds: session.expiresAt ? session.expiresAt - now : null,
|
|
940
|
+
lastHeartbeat: session.lastHeartbeat
|
|
941
|
+
});
|
|
942
|
+
} catch (err) {
|
|
943
|
+
return asError(err);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/index.ts
|
|
950
|
+
var config = loadConfig();
|
|
951
|
+
var client = createCreatorClient({
|
|
952
|
+
endpoint: config.endpoint,
|
|
953
|
+
auth: new StaticTokenProvider(config.token)
|
|
954
|
+
});
|
|
955
|
+
var server = new McpServer({
|
|
956
|
+
name: "eyejack-creator",
|
|
957
|
+
version: "0.1.0"
|
|
958
|
+
});
|
|
959
|
+
registerTools(server, client);
|
|
960
|
+
var transport = new StdioServerTransport();
|
|
961
|
+
await server.connect(transport);
|
|
962
|
+
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014 read-only tools ready.`);
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eyejack-creator/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "EyeJack Creator MCP server — connect Claude Code / Codex / Cursor to a live EyeJack editing session.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"eyejack-mcp": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
23
|
+
"zod": "^3.23.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"esbuild": "^0.24.0",
|
|
27
|
+
"typescript": "^5.4.0",
|
|
28
|
+
"@eyejack/creator-api": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
32
|
+
"build": "npm run typecheck && rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node18 --outfile=dist/index.js --external:@modelcontextprotocol/sdk --external:zod --banner:js=\"#!/usr/bin/env node\"",
|
|
33
|
+
"smoke": "node examples/smoke-client.mjs"
|
|
34
|
+
}
|
|
35
|
+
}
|