@eyejack-creator/mcp 0.1.0 → 0.3.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 +7 -4
- package/dist/index.js +486 -175
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @eyejack-creator/mcp
|
|
2
2
|
|
|
3
|
-
EyeJack Creator MCP server — connect **Claude Code / Codex / Cursor** to a live EyeJack editing session.
|
|
3
|
+
EyeJack Creator MCP server — connect **Claude Code / Codex / Cursor** to a live EyeJack editing session. Your AI reads the scene, **edits it** (revision-checked writes land in the open editor live, with a human-readable action log) and **sees it** — the screenshot tool renders the editor's current view back to the model, closing the read → edit → inspect loop.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
Claude Code ──stdio──► @eyejack/mcp ──► @eyejack/creator-api ──► AppSync (agent session token)
|
|
@@ -19,16 +19,19 @@ claude mcp add eyejack \
|
|
|
19
19
|
|
|
20
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
21
|
|
|
22
|
-
2. Ask away: *"What's in my scene?" · "
|
|
22
|
+
2. Ask away: *"What's in my scene?" · "Arrange the panels in a row" · "Duplicate the logo panel and place it on the left"* — edits appear in your open editor as they happen.
|
|
23
23
|
|
|
24
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
25
|
|
|
26
|
-
## Tools
|
|
26
|
+
## Tools
|
|
27
27
|
|
|
28
|
-
| Tool |
|
|
28
|
+
| Tool | Does |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `get_scene` | Full structured snapshot: metadata, parsed scene document (unity-space, degrees), `sceneRevision`, referenced files |
|
|
31
31
|
| `list_assets` | The artwork's asset files with `usedInScene` resolution |
|
|
32
|
+
| `update_scene` | Whole-document, revision-checked scene write — applied **live** in the open editor with your `ops` action log; conflicts return the current scene to rebase on |
|
|
33
|
+
| `publish_artwork` | Publish the artwork to its public launch URL (draft/disabled → published) |
|
|
34
|
+
| `screenshot` | JPEG of the editor's current view, rendered by the open tab and relayed over AppSync (auto-degraded under the transport cap) — the agent's eyes |
|
|
32
35
|
| `get_session_status` | Session scope, active state, expiry |
|
|
33
36
|
|
|
34
37
|
Tool descriptions embed the scene-format contract so models handle coordinates/conventions correctly without external docs.
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { WebSocket as WsWebSocket } from "ws";
|
|
6
7
|
|
|
7
8
|
// ../creator-api/dist/transport.js
|
|
8
9
|
var GraphQLRequestError = class extends Error {
|
|
@@ -282,100 +283,6 @@ var STAGES = {
|
|
|
282
283
|
}
|
|
283
284
|
};
|
|
284
285
|
|
|
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
286
|
// ../creator-api/dist/documents/artworks.js
|
|
380
287
|
var ARTWORK_FIELDS = `
|
|
381
288
|
id
|
|
@@ -546,79 +453,6 @@ var DELETE_ARTWORK = `
|
|
|
546
453
|
}
|
|
547
454
|
`;
|
|
548
455
|
|
|
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
456
|
// ../creator-api/dist/domains/scene.js
|
|
623
457
|
var MAX_DOC_BYTES = 3e5;
|
|
624
458
|
function parseSceneConfig(raw) {
|
|
@@ -781,6 +615,326 @@ var SceneApi = class {
|
|
|
781
615
|
}
|
|
782
616
|
};
|
|
783
617
|
|
|
618
|
+
// ../creator-api/dist/domains/agent.js
|
|
619
|
+
var CREATE_AGENT_SESSION = `
|
|
620
|
+
mutation CreateAgentSession($artworkId: ID!) {
|
|
621
|
+
createAgentSession(artworkId: $artworkId) {
|
|
622
|
+
sessionId
|
|
623
|
+
token
|
|
624
|
+
artworkId
|
|
625
|
+
expiresAt
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
`;
|
|
629
|
+
var AGENT_SESSION_HEARTBEAT = `
|
|
630
|
+
mutation AgentSessionHeartbeat($sessionId: ID!) {
|
|
631
|
+
agentSessionHeartbeat(sessionId: $sessionId) {
|
|
632
|
+
sessionId
|
|
633
|
+
artworkId
|
|
634
|
+
status
|
|
635
|
+
expiresAt
|
|
636
|
+
lastHeartbeat
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
`;
|
|
640
|
+
var END_AGENT_SESSION = `
|
|
641
|
+
mutation EndAgentSession($sessionId: ID!) {
|
|
642
|
+
endAgentSession(sessionId: $sessionId) {
|
|
643
|
+
sessionId
|
|
644
|
+
artworkId
|
|
645
|
+
status
|
|
646
|
+
expiresAt
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
`;
|
|
650
|
+
var AGENT_GET_SESSION = `
|
|
651
|
+
query AgentGetSession {
|
|
652
|
+
agentGetSession {
|
|
653
|
+
sessionId
|
|
654
|
+
artworkId
|
|
655
|
+
userid
|
|
656
|
+
status
|
|
657
|
+
expiresAt
|
|
658
|
+
lastHeartbeat
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
`;
|
|
662
|
+
var GET_ARTWORK_SCENE_BACKUP = `
|
|
663
|
+
query GetArtworkSceneBackup($artworkId: ID!) {
|
|
664
|
+
getArtworkSceneBackup(artworkId: $artworkId) {
|
|
665
|
+
artworkId
|
|
666
|
+
sessionId
|
|
667
|
+
sceneConfig
|
|
668
|
+
sceneRevision
|
|
669
|
+
createdAt
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
`;
|
|
673
|
+
var REQUEST_AGENT_ACTION = `
|
|
674
|
+
mutation RequestAgentAction($sessionId: ID!, $requestId: ID!, $actionType: String!, $payload: String) {
|
|
675
|
+
requestAgentAction(sessionId: $sessionId, requestId: $requestId, actionType: $actionType, payload: $payload) {
|
|
676
|
+
sessionId
|
|
677
|
+
requestId
|
|
678
|
+
actionType
|
|
679
|
+
payload
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
`;
|
|
683
|
+
var POST_AGENT_ACTION_RESULT = `
|
|
684
|
+
mutation PostAgentActionResult($sessionId: ID!, $requestId: ID!, $ok: Boolean!, $resultType: String, $payload: String, $error: String) {
|
|
685
|
+
postAgentActionResult(sessionId: $sessionId, requestId: $requestId, ok: $ok, resultType: $resultType, payload: $payload, error: $error) {
|
|
686
|
+
sessionId
|
|
687
|
+
requestId
|
|
688
|
+
ok
|
|
689
|
+
resultType
|
|
690
|
+
payload
|
|
691
|
+
error
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
`;
|
|
695
|
+
var ON_AGENT_ACTION = `
|
|
696
|
+
subscription OnAgentAction($sessionId: ID!) {
|
|
697
|
+
onAgentAction(sessionId: $sessionId) {
|
|
698
|
+
sessionId
|
|
699
|
+
requestId
|
|
700
|
+
actionType
|
|
701
|
+
payload
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
`;
|
|
705
|
+
var ON_AGENT_ACTION_RESULT = `
|
|
706
|
+
subscription OnAgentActionResult($sessionId: ID!) {
|
|
707
|
+
onAgentActionResult(sessionId: $sessionId) {
|
|
708
|
+
sessionId
|
|
709
|
+
requestId
|
|
710
|
+
ok
|
|
711
|
+
resultType
|
|
712
|
+
payload
|
|
713
|
+
error
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
`;
|
|
717
|
+
var AGENT_GET_SCENE = `
|
|
718
|
+
query AgentGetScene {
|
|
719
|
+
agentGetScene {
|
|
720
|
+
artworkId
|
|
721
|
+
name
|
|
722
|
+
status
|
|
723
|
+
sceneConfig
|
|
724
|
+
sceneRevision
|
|
725
|
+
modified
|
|
726
|
+
files {
|
|
727
|
+
type
|
|
728
|
+
path
|
|
729
|
+
checksum
|
|
730
|
+
fileID
|
|
731
|
+
size
|
|
732
|
+
width
|
|
733
|
+
height
|
|
734
|
+
pz
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
`;
|
|
739
|
+
var AgentApi = class {
|
|
740
|
+
constructor(client2, realtime) {
|
|
741
|
+
this.client = client2;
|
|
742
|
+
this.realtime = realtime;
|
|
743
|
+
}
|
|
744
|
+
// --- Browser-side (Cognito) session lifecycle ---
|
|
745
|
+
async createSession(artworkId) {
|
|
746
|
+
const data = await this.client.execute(CREATE_AGENT_SESSION, { artworkId });
|
|
747
|
+
return data.createAgentSession;
|
|
748
|
+
}
|
|
749
|
+
async heartbeat(sessionId) {
|
|
750
|
+
const data = await this.client.execute(AGENT_SESSION_HEARTBEAT, { sessionId });
|
|
751
|
+
return data.agentSessionHeartbeat;
|
|
752
|
+
}
|
|
753
|
+
async endSession(sessionId) {
|
|
754
|
+
const data = await this.client.execute(END_AGENT_SESSION, { sessionId });
|
|
755
|
+
return data.endAgentSession;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The scene snapshot createAgentSession took before the (most recent)
|
|
759
|
+
* session started — what "Revert AI changes" restores. Browser-side
|
|
760
|
+
* (Cognito, owner-checked); null when no session ever ran or the 7-day
|
|
761
|
+
* TTL reaped it.
|
|
762
|
+
*/
|
|
763
|
+
async getSceneBackup(artworkId) {
|
|
764
|
+
const data = await this.client.execute(GET_ARTWORK_SCENE_BACKUP, { artworkId });
|
|
765
|
+
return data.getArtworkSceneBackup;
|
|
766
|
+
}
|
|
767
|
+
// --- Agent-side (session token) scoped reads ---
|
|
768
|
+
async getSession() {
|
|
769
|
+
const data = await this.client.execute(AGENT_GET_SESSION);
|
|
770
|
+
return data.agentGetSession;
|
|
771
|
+
}
|
|
772
|
+
async getScene() {
|
|
773
|
+
const data = await this.client.execute(AGENT_GET_SCENE);
|
|
774
|
+
return data.agentGetScene;
|
|
775
|
+
}
|
|
776
|
+
// --- Agent-side (session token) writes (P4) ---
|
|
777
|
+
/**
|
|
778
|
+
* Revision-checked scene write with a session token. Same mutation and
|
|
779
|
+
* fan-out as SceneApi.save, but the conflict re-read uses agentGetScene
|
|
780
|
+
* (a session token cannot call getArtwork) and hands back the fresh record
|
|
781
|
+
* so the agent can rebase in one round trip. The backend hard-scopes the
|
|
782
|
+
* write to the session's artwork regardless of artworkId.
|
|
783
|
+
*/
|
|
784
|
+
async saveScene(params) {
|
|
785
|
+
const raw = typeof params.doc === "string" ? params.doc : stringifySceneDocument(params.doc);
|
|
786
|
+
if (typeof params.doc !== "string") {
|
|
787
|
+
const errors = validateSceneDocument(params.doc, params.validateAgainstFiles).filter((i) => i.level === "error");
|
|
788
|
+
if (errors.length > 0) {
|
|
789
|
+
throw new Error(`Refusing to save invalid scene document: ${errors.map((i) => i.message).join("; ")}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const data = await this.client.execute(UPDATE_ARTWORK_SCENE, {
|
|
794
|
+
input: {
|
|
795
|
+
id: params.artworkId,
|
|
796
|
+
expectedRevision: params.expectedRevision,
|
|
797
|
+
sceneConfig: raw,
|
|
798
|
+
ops: params.ops ?? null,
|
|
799
|
+
originId: params.originId ?? null
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
const payload = data.updateArtworkScene;
|
|
803
|
+
return { conflict: false, revision: payload.sceneRevision, modified: payload.modified ?? null, payload };
|
|
804
|
+
} catch (err) {
|
|
805
|
+
if (err instanceof GraphQLRequestError && err.errorType === "SceneRevisionConflict") {
|
|
806
|
+
const current = await this.getScene().catch(() => null);
|
|
807
|
+
return { conflict: true, currentRevision: current?.sceneRevision ?? null, current };
|
|
808
|
+
}
|
|
809
|
+
throw err;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Publish the session's artwork (guarded server-side: a session token can
|
|
814
|
+
* publish ONLY its own artwork; publish transitions draft/disabled →
|
|
815
|
+
* published — an already-published artwork rejects with a condition error).
|
|
816
|
+
*/
|
|
817
|
+
async publish(artworkId) {
|
|
818
|
+
const data = await this.client.execute(PUBLISH_ARTWORK, {
|
|
819
|
+
id: artworkId
|
|
820
|
+
});
|
|
821
|
+
return data.publishArtwork;
|
|
822
|
+
}
|
|
823
|
+
// --- Runtime action channel (P5) ---
|
|
824
|
+
/** AGENT side: fire an action request into the session's channel. */
|
|
825
|
+
async requestAction(params) {
|
|
826
|
+
const data = await this.client.execute(REQUEST_AGENT_ACTION, { ...params, payload: params.payload ?? null });
|
|
827
|
+
return data.requestAgentAction;
|
|
828
|
+
}
|
|
829
|
+
/** BROWSER side: answer an action request (session-owner-checked server-side). */
|
|
830
|
+
async postActionResult(params) {
|
|
831
|
+
const data = await this.client.execute(POST_AGENT_ACTION_RESULT, {
|
|
832
|
+
...params,
|
|
833
|
+
resultType: params.resultType ?? null,
|
|
834
|
+
payload: params.payload ?? null,
|
|
835
|
+
error: params.error ?? null
|
|
836
|
+
});
|
|
837
|
+
return data.postAgentActionResult;
|
|
838
|
+
}
|
|
839
|
+
/** BROWSER side: listen for the agent's action requests (subscribe-time owner check). */
|
|
840
|
+
watchActions(sessionId, handlers) {
|
|
841
|
+
if (!this.realtime)
|
|
842
|
+
throw new Error("AgentApi.watchActions: realtime client not configured");
|
|
843
|
+
return this.realtime().subscribe(ON_AGENT_ACTION, { sessionId }, {
|
|
844
|
+
next: (data) => {
|
|
845
|
+
if (data?.onAgentAction)
|
|
846
|
+
handlers.next(data.onAgentAction);
|
|
847
|
+
},
|
|
848
|
+
error: handlers.error
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
/** AGENT side: listen for the browser's results (subscribe-time session pin). */
|
|
852
|
+
watchActionResults(sessionId, handlers) {
|
|
853
|
+
if (!this.realtime)
|
|
854
|
+
throw new Error("AgentApi.watchActionResults: realtime client not configured");
|
|
855
|
+
return this.realtime().subscribe(ON_AGENT_ACTION_RESULT, { sessionId }, {
|
|
856
|
+
next: (data) => {
|
|
857
|
+
if (data?.onAgentActionResult)
|
|
858
|
+
handlers.next(data.onAgentActionResult);
|
|
859
|
+
},
|
|
860
|
+
error: handlers.error
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// ../creator-api/dist/domains/artworks.js
|
|
866
|
+
var ArtworksApi = class {
|
|
867
|
+
constructor(client2, realtime) {
|
|
868
|
+
this.client = client2;
|
|
869
|
+
this.realtime = realtime;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Live artwork-record updates for a user (fattened onUpdateArtwork) over the
|
|
873
|
+
* SDK's realtime client. Payload fields are null for writers whose mutation
|
|
874
|
+
* selection didn't include them (e.g. creator-electron) — guard accordingly.
|
|
875
|
+
*/
|
|
876
|
+
watchUser(userid, handlers) {
|
|
877
|
+
if (!this.realtime)
|
|
878
|
+
throw new Error("ArtworksApi.watchUser: realtime client not configured");
|
|
879
|
+
return this.realtime().subscribe(ON_UPDATE_ARTWORK, { userid }, {
|
|
880
|
+
next: (data) => {
|
|
881
|
+
if (data?.onUpdateArtwork)
|
|
882
|
+
handlers.next(data.onUpdateArtwork);
|
|
883
|
+
},
|
|
884
|
+
error: handlers.error
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async get(id) {
|
|
888
|
+
const data = await this.client.execute(GET_ARTWORK, { id });
|
|
889
|
+
return data.getArtwork;
|
|
890
|
+
}
|
|
891
|
+
/** Batch fetch by ids (DynamoDB BatchGetItem under the hood). Missing ids are dropped. */
|
|
892
|
+
async getMany(ids) {
|
|
893
|
+
if (ids.length === 0)
|
|
894
|
+
return [];
|
|
895
|
+
const data = await this.client.execute(GET_ARTWORKS, {
|
|
896
|
+
ids
|
|
897
|
+
});
|
|
898
|
+
return (data.getArtworks || []).filter((a) => !!a);
|
|
899
|
+
}
|
|
900
|
+
async listByUser(userid, after) {
|
|
901
|
+
const data = await this.client.execute(GET_ARTWORKS_BY_USERID, { userid, after: after ?? null });
|
|
902
|
+
return data.getArtworksByUserid;
|
|
903
|
+
}
|
|
904
|
+
async create(input) {
|
|
905
|
+
const data = await this.client.execute(CREATE_ARTWORK, { input });
|
|
906
|
+
return data.createArtwork;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Legacy whole-record update — the same mutation the editor has always used.
|
|
910
|
+
* The server bumps sceneRevision whenever input contains a non-null
|
|
911
|
+
* sceneConfig. Fields absent from the input are left untouched; fields
|
|
912
|
+
* explicitly null are REMOVED from the record (VTL semantics) — mirror the
|
|
913
|
+
* legacy payload shape when swapping call sites.
|
|
914
|
+
*/
|
|
915
|
+
async update(input) {
|
|
916
|
+
const data = await this.client.execute(UPDATE_ARTWORK, {
|
|
917
|
+
input
|
|
918
|
+
});
|
|
919
|
+
return data.updateArtwork;
|
|
920
|
+
}
|
|
921
|
+
async publish(id) {
|
|
922
|
+
const data = await this.client.execute(PUBLISH_ARTWORK, { id });
|
|
923
|
+
return data.publishArtwork;
|
|
924
|
+
}
|
|
925
|
+
async unpublish(id) {
|
|
926
|
+
const data = await this.client.execute(UNPUBLISH_ARTWORK, { id });
|
|
927
|
+
return data.unpublishArtwork;
|
|
928
|
+
}
|
|
929
|
+
/** Soft delete (status='deleted') via the backend's delete pipeline. */
|
|
930
|
+
async remove(id) {
|
|
931
|
+
const data = await this.client.execute(DELETE_ARTWORK, {
|
|
932
|
+
input: { id }
|
|
933
|
+
});
|
|
934
|
+
return data.deleteArtwork;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
784
938
|
// ../creator-api/dist/client.js
|
|
785
939
|
function createCreatorClient(options) {
|
|
786
940
|
const endpoint = options.endpoint ?? (options.stage ? STAGES[options.stage].graphqlEndpoint : void 0);
|
|
@@ -800,7 +954,7 @@ function createCreatorClient(options) {
|
|
|
800
954
|
}
|
|
801
955
|
return realtimeInstance;
|
|
802
956
|
};
|
|
803
|
-
const agent = new AgentApi(graphql);
|
|
957
|
+
const agent = new AgentApi(graphql, getRealtime);
|
|
804
958
|
const artworks = new ArtworksApi(graphql, getRealtime);
|
|
805
959
|
const scene = new SceneApi(graphql, artworks, getRealtime);
|
|
806
960
|
return {
|
|
@@ -841,10 +995,13 @@ function loadConfig() {
|
|
|
841
995
|
console.error(`Unknown EYEJACK_STAGE "${stage}" and no EYEJACK_GRAPHQL_ENDPOINT set.`);
|
|
842
996
|
process.exit(1);
|
|
843
997
|
}
|
|
844
|
-
|
|
998
|
+
const launchBaseUrl = stage === "prd" ? "https://launch.eyejack.io" : "https://launch.eyejack.dev";
|
|
999
|
+
return { token, endpoint, stage, launchBaseUrl };
|
|
845
1000
|
}
|
|
846
1001
|
|
|
847
1002
|
// src/tools.ts
|
|
1003
|
+
import { randomUUID } from "node:crypto";
|
|
1004
|
+
import { z } from "zod";
|
|
848
1005
|
var SCENE_CONTRACT = `Scene document conventions (the stored EyeJack format):
|
|
849
1006
|
- Coordinate space is unity-style left-handed; rotations are in DEGREES.
|
|
850
1007
|
- Only scenes[0] is rendered ("root"); its children[] are the panels/models.
|
|
@@ -852,7 +1009,7 @@ var SCENE_CONTRACT = `Scene document conventions (the stored EyeJack format):
|
|
|
852
1009
|
- A child's assetID must equal a files[].fileID (the asset it displays).
|
|
853
1010
|
- files[] entries carry type (image | video | video-alpha | glb | image-target | cover), path (CDN url), width, height.
|
|
854
1011
|
- 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
|
|
1012
|
+
- sceneRevision is the optimistic-concurrency counter: pass your latest read's revision as expectedRevision when writing; every successful write bumps it by 1.`;
|
|
856
1013
|
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
1014
|
function errorText(err) {
|
|
858
1015
|
if (err instanceof TransportError && (err.status === 401 || err.status === 403)) {
|
|
@@ -873,7 +1030,14 @@ var asError = (err) => ({
|
|
|
873
1030
|
content: [{ type: "text", text: errorText(err) }],
|
|
874
1031
|
isError: true
|
|
875
1032
|
});
|
|
876
|
-
function registerTools(server2, client2) {
|
|
1033
|
+
function registerTools(server2, client2, options) {
|
|
1034
|
+
let cachedArtworkId = null;
|
|
1035
|
+
const artworkId = async () => {
|
|
1036
|
+
if (!cachedArtworkId) {
|
|
1037
|
+
cachedArtworkId = (await client2.agent.getSession()).artworkId;
|
|
1038
|
+
}
|
|
1039
|
+
return cachedArtworkId;
|
|
1040
|
+
};
|
|
877
1041
|
server2.tool(
|
|
878
1042
|
"get_scene",
|
|
879
1043
|
`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}`,
|
|
@@ -925,6 +1089,146 @@ function registerTools(server2, client2) {
|
|
|
925
1089
|
}
|
|
926
1090
|
}
|
|
927
1091
|
);
|
|
1092
|
+
const updateSceneParams = {
|
|
1093
|
+
expectedRevision: z.number().int().min(0).describe("The sceneRevision your edit is based on, from your latest read."),
|
|
1094
|
+
scene: z.record(z.any()).describe(
|
|
1095
|
+
"The complete scene document to store: { version, settings?, assets, scenes }. Top-level keys are frozen to these; per-child additive keys are allowed."
|
|
1096
|
+
),
|
|
1097
|
+
ops: z.array(z.string().min(1).max(200)).min(1).max(10).describe(
|
|
1098
|
+
'Short human-readable summaries of what changed (e.g. "Moved the logo panel 0.5 left"). Shown live to the user in their editor.'
|
|
1099
|
+
)
|
|
1100
|
+
};
|
|
1101
|
+
server2.tool(
|
|
1102
|
+
"update_scene",
|
|
1103
|
+
`Write the connected artwork's scene (whole-document, revision-checked). The user's open editor applies the change LIVE and shows your ops descriptions, so keep ops honest and human-readable. Rules: send the COMPLETE document (this is not a patch \u2014 anything you omit is deleted); preserve any unknown keys you read; base expectedRevision on your latest get_scene (or the revision returned by your last successful update_scene). On a revision conflict nothing is written \u2014 the response includes the current scene so you can rebase and retry. ${SCENE_CONTRACT}`,
|
|
1104
|
+
updateSceneParams,
|
|
1105
|
+
async (args) => {
|
|
1106
|
+
const { expectedRevision, scene, ops } = args;
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await client2.agent.saveScene({
|
|
1109
|
+
artworkId: await artworkId(),
|
|
1110
|
+
doc: scene,
|
|
1111
|
+
expectedRevision,
|
|
1112
|
+
ops,
|
|
1113
|
+
originId: "agent:mcp"
|
|
1114
|
+
});
|
|
1115
|
+
if (result.conflict) {
|
|
1116
|
+
return asText({
|
|
1117
|
+
conflict: true,
|
|
1118
|
+
message: "Not written: the scene changed since your read (the human may have edited it). Rebase your change onto the current scene below and retry with its revision.",
|
|
1119
|
+
currentRevision: result.currentRevision,
|
|
1120
|
+
currentScene: result.current ? parseSceneConfig(result.current.sceneConfig) : null
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
return asText({
|
|
1124
|
+
conflict: false,
|
|
1125
|
+
revision: result.revision,
|
|
1126
|
+
message: `Saved (revision ${result.revision}). The user's editor has applied your change live.`
|
|
1127
|
+
});
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
return asError(err);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
server2.tool(
|
|
1134
|
+
"publish_artwork",
|
|
1135
|
+
`Publish the connected artwork so it is publicly viewable at its launch URL (${options.launchBaseUrl}/<artworkId>). Only works when the artwork is in draft or disabled state \u2014 already-published artworks reject (their live page already reflects saved scene changes). Ask the user before publishing unless they already told you to.`,
|
|
1136
|
+
async () => {
|
|
1137
|
+
try {
|
|
1138
|
+
const id = await artworkId();
|
|
1139
|
+
const artwork = await client2.agent.publish(id);
|
|
1140
|
+
return asText({
|
|
1141
|
+
id: artwork.id,
|
|
1142
|
+
status: artwork.status,
|
|
1143
|
+
url: `${options.launchBaseUrl}/${artwork.id}`
|
|
1144
|
+
});
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
if (err instanceof GraphQLRequestError && `${err.errorType} ${err.message}`.includes("ConditionalCheckFailed")) {
|
|
1147
|
+
return asText(
|
|
1148
|
+
"Nothing to do: the artwork is already published (publish only transitions draft/disabled \u2192 published). Saved scene changes are already live on its launch URL."
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
return asError(err);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
);
|
|
1155
|
+
let resultSub = null;
|
|
1156
|
+
const pendingResults = /* @__PURE__ */ new Map();
|
|
1157
|
+
const ensureResultListener = async () => {
|
|
1158
|
+
if (resultSub) return;
|
|
1159
|
+
resultSub = client2.agent.watchActionResults(options.sessionId, {
|
|
1160
|
+
next: (result) => {
|
|
1161
|
+
const resolve = pendingResults.get(result.requestId);
|
|
1162
|
+
if (resolve) {
|
|
1163
|
+
pendingResults.delete(result.requestId);
|
|
1164
|
+
resolve(result);
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
error: (err) => {
|
|
1168
|
+
console.error("[eyejack-mcp] action result channel error:", err);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1172
|
+
};
|
|
1173
|
+
const requestOnce = async (payload, timeoutMs) => {
|
|
1174
|
+
const requestId = randomUUID();
|
|
1175
|
+
const waiter = new Promise((resolve) => {
|
|
1176
|
+
const timer = setTimeout(() => {
|
|
1177
|
+
pendingResults.delete(requestId);
|
|
1178
|
+
resolve(null);
|
|
1179
|
+
}, timeoutMs);
|
|
1180
|
+
pendingResults.set(requestId, (result) => {
|
|
1181
|
+
clearTimeout(timer);
|
|
1182
|
+
resolve(result);
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
await client2.agent.requestAction({
|
|
1186
|
+
sessionId: options.sessionId,
|
|
1187
|
+
requestId,
|
|
1188
|
+
actionType: "SCREENSHOT",
|
|
1189
|
+
payload
|
|
1190
|
+
});
|
|
1191
|
+
return waiter;
|
|
1192
|
+
};
|
|
1193
|
+
const screenshotParams = {
|
|
1194
|
+
width: z.number().int().min(64).max(2048).optional().describe("Image width in px (default 1280; height follows the editor aspect)."),
|
|
1195
|
+
quality: z.number().min(0.1).max(1).optional().describe("JPEG quality 0.1\u20131 (default 0.8; auto-degraded if the image exceeds the transport cap).")
|
|
1196
|
+
};
|
|
1197
|
+
server2.tool(
|
|
1198
|
+
"screenshot",
|
|
1199
|
+
"See the scene: capture a rendered JPEG of the connected artwork exactly as the user's open editor shows it. Use it to visually verify your update_scene edits (read \u2192 edit \u2192 screenshot \u2192 correct). Requires the editor tab open in a desktop browser; if nobody answers, the tab is closed, hidden, or on a touch device.",
|
|
1200
|
+
screenshotParams,
|
|
1201
|
+
async (args) => {
|
|
1202
|
+
const { width, quality } = args;
|
|
1203
|
+
try {
|
|
1204
|
+
await ensureResultListener();
|
|
1205
|
+
const payload = JSON.stringify({ width: width ?? 1280, quality: quality ?? 0.8 });
|
|
1206
|
+
let result = await requestOnce(payload, 2e4);
|
|
1207
|
+
if (!result) {
|
|
1208
|
+
result = await requestOnce(payload, 2e4);
|
|
1209
|
+
}
|
|
1210
|
+
if (!result) {
|
|
1211
|
+
return asText(
|
|
1212
|
+
"No response from the editor tab (timed out twice). Ask the user to check the EyeJack editor tab is open, visible, and showing this artwork, then try again."
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
if (!result.ok || !result.payload) {
|
|
1216
|
+
return asText(`The editor could not capture a screenshot: ${result.error || "unknown error"}`);
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
content: [
|
|
1220
|
+
{
|
|
1221
|
+
type: "image",
|
|
1222
|
+
data: result.payload,
|
|
1223
|
+
mimeType: result.resultType || "image/jpeg"
|
|
1224
|
+
}
|
|
1225
|
+
]
|
|
1226
|
+
};
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
return asError(err);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
);
|
|
928
1232
|
server2.tool(
|
|
929
1233
|
"get_session_status",
|
|
930
1234
|
"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).",
|
|
@@ -950,13 +1254,20 @@ function registerTools(server2, client2) {
|
|
|
950
1254
|
var config = loadConfig();
|
|
951
1255
|
var client = createCreatorClient({
|
|
952
1256
|
endpoint: config.endpoint,
|
|
953
|
-
auth: new StaticTokenProvider(config.token)
|
|
1257
|
+
auth: new StaticTokenProvider(config.token),
|
|
1258
|
+
// Node ≥22 ships a global WebSocket; older runtimes use `ws`. Needed for
|
|
1259
|
+
// the screenshot tool's result subscription.
|
|
1260
|
+
WebSocketImpl: globalThis.WebSocket ?? WsWebSocket
|
|
954
1261
|
});
|
|
955
1262
|
var server = new McpServer({
|
|
956
1263
|
name: "eyejack-creator",
|
|
957
|
-
version: "0.
|
|
1264
|
+
version: "0.3.0"
|
|
1265
|
+
});
|
|
1266
|
+
registerTools(server, client, {
|
|
1267
|
+
launchBaseUrl: config.launchBaseUrl,
|
|
1268
|
+
// The token is "<sessionId>.<secret>" — the channel tools need the id.
|
|
1269
|
+
sessionId: config.token.split(".")[0]
|
|
958
1270
|
});
|
|
959
|
-
registerTools(server, client);
|
|
960
1271
|
var transport = new StdioServerTransport();
|
|
961
1272
|
await server.connect(transport);
|
|
962
|
-
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014 read
|
|
1273
|
+
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014 scene read/write + screenshot tools ready.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eyejack-creator/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "EyeJack Creator MCP server — connect Claude Code / Codex / Cursor to a live EyeJack editing session.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
23
|
-
"
|
|
23
|
+
"ws": "^8.18.0",
|
|
24
|
+
"zod": "3.24.2"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"esbuild": "^0.24.0",
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"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
|
+
"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 --external:ws --banner:js=\"#!/usr/bin/env node\"",
|
|
33
34
|
"smoke": "node examples/smoke-client.mjs"
|
|
34
35
|
}
|
|
35
36
|
}
|