@eyejack-creator/mcp 0.2.0 → 0.4.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 +2 -1
- package/dist/index.js +382 -39
- package/package.json +3 -2
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. Your AI reads the scene **
|
|
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)
|
|
@@ -31,6 +31,7 @@ The token lives as long as your editor tab (the tab heartbeats it; closing the t
|
|
|
31
31
|
| `list_assets` | The artwork's asset files with `usedInScene` resolution |
|
|
32
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
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 |
|
|
34
35
|
| `get_session_status` | Session scope, active state, expiry |
|
|
35
36
|
|
|
36
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 {
|
|
@@ -616,11 +617,10 @@ var SceneApi = class {
|
|
|
616
617
|
|
|
617
618
|
// ../creator-api/dist/domains/agent.js
|
|
618
619
|
var CREATE_AGENT_SESSION = `
|
|
619
|
-
mutation CreateAgentSession
|
|
620
|
-
createAgentSession
|
|
620
|
+
mutation CreateAgentSession {
|
|
621
|
+
createAgentSession {
|
|
621
622
|
sessionId
|
|
622
623
|
token
|
|
623
|
-
artworkId
|
|
624
624
|
expiresAt
|
|
625
625
|
}
|
|
626
626
|
}
|
|
@@ -669,9 +669,78 @@ var GET_ARTWORK_SCENE_BACKUP = `
|
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
671
|
`;
|
|
672
|
+
var REQUEST_AGENT_ACTION = `
|
|
673
|
+
mutation RequestAgentAction($sessionId: ID!, $requestId: ID!, $actionType: String!, $payload: String) {
|
|
674
|
+
requestAgentAction(sessionId: $sessionId, requestId: $requestId, actionType: $actionType, payload: $payload) {
|
|
675
|
+
sessionId
|
|
676
|
+
requestId
|
|
677
|
+
actionType
|
|
678
|
+
payload
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
`;
|
|
682
|
+
var POST_AGENT_ACTION_RESULT = `
|
|
683
|
+
mutation PostAgentActionResult($sessionId: ID!, $requestId: ID!, $ok: Boolean!, $resultType: String, $payload: String, $error: String) {
|
|
684
|
+
postAgentActionResult(sessionId: $sessionId, requestId: $requestId, ok: $ok, resultType: $resultType, payload: $payload, error: $error) {
|
|
685
|
+
sessionId
|
|
686
|
+
requestId
|
|
687
|
+
ok
|
|
688
|
+
resultType
|
|
689
|
+
payload
|
|
690
|
+
error
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
`;
|
|
694
|
+
var ON_AGENT_ACTION = `
|
|
695
|
+
subscription OnAgentAction($sessionId: ID!) {
|
|
696
|
+
onAgentAction(sessionId: $sessionId) {
|
|
697
|
+
sessionId
|
|
698
|
+
requestId
|
|
699
|
+
actionType
|
|
700
|
+
payload
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
`;
|
|
704
|
+
var ON_AGENT_ACTION_RESULT = `
|
|
705
|
+
subscription OnAgentActionResult($sessionId: ID!) {
|
|
706
|
+
onAgentActionResult(sessionId: $sessionId) {
|
|
707
|
+
sessionId
|
|
708
|
+
requestId
|
|
709
|
+
ok
|
|
710
|
+
resultType
|
|
711
|
+
payload
|
|
712
|
+
error
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
`;
|
|
716
|
+
var AGENT_LIST_ARTWORKS = `
|
|
717
|
+
query AgentListArtworks {
|
|
718
|
+
agentListArtworks {
|
|
719
|
+
id
|
|
720
|
+
userid
|
|
721
|
+
name
|
|
722
|
+
description
|
|
723
|
+
status
|
|
724
|
+
artworkType
|
|
725
|
+
sceneRevision
|
|
726
|
+
modified
|
|
727
|
+
created
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
`;
|
|
731
|
+
var AGENT_ENSURE_SCENE_BACKUP = `
|
|
732
|
+
mutation AgentEnsureSceneBackup($artworkId: ID!) {
|
|
733
|
+
agentEnsureSceneBackup(artworkId: $artworkId) {
|
|
734
|
+
artworkId
|
|
735
|
+
sessionId
|
|
736
|
+
sceneRevision
|
|
737
|
+
createdAt
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
`;
|
|
672
741
|
var AGENT_GET_SCENE = `
|
|
673
|
-
query AgentGetScene {
|
|
674
|
-
agentGetScene {
|
|
742
|
+
query AgentGetScene($artworkId: ID) {
|
|
743
|
+
agentGetScene(artworkId: $artworkId) {
|
|
675
744
|
artworkId
|
|
676
745
|
name
|
|
677
746
|
status
|
|
@@ -692,12 +761,15 @@ var AGENT_GET_SCENE = `
|
|
|
692
761
|
}
|
|
693
762
|
`;
|
|
694
763
|
var AgentApi = class {
|
|
695
|
-
constructor(client2) {
|
|
764
|
+
constructor(client2, realtime) {
|
|
696
765
|
this.client = client2;
|
|
766
|
+
this.realtime = realtime;
|
|
767
|
+
this.backupEnsured = /* @__PURE__ */ new Set();
|
|
697
768
|
}
|
|
698
769
|
// --- Browser-side (Cognito) session lifecycle ---
|
|
699
|
-
|
|
700
|
-
|
|
770
|
+
/** Mint an ACCOUNT-scoped session (P6a) — authorizes everything the account owns. */
|
|
771
|
+
async createSession() {
|
|
772
|
+
const data = await this.client.execute(CREATE_AGENT_SESSION);
|
|
701
773
|
return data.createAgentSession;
|
|
702
774
|
}
|
|
703
775
|
async heartbeat(sessionId) {
|
|
@@ -723,10 +795,52 @@ var AgentApi = class {
|
|
|
723
795
|
const data = await this.client.execute(AGENT_GET_SESSION);
|
|
724
796
|
return data.agentGetSession;
|
|
725
797
|
}
|
|
726
|
-
|
|
727
|
-
|
|
798
|
+
/** Read any artwork the session's account owns (ownership checked server-side). */
|
|
799
|
+
async getScene(artworkId) {
|
|
800
|
+
const data = await this.client.execute(AGENT_GET_SCENE, {
|
|
801
|
+
artworkId
|
|
802
|
+
});
|
|
728
803
|
return data.agentGetScene;
|
|
729
804
|
}
|
|
805
|
+
/** The account's workspace: every non-deleted artwork it owns. */
|
|
806
|
+
async listArtworks() {
|
|
807
|
+
const data = await this.client.execute(AGENT_LIST_ARTWORKS);
|
|
808
|
+
return data.agentListArtworks ?? [];
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Create a new panels (worldTarget) artwork owned by the session's account
|
|
812
|
+
* (the server forces ownership regardless of input). Mirrors the editor's
|
|
813
|
+
* new-panels defaults; the artwork starts published like the web flow.
|
|
814
|
+
*/
|
|
815
|
+
async createArtwork(params) {
|
|
816
|
+
const id = `Artwork-${globalThis.crypto.randomUUID()}`;
|
|
817
|
+
const input = {
|
|
818
|
+
id,
|
|
819
|
+
files: [],
|
|
820
|
+
creatorVersion: "agent-mcp",
|
|
821
|
+
status: "published",
|
|
822
|
+
artworkType: "worldTarget",
|
|
823
|
+
name: params?.name ?? "Untitled panels",
|
|
824
|
+
description: params?.description ?? "",
|
|
825
|
+
downloadsCount: 0,
|
|
826
|
+
viewsCount: 0,
|
|
827
|
+
publishEnabled: true,
|
|
828
|
+
enabled: true,
|
|
829
|
+
animationHasAlpha: false,
|
|
830
|
+
animationVideoOrientation: "horizontal"
|
|
831
|
+
};
|
|
832
|
+
const data = await this.client.execute(CREATE_ARTWORK, { input });
|
|
833
|
+
return data.createArtwork;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Lazy pre-write snapshot (P6a): idempotent per (session, artwork) —
|
|
837
|
+
* powers the editor's "Revert AI changes". saveScene calls this
|
|
838
|
+
* automatically before a session's first write to each artwork.
|
|
839
|
+
*/
|
|
840
|
+
async ensureSceneBackup(artworkId) {
|
|
841
|
+
const data = await this.client.execute(AGENT_ENSURE_SCENE_BACKUP, { artworkId });
|
|
842
|
+
return data.agentEnsureSceneBackup;
|
|
843
|
+
}
|
|
730
844
|
// --- Agent-side (session token) writes (P4) ---
|
|
731
845
|
/**
|
|
732
846
|
* Revision-checked scene write with a session token. Same mutation and
|
|
@@ -743,6 +857,13 @@ var AgentApi = class {
|
|
|
743
857
|
throw new Error(`Refusing to save invalid scene document: ${errors.map((i) => i.message).join("; ")}`);
|
|
744
858
|
}
|
|
745
859
|
}
|
|
860
|
+
if (!this.backupEnsured.has(params.artworkId)) {
|
|
861
|
+
try {
|
|
862
|
+
await this.ensureSceneBackup(params.artworkId);
|
|
863
|
+
this.backupEnsured.add(params.artworkId);
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
}
|
|
746
867
|
try {
|
|
747
868
|
const data = await this.client.execute(UPDATE_ARTWORK_SCENE, {
|
|
748
869
|
input: {
|
|
@@ -757,7 +878,7 @@ var AgentApi = class {
|
|
|
757
878
|
return { conflict: false, revision: payload.sceneRevision, modified: payload.modified ?? null, payload };
|
|
758
879
|
} catch (err) {
|
|
759
880
|
if (err instanceof GraphQLRequestError && err.errorType === "SceneRevisionConflict") {
|
|
760
|
-
const current = await this.getScene().catch(() => null);
|
|
881
|
+
const current = await this.getScene(params.artworkId).catch(() => null);
|
|
761
882
|
return { conflict: true, currentRevision: current?.sceneRevision ?? null, current };
|
|
762
883
|
}
|
|
763
884
|
throw err;
|
|
@@ -774,6 +895,46 @@ var AgentApi = class {
|
|
|
774
895
|
});
|
|
775
896
|
return data.publishArtwork;
|
|
776
897
|
}
|
|
898
|
+
// --- Runtime action channel (P5) ---
|
|
899
|
+
/** AGENT side: fire an action request into the session's channel. */
|
|
900
|
+
async requestAction(params) {
|
|
901
|
+
const data = await this.client.execute(REQUEST_AGENT_ACTION, { ...params, payload: params.payload ?? null });
|
|
902
|
+
return data.requestAgentAction;
|
|
903
|
+
}
|
|
904
|
+
/** BROWSER side: answer an action request (session-owner-checked server-side). */
|
|
905
|
+
async postActionResult(params) {
|
|
906
|
+
const data = await this.client.execute(POST_AGENT_ACTION_RESULT, {
|
|
907
|
+
...params,
|
|
908
|
+
resultType: params.resultType ?? null,
|
|
909
|
+
payload: params.payload ?? null,
|
|
910
|
+
error: params.error ?? null
|
|
911
|
+
});
|
|
912
|
+
return data.postAgentActionResult;
|
|
913
|
+
}
|
|
914
|
+
/** BROWSER side: listen for the agent's action requests (subscribe-time owner check). */
|
|
915
|
+
watchActions(sessionId, handlers) {
|
|
916
|
+
if (!this.realtime)
|
|
917
|
+
throw new Error("AgentApi.watchActions: realtime client not configured");
|
|
918
|
+
return this.realtime().subscribe(ON_AGENT_ACTION, { sessionId }, {
|
|
919
|
+
next: (data) => {
|
|
920
|
+
if (data?.onAgentAction)
|
|
921
|
+
handlers.next(data.onAgentAction);
|
|
922
|
+
},
|
|
923
|
+
error: handlers.error
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
/** AGENT side: listen for the browser's results (subscribe-time session pin). */
|
|
927
|
+
watchActionResults(sessionId, handlers) {
|
|
928
|
+
if (!this.realtime)
|
|
929
|
+
throw new Error("AgentApi.watchActionResults: realtime client not configured");
|
|
930
|
+
return this.realtime().subscribe(ON_AGENT_ACTION_RESULT, { sessionId }, {
|
|
931
|
+
next: (data) => {
|
|
932
|
+
if (data?.onAgentActionResult)
|
|
933
|
+
handlers.next(data.onAgentActionResult);
|
|
934
|
+
},
|
|
935
|
+
error: handlers.error
|
|
936
|
+
});
|
|
937
|
+
}
|
|
777
938
|
};
|
|
778
939
|
|
|
779
940
|
// ../creator-api/dist/domains/artworks.js
|
|
@@ -868,7 +1029,7 @@ function createCreatorClient(options) {
|
|
|
868
1029
|
}
|
|
869
1030
|
return realtimeInstance;
|
|
870
1031
|
};
|
|
871
|
-
const agent = new AgentApi(graphql);
|
|
1032
|
+
const agent = new AgentApi(graphql, getRealtime);
|
|
872
1033
|
const artworks = new ArtworksApi(graphql, getRealtime);
|
|
873
1034
|
const scene = new SceneApi(graphql, artworks, getRealtime);
|
|
874
1035
|
return {
|
|
@@ -914,6 +1075,7 @@ function loadConfig() {
|
|
|
914
1075
|
}
|
|
915
1076
|
|
|
916
1077
|
// src/tools.ts
|
|
1078
|
+
import { randomUUID } from "node:crypto";
|
|
917
1079
|
import { z } from "zod";
|
|
918
1080
|
var SCENE_CONTRACT = `Scene document conventions (the stored EyeJack format):
|
|
919
1081
|
- Coordinate space is unity-style left-handed; rotations are in DEGREES.
|
|
@@ -932,7 +1094,7 @@ function errorText(err) {
|
|
|
932
1094
|
if ((err.errorType || "").includes("Unauthorized")) {
|
|
933
1095
|
return REJECTED_HINT;
|
|
934
1096
|
}
|
|
935
|
-
return `EyeJack API error
|
|
1097
|
+
return `EyeJack API error (${err.errorType || "unknown"}): ${err.message}`;
|
|
936
1098
|
}
|
|
937
1099
|
return `Error: ${err.message}`;
|
|
938
1100
|
}
|
|
@@ -944,19 +1106,92 @@ var asError = (err) => ({
|
|
|
944
1106
|
isError: true
|
|
945
1107
|
});
|
|
946
1108
|
function registerTools(server2, client2, options) {
|
|
947
|
-
let
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
1109
|
+
let resultSub = null;
|
|
1110
|
+
const pendingResults = /* @__PURE__ */ new Map();
|
|
1111
|
+
const ensureResultListener = async () => {
|
|
1112
|
+
if (resultSub) return;
|
|
1113
|
+
resultSub = client2.agent.watchActionResults(options.sessionId, {
|
|
1114
|
+
next: (result) => {
|
|
1115
|
+
const resolve = pendingResults.get(result.requestId);
|
|
1116
|
+
if (resolve) {
|
|
1117
|
+
pendingResults.delete(result.requestId);
|
|
1118
|
+
resolve(result);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
error: (err) => {
|
|
1122
|
+
console.error("[eyejack-mcp] action result channel error:", err);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1126
|
+
};
|
|
1127
|
+
const requestOnce = async (actionType, payload, timeoutMs) => {
|
|
1128
|
+
const requestId = randomUUID();
|
|
1129
|
+
const waiter = new Promise((resolve) => {
|
|
1130
|
+
const timer = setTimeout(() => {
|
|
1131
|
+
pendingResults.delete(requestId);
|
|
1132
|
+
resolve(null);
|
|
1133
|
+
}, timeoutMs);
|
|
1134
|
+
pendingResults.set(requestId, (result) => {
|
|
1135
|
+
clearTimeout(timer);
|
|
1136
|
+
resolve(result);
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
await client2.agent.requestAction({
|
|
1140
|
+
sessionId: options.sessionId,
|
|
1141
|
+
requestId,
|
|
1142
|
+
actionType,
|
|
1143
|
+
payload
|
|
1144
|
+
});
|
|
1145
|
+
return waiter;
|
|
1146
|
+
};
|
|
1147
|
+
const NO_ARTWORK_HINT = "No artworkId given and no artwork is open in the editor tab. Pass artworkId explicitly (see list_artworks) or use open_artwork first.";
|
|
1148
|
+
const resolveArtworkId = async (explicit) => {
|
|
1149
|
+
if (explicit) return explicit;
|
|
1150
|
+
await ensureResultListener();
|
|
1151
|
+
const pong = await requestOnce("PING", null, 6e3);
|
|
1152
|
+
if (pong?.ok && pong.payload) {
|
|
1153
|
+
try {
|
|
1154
|
+
const parsed = JSON.parse(pong.payload);
|
|
1155
|
+
if (parsed.activeArtworkId) return parsed.activeArtworkId;
|
|
1156
|
+
} catch {
|
|
1157
|
+
}
|
|
951
1158
|
}
|
|
952
|
-
|
|
1159
|
+
throw new Error(NO_ARTWORK_HINT);
|
|
1160
|
+
};
|
|
1161
|
+
const artworkIdParam = {
|
|
1162
|
+
artworkId: z.string().optional().describe(
|
|
1163
|
+
'Target artwork id ("Artwork-\u2026"), any panels project the account owns. Omit to target the one open in the editor tab.'
|
|
1164
|
+
)
|
|
953
1165
|
};
|
|
954
1166
|
server2.tool(
|
|
955
|
-
"
|
|
956
|
-
|
|
1167
|
+
"list_artworks",
|
|
1168
|
+
"List every artwork in the connected EyeJack account \u2014 the whole workspace this session can read and edit. Only worldTarget (panels) artworks are editable through this server; other types are read-only context.",
|
|
957
1169
|
async () => {
|
|
958
1170
|
try {
|
|
959
|
-
const
|
|
1171
|
+
const artworks = await client2.agent.listArtworks();
|
|
1172
|
+
return asText(
|
|
1173
|
+
artworks.map((a) => ({
|
|
1174
|
+
id: a.id,
|
|
1175
|
+
name: a.name,
|
|
1176
|
+
artworkType: a.artworkType,
|
|
1177
|
+
status: a.status,
|
|
1178
|
+
sceneRevision: a.sceneRevision ?? 0,
|
|
1179
|
+
modified: a.modified
|
|
1180
|
+
}))
|
|
1181
|
+
);
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
return asError(err);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
);
|
|
1187
|
+
server2.tool(
|
|
1188
|
+
"get_scene",
|
|
1189
|
+
`Read the full structured snapshot of an artwork: metadata, the parsed scene document, the current sceneRevision, and the asset files it references. The session covers the whole account (P6a) \u2014 omit artworkId to read whatever is open in the editor tab. ${SCENE_CONTRACT}`,
|
|
1190
|
+
artworkIdParam,
|
|
1191
|
+
async (args) => {
|
|
1192
|
+
try {
|
|
1193
|
+
const id = await resolveArtworkId(args.artworkId);
|
|
1194
|
+
const record = await client2.agent.getScene(id);
|
|
960
1195
|
const doc = parseSceneConfig(record.sceneConfig);
|
|
961
1196
|
return asText({
|
|
962
1197
|
artworkId: record.artworkId,
|
|
@@ -981,10 +1216,12 @@ function registerTools(server2, client2, options) {
|
|
|
981
1216
|
);
|
|
982
1217
|
server2.tool(
|
|
983
1218
|
"list_assets",
|
|
984
|
-
"List the asset files (images, videos, 3D models) available on
|
|
985
|
-
|
|
1219
|
+
"List the asset files (images, videos, 3D models) available on an artwork \u2014 the palette its panels can reference via assetID == fileID. Omit artworkId to target the open artwork. Read-only.",
|
|
1220
|
+
artworkIdParam,
|
|
1221
|
+
async (args) => {
|
|
986
1222
|
try {
|
|
987
|
-
const
|
|
1223
|
+
const id = await resolveArtworkId(args.artworkId);
|
|
1224
|
+
const record = await client2.agent.getScene(id);
|
|
988
1225
|
const doc = parseSceneConfig(record.sceneConfig);
|
|
989
1226
|
const usedIds = new Set((doc?.scenes?.[0]?.children || []).map((c) => c.assetID));
|
|
990
1227
|
return asText(
|
|
@@ -1002,8 +1239,55 @@ function registerTools(server2, client2, options) {
|
|
|
1002
1239
|
}
|
|
1003
1240
|
}
|
|
1004
1241
|
);
|
|
1242
|
+
const createArtworkParams = {
|
|
1243
|
+
name: z.string().min(1).max(120).optional().describe('Project name (default "Untitled panels").'),
|
|
1244
|
+
description: z.string().max(2e3).optional().describe("Optional description.")
|
|
1245
|
+
};
|
|
1246
|
+
server2.tool(
|
|
1247
|
+
"create_artwork",
|
|
1248
|
+
"Create a new, empty panels (worldTarget) project in the connected account. It starts with no assets \u2014 the human adds media in the browser; you can arrange the scene once assets exist. Follow up with open_artwork to bring it up in their editor.",
|
|
1249
|
+
createArtworkParams,
|
|
1250
|
+
async (args) => {
|
|
1251
|
+
const { name, description } = args;
|
|
1252
|
+
try {
|
|
1253
|
+
const created = await client2.agent.createArtwork({ name, description });
|
|
1254
|
+
return asText({
|
|
1255
|
+
id: created.id,
|
|
1256
|
+
status: created.status,
|
|
1257
|
+
message: `Created "${name ?? "Untitled panels"}" (${created.id}). Use open_artwork to show it in the user's editor.`
|
|
1258
|
+
});
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
return asError(err);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
);
|
|
1264
|
+
const openArtworkParams = {
|
|
1265
|
+
artworkId: z.string().describe('The panels artwork to open ("Artwork-\u2026").')
|
|
1266
|
+
};
|
|
1267
|
+
server2.tool(
|
|
1268
|
+
"open_artwork",
|
|
1269
|
+
"Navigate the user's open editor tab to a panels artwork \u2014 steering the viewport. Viewport-bound tools (screenshot) always target whatever is open. Requires the editor tab to be open; the user sees the navigation happen, so tell them why you are switching.",
|
|
1270
|
+
openArtworkParams,
|
|
1271
|
+
async (args) => {
|
|
1272
|
+
const { artworkId } = args;
|
|
1273
|
+
try {
|
|
1274
|
+
await ensureResultListener();
|
|
1275
|
+
const result = await requestOnce("OPEN_ARTWORK", JSON.stringify({ artworkId }), 15e3);
|
|
1276
|
+
if (!result) {
|
|
1277
|
+
return asText("No response from the editor tab \u2014 is it open and visible?");
|
|
1278
|
+
}
|
|
1279
|
+
if (!result.ok) {
|
|
1280
|
+
return asText(`Could not open the artwork: ${result.error || "unknown error"}`);
|
|
1281
|
+
}
|
|
1282
|
+
return asText({ opened: artworkId });
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
return asError(err);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1005
1288
|
const updateSceneParams = {
|
|
1006
|
-
|
|
1289
|
+
artworkId: z.string().optional().describe("Target artwork id. Omit to write the one open in the editor tab."),
|
|
1290
|
+
expectedRevision: z.number().int().min(0).describe("The sceneRevision your edit is based on, from your latest read of THIS artwork."),
|
|
1007
1291
|
scene: z.record(z.any()).describe(
|
|
1008
1292
|
"The complete scene document to store: { version, settings?, assets, scenes }. Top-level keys are frozen to these; per-child additive keys are allowed."
|
|
1009
1293
|
),
|
|
@@ -1013,13 +1297,14 @@ function registerTools(server2, client2, options) {
|
|
|
1013
1297
|
};
|
|
1014
1298
|
server2.tool(
|
|
1015
1299
|
"update_scene",
|
|
1016
|
-
`Write
|
|
1300
|
+
`Write an artwork's scene (whole-document, revision-checked; account-scoped \u2014 any panels project you own, open in the editor or not). If the artwork is open, the user's 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 read of the SAME artwork. On a revision conflict nothing is written \u2014 the response includes the current scene so you can rebase and retry. ${SCENE_CONTRACT}`,
|
|
1017
1301
|
updateSceneParams,
|
|
1018
1302
|
async (args) => {
|
|
1019
|
-
const { expectedRevision, scene, ops } = args;
|
|
1303
|
+
const { artworkId, expectedRevision, scene, ops } = args;
|
|
1020
1304
|
try {
|
|
1305
|
+
const id = await resolveArtworkId(artworkId);
|
|
1021
1306
|
const result = await client2.agent.saveScene({
|
|
1022
|
-
artworkId:
|
|
1307
|
+
artworkId: id,
|
|
1023
1308
|
doc: scene,
|
|
1024
1309
|
expectedRevision,
|
|
1025
1310
|
ops,
|
|
@@ -1028,6 +1313,7 @@ function registerTools(server2, client2, options) {
|
|
|
1028
1313
|
if (result.conflict) {
|
|
1029
1314
|
return asText({
|
|
1030
1315
|
conflict: true,
|
|
1316
|
+
artworkId: id,
|
|
1031
1317
|
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.",
|
|
1032
1318
|
currentRevision: result.currentRevision,
|
|
1033
1319
|
currentScene: result.current ? parseSceneConfig(result.current.sceneConfig) : null
|
|
@@ -1035,8 +1321,9 @@ function registerTools(server2, client2, options) {
|
|
|
1035
1321
|
}
|
|
1036
1322
|
return asText({
|
|
1037
1323
|
conflict: false,
|
|
1324
|
+
artworkId: id,
|
|
1038
1325
|
revision: result.revision,
|
|
1039
|
-
message: `Saved (revision ${result.revision})
|
|
1326
|
+
message: `Saved (revision ${result.revision}).`
|
|
1040
1327
|
});
|
|
1041
1328
|
} catch (err) {
|
|
1042
1329
|
return asError(err);
|
|
@@ -1045,10 +1332,11 @@ function registerTools(server2, client2, options) {
|
|
|
1045
1332
|
);
|
|
1046
1333
|
server2.tool(
|
|
1047
1334
|
"publish_artwork",
|
|
1048
|
-
`Publish
|
|
1049
|
-
|
|
1335
|
+
`Publish an 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). Omit artworkId to target the open artwork. Ask the user before publishing unless they already told you to.`,
|
|
1336
|
+
artworkIdParam,
|
|
1337
|
+
async (args) => {
|
|
1050
1338
|
try {
|
|
1051
|
-
const id = await artworkId
|
|
1339
|
+
const id = await resolveArtworkId(args.artworkId);
|
|
1052
1340
|
const artwork = await client2.agent.publish(id);
|
|
1053
1341
|
return asText({
|
|
1054
1342
|
id: artwork.id,
|
|
@@ -1058,23 +1346,71 @@ function registerTools(server2, client2, options) {
|
|
|
1058
1346
|
} catch (err) {
|
|
1059
1347
|
if (err instanceof GraphQLRequestError && `${err.errorType} ${err.message}`.includes("ConditionalCheckFailed")) {
|
|
1060
1348
|
return asText(
|
|
1061
|
-
"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."
|
|
1349
|
+
"Nothing to do: the artwork is already published (publish only transitions draft/disabled \u2192 published), or the account does not own it. Saved scene changes are already live on its launch URL."
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
return asError(err);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
const screenshotParams = {
|
|
1357
|
+
width: z.number().int().min(64).max(2048).optional().describe("Image width in px (default 1280; height follows the editor aspect)."),
|
|
1358
|
+
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).")
|
|
1359
|
+
};
|
|
1360
|
+
server2.tool(
|
|
1361
|
+
"screenshot",
|
|
1362
|
+
"See the scene: capture a rendered JPEG of whatever artwork the user's editor tab currently shows (use open_artwork to switch it first). 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.",
|
|
1363
|
+
screenshotParams,
|
|
1364
|
+
async (args) => {
|
|
1365
|
+
const { width, quality } = args;
|
|
1366
|
+
try {
|
|
1367
|
+
await ensureResultListener();
|
|
1368
|
+
const payload = JSON.stringify({ width: width ?? 1280, quality: quality ?? 0.8 });
|
|
1369
|
+
let result = await requestOnce("SCREENSHOT", payload, 2e4);
|
|
1370
|
+
if (!result) {
|
|
1371
|
+
result = await requestOnce("SCREENSHOT", payload, 2e4);
|
|
1372
|
+
}
|
|
1373
|
+
if (!result) {
|
|
1374
|
+
return asText(
|
|
1375
|
+
"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."
|
|
1062
1376
|
);
|
|
1063
1377
|
}
|
|
1378
|
+
if (!result.ok || !result.payload) {
|
|
1379
|
+
return asText(`The editor could not capture a screenshot: ${result.error || "unknown error"}`);
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
content: [
|
|
1383
|
+
{
|
|
1384
|
+
type: "image",
|
|
1385
|
+
data: result.payload,
|
|
1386
|
+
mimeType: result.resultType || "image/jpeg"
|
|
1387
|
+
}
|
|
1388
|
+
]
|
|
1389
|
+
};
|
|
1390
|
+
} catch (err) {
|
|
1064
1391
|
return asError(err);
|
|
1065
1392
|
}
|
|
1066
1393
|
}
|
|
1067
1394
|
);
|
|
1068
1395
|
server2.tool(
|
|
1069
1396
|
"get_session_status",
|
|
1070
|
-
"Inspect the agent session
|
|
1397
|
+
"Inspect the agent session: whether it is active, when it expires (the editor tab heartbeats it while open; closing the tab lets it lapse), and which artwork the tab currently shows (activeArtworkId; null when no panels page is open).",
|
|
1071
1398
|
async () => {
|
|
1072
1399
|
try {
|
|
1073
1400
|
const session = await client2.agent.getSession();
|
|
1074
1401
|
const now = Math.floor(Date.now() / 1e3);
|
|
1402
|
+
let activeArtworkId = null;
|
|
1403
|
+
try {
|
|
1404
|
+
await ensureResultListener();
|
|
1405
|
+
const pong = await requestOnce("PING", null, 5e3);
|
|
1406
|
+
if (pong?.ok && pong.payload) {
|
|
1407
|
+
activeArtworkId = JSON.parse(pong.payload).activeArtworkId ?? null;
|
|
1408
|
+
}
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1075
1411
|
return asText({
|
|
1076
|
-
artworkId: session.artworkId,
|
|
1077
1412
|
status: session.status,
|
|
1413
|
+
activeArtworkId,
|
|
1078
1414
|
expiresAt: session.expiresAt,
|
|
1079
1415
|
expiresInSeconds: session.expiresAt ? session.expiresAt - now : null,
|
|
1080
1416
|
lastHeartbeat: session.lastHeartbeat
|
|
@@ -1090,13 +1426,20 @@ function registerTools(server2, client2, options) {
|
|
|
1090
1426
|
var config = loadConfig();
|
|
1091
1427
|
var client = createCreatorClient({
|
|
1092
1428
|
endpoint: config.endpoint,
|
|
1093
|
-
auth: new StaticTokenProvider(config.token)
|
|
1429
|
+
auth: new StaticTokenProvider(config.token),
|
|
1430
|
+
// Node ≥22 ships a global WebSocket; older runtimes use `ws`. Needed for
|
|
1431
|
+
// the screenshot tool's result subscription.
|
|
1432
|
+
WebSocketImpl: globalThis.WebSocket ?? WsWebSocket
|
|
1094
1433
|
});
|
|
1095
1434
|
var server = new McpServer({
|
|
1096
1435
|
name: "eyejack-creator",
|
|
1097
|
-
version: "0.
|
|
1436
|
+
version: "0.4.0"
|
|
1437
|
+
});
|
|
1438
|
+
registerTools(server, client, {
|
|
1439
|
+
launchBaseUrl: config.launchBaseUrl,
|
|
1440
|
+
// The token is "<sessionId>.<secret>" — the channel tools need the id.
|
|
1441
|
+
sessionId: config.token.split(".")[0]
|
|
1098
1442
|
});
|
|
1099
|
-
registerTools(server, client, { launchBaseUrl: config.launchBaseUrl });
|
|
1100
1443
|
var transport = new StdioServerTransport();
|
|
1101
1444
|
await server.connect(transport);
|
|
1102
|
-
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014
|
|
1445
|
+
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014 account-scoped workspace tools ready.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eyejack-creator/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
23
|
+
"ws": "^8.18.0",
|
|
23
24
|
"zod": "3.24.2"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
@@ -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
|
}
|