@eyejack-creator/mcp 0.3.0 → 0.5.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 +1 -0
- package/dist/index.js +349 -74
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ The token lives as long as your editor tab (the tab heartbeats it; closing the t
|
|
|
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
|
+
| `import_asset` | Upload a local media file (image/video/GLB) — the open editor ingests it through its normal transcode/upload pipeline and auto-places it in the scene |
|
|
32
33
|
| `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
34
|
| `publish_artwork` | Publish the artwork to its public launch URL (draft/disabled → published) |
|
|
34
35
|
| `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 |
|
package/dist/index.js
CHANGED
|
@@ -617,11 +617,10 @@ var SceneApi = class {
|
|
|
617
617
|
|
|
618
618
|
// ../creator-api/dist/domains/agent.js
|
|
619
619
|
var CREATE_AGENT_SESSION = `
|
|
620
|
-
mutation CreateAgentSession
|
|
621
|
-
createAgentSession
|
|
620
|
+
mutation CreateAgentSession {
|
|
621
|
+
createAgentSession {
|
|
622
622
|
sessionId
|
|
623
623
|
token
|
|
624
|
-
artworkId
|
|
625
624
|
expiresAt
|
|
626
625
|
}
|
|
627
626
|
}
|
|
@@ -714,9 +713,44 @@ var ON_AGENT_ACTION_RESULT = `
|
|
|
714
713
|
}
|
|
715
714
|
}
|
|
716
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_CREATE_UPLOAD_URL = `
|
|
732
|
+
mutation AgentCreateUploadUrl($filename: String!, $contentType: String) {
|
|
733
|
+
agentCreateUploadUrl(filename: $filename, contentType: $contentType) {
|
|
734
|
+
uploadUrl
|
|
735
|
+
downloadUrl
|
|
736
|
+
key
|
|
737
|
+
expiresAt
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
`;
|
|
741
|
+
var AGENT_ENSURE_SCENE_BACKUP = `
|
|
742
|
+
mutation AgentEnsureSceneBackup($artworkId: ID!) {
|
|
743
|
+
agentEnsureSceneBackup(artworkId: $artworkId) {
|
|
744
|
+
artworkId
|
|
745
|
+
sessionId
|
|
746
|
+
sceneRevision
|
|
747
|
+
createdAt
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
`;
|
|
717
751
|
var AGENT_GET_SCENE = `
|
|
718
|
-
query AgentGetScene {
|
|
719
|
-
agentGetScene {
|
|
752
|
+
query AgentGetScene($artworkId: ID) {
|
|
753
|
+
agentGetScene(artworkId: $artworkId) {
|
|
720
754
|
artworkId
|
|
721
755
|
name
|
|
722
756
|
status
|
|
@@ -740,10 +774,12 @@ var AgentApi = class {
|
|
|
740
774
|
constructor(client2, realtime) {
|
|
741
775
|
this.client = client2;
|
|
742
776
|
this.realtime = realtime;
|
|
777
|
+
this.backupEnsured = /* @__PURE__ */ new Set();
|
|
743
778
|
}
|
|
744
779
|
// --- Browser-side (Cognito) session lifecycle ---
|
|
745
|
-
|
|
746
|
-
|
|
780
|
+
/** Mint an ACCOUNT-scoped session (P6a) — authorizes everything the account owns. */
|
|
781
|
+
async createSession() {
|
|
782
|
+
const data = await this.client.execute(CREATE_AGENT_SESSION);
|
|
747
783
|
return data.createAgentSession;
|
|
748
784
|
}
|
|
749
785
|
async heartbeat(sessionId) {
|
|
@@ -769,10 +805,61 @@ var AgentApi = class {
|
|
|
769
805
|
const data = await this.client.execute(AGENT_GET_SESSION);
|
|
770
806
|
return data.agentGetSession;
|
|
771
807
|
}
|
|
772
|
-
|
|
773
|
-
|
|
808
|
+
/** Read any artwork the session's account owns (ownership checked server-side). */
|
|
809
|
+
async getScene(artworkId) {
|
|
810
|
+
const data = await this.client.execute(AGENT_GET_SCENE, {
|
|
811
|
+
artworkId
|
|
812
|
+
});
|
|
774
813
|
return data.agentGetScene;
|
|
775
814
|
}
|
|
815
|
+
/** The account's workspace: every non-deleted artwork it owns. */
|
|
816
|
+
async listArtworks() {
|
|
817
|
+
const data = await this.client.execute(AGENT_LIST_ARTWORKS);
|
|
818
|
+
return data.agentListArtworks ?? [];
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Create a new panels (worldTarget) artwork owned by the session's account
|
|
822
|
+
* (the server forces ownership regardless of input). Mirrors the editor's
|
|
823
|
+
* new-panels defaults; the artwork starts published like the web flow.
|
|
824
|
+
*/
|
|
825
|
+
async createArtwork(params) {
|
|
826
|
+
const id = `Artwork-${globalThis.crypto.randomUUID()}`;
|
|
827
|
+
const input = {
|
|
828
|
+
id,
|
|
829
|
+
files: [],
|
|
830
|
+
creatorVersion: "agent-mcp",
|
|
831
|
+
status: "published",
|
|
832
|
+
artworkType: "worldTarget",
|
|
833
|
+
name: params?.name ?? "Untitled panels",
|
|
834
|
+
description: params?.description ?? "",
|
|
835
|
+
downloadsCount: 0,
|
|
836
|
+
viewsCount: 0,
|
|
837
|
+
publishEnabled: true,
|
|
838
|
+
enabled: true,
|
|
839
|
+
animationHasAlpha: false,
|
|
840
|
+
animationVideoOrientation: "horizontal"
|
|
841
|
+
};
|
|
842
|
+
const data = await this.client.execute(CREATE_ARTWORK, { input });
|
|
843
|
+
return data.createArtwork;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Presigned S3 staging pair for asset imports (P6): the agent PUTs local
|
|
847
|
+
* file bytes to uploadUrl, then hands downloadUrl to the browser (via the
|
|
848
|
+
* IMPORT_ASSET action) to pull into the normal asset pipeline.
|
|
849
|
+
*/
|
|
850
|
+
async createUploadUrl(filename, contentType) {
|
|
851
|
+
const data = await this.client.execute(AGENT_CREATE_UPLOAD_URL, { filename, contentType: contentType ?? null });
|
|
852
|
+
return data.agentCreateUploadUrl;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Lazy pre-write snapshot (P6a): idempotent per (session, artwork) —
|
|
856
|
+
* powers the editor's "Revert AI changes". saveScene calls this
|
|
857
|
+
* automatically before a session's first write to each artwork.
|
|
858
|
+
*/
|
|
859
|
+
async ensureSceneBackup(artworkId) {
|
|
860
|
+
const data = await this.client.execute(AGENT_ENSURE_SCENE_BACKUP, { artworkId });
|
|
861
|
+
return data.agentEnsureSceneBackup;
|
|
862
|
+
}
|
|
776
863
|
// --- Agent-side (session token) writes (P4) ---
|
|
777
864
|
/**
|
|
778
865
|
* Revision-checked scene write with a session token. Same mutation and
|
|
@@ -789,6 +876,13 @@ var AgentApi = class {
|
|
|
789
876
|
throw new Error(`Refusing to save invalid scene document: ${errors.map((i) => i.message).join("; ")}`);
|
|
790
877
|
}
|
|
791
878
|
}
|
|
879
|
+
if (!this.backupEnsured.has(params.artworkId)) {
|
|
880
|
+
try {
|
|
881
|
+
await this.ensureSceneBackup(params.artworkId);
|
|
882
|
+
this.backupEnsured.add(params.artworkId);
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
}
|
|
792
886
|
try {
|
|
793
887
|
const data = await this.client.execute(UPDATE_ARTWORK_SCENE, {
|
|
794
888
|
input: {
|
|
@@ -803,7 +897,7 @@ var AgentApi = class {
|
|
|
803
897
|
return { conflict: false, revision: payload.sceneRevision, modified: payload.modified ?? null, payload };
|
|
804
898
|
} catch (err) {
|
|
805
899
|
if (err instanceof GraphQLRequestError && err.errorType === "SceneRevisionConflict") {
|
|
806
|
-
const current = await this.getScene().catch(() => null);
|
|
900
|
+
const current = await this.getScene(params.artworkId).catch(() => null);
|
|
807
901
|
return { conflict: true, currentRevision: current?.sceneRevision ?? null, current };
|
|
808
902
|
}
|
|
809
903
|
throw err;
|
|
@@ -1001,6 +1095,8 @@ function loadConfig() {
|
|
|
1001
1095
|
|
|
1002
1096
|
// src/tools.ts
|
|
1003
1097
|
import { randomUUID } from "node:crypto";
|
|
1098
|
+
import { readFile } from "node:fs/promises";
|
|
1099
|
+
import { basename, extname } from "node:path";
|
|
1004
1100
|
import { z } from "zod";
|
|
1005
1101
|
var SCENE_CONTRACT = `Scene document conventions (the stored EyeJack format):
|
|
1006
1102
|
- Coordinate space is unity-style left-handed; rotations are in DEGREES.
|
|
@@ -1019,7 +1115,7 @@ function errorText(err) {
|
|
|
1019
1115
|
if ((err.errorType || "").includes("Unauthorized")) {
|
|
1020
1116
|
return REJECTED_HINT;
|
|
1021
1117
|
}
|
|
1022
|
-
return `EyeJack API error
|
|
1118
|
+
return `EyeJack API error (${err.errorType || "unknown"}): ${err.message}`;
|
|
1023
1119
|
}
|
|
1024
1120
|
return `Error: ${err.message}`;
|
|
1025
1121
|
}
|
|
@@ -1031,19 +1127,97 @@ var asError = (err) => ({
|
|
|
1031
1127
|
isError: true
|
|
1032
1128
|
});
|
|
1033
1129
|
function registerTools(server2, client2, options) {
|
|
1034
|
-
let
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1130
|
+
let resultSub = null;
|
|
1131
|
+
const pendingResults = /* @__PURE__ */ new Map();
|
|
1132
|
+
const ensureResultListener = async () => {
|
|
1133
|
+
if (resultSub) return;
|
|
1134
|
+
resultSub = client2.agent.watchActionResults(options.sessionId, {
|
|
1135
|
+
next: (result) => {
|
|
1136
|
+
const resolve = pendingResults.get(result.requestId);
|
|
1137
|
+
if (resolve) {
|
|
1138
|
+
pendingResults.delete(result.requestId);
|
|
1139
|
+
resolve(result);
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
error: (err) => {
|
|
1143
|
+
console.error("[eyejack-mcp] action result channel error:", err);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1147
|
+
};
|
|
1148
|
+
const requestOnce = async (actionType, payload, timeoutMs) => {
|
|
1149
|
+
const requestId = randomUUID();
|
|
1150
|
+
const waiter = new Promise((resolve) => {
|
|
1151
|
+
const timer = setTimeout(() => {
|
|
1152
|
+
pendingResults.delete(requestId);
|
|
1153
|
+
resolve(null);
|
|
1154
|
+
}, timeoutMs);
|
|
1155
|
+
pendingResults.set(requestId, (result) => {
|
|
1156
|
+
clearTimeout(timer);
|
|
1157
|
+
resolve(result);
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
await client2.agent.requestAction({
|
|
1161
|
+
sessionId: options.sessionId,
|
|
1162
|
+
requestId,
|
|
1163
|
+
actionType,
|
|
1164
|
+
payload
|
|
1165
|
+
});
|
|
1166
|
+
return waiter;
|
|
1167
|
+
};
|
|
1168
|
+
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.";
|
|
1169
|
+
const queryActiveArtwork = async () => {
|
|
1170
|
+
await ensureResultListener();
|
|
1171
|
+
const pong = await requestOnce("PING", null, 6e3);
|
|
1172
|
+
if (pong?.ok && pong.payload) {
|
|
1173
|
+
try {
|
|
1174
|
+
return JSON.parse(pong.payload).activeArtworkId ?? null;
|
|
1175
|
+
} catch {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1038
1178
|
}
|
|
1039
|
-
return
|
|
1179
|
+
return null;
|
|
1180
|
+
};
|
|
1181
|
+
const resolveArtworkId = async (explicit) => {
|
|
1182
|
+
if (explicit) return explicit;
|
|
1183
|
+
const active = await queryActiveArtwork();
|
|
1184
|
+
if (active) return active;
|
|
1185
|
+
throw new Error(NO_ARTWORK_HINT);
|
|
1186
|
+
};
|
|
1187
|
+
const artworkIdParam = {
|
|
1188
|
+
artworkId: z.string().optional().describe(
|
|
1189
|
+
'Target artwork id ("Artwork-\u2026"), any panels project the account owns. Omit to target the one open in the editor tab.'
|
|
1190
|
+
)
|
|
1040
1191
|
};
|
|
1041
1192
|
server2.tool(
|
|
1042
|
-
"
|
|
1043
|
-
|
|
1193
|
+
"list_artworks",
|
|
1194
|
+
"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.",
|
|
1044
1195
|
async () => {
|
|
1045
1196
|
try {
|
|
1046
|
-
const
|
|
1197
|
+
const artworks = await client2.agent.listArtworks();
|
|
1198
|
+
return asText(
|
|
1199
|
+
artworks.map((a) => ({
|
|
1200
|
+
id: a.id,
|
|
1201
|
+
name: a.name,
|
|
1202
|
+
artworkType: a.artworkType,
|
|
1203
|
+
status: a.status,
|
|
1204
|
+
sceneRevision: a.sceneRevision ?? 0,
|
|
1205
|
+
modified: a.modified
|
|
1206
|
+
}))
|
|
1207
|
+
);
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
return asError(err);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
);
|
|
1213
|
+
server2.tool(
|
|
1214
|
+
"get_scene",
|
|
1215
|
+
`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}`,
|
|
1216
|
+
artworkIdParam,
|
|
1217
|
+
async (args) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const id = await resolveArtworkId(args.artworkId);
|
|
1220
|
+
const record = await client2.agent.getScene(id);
|
|
1047
1221
|
const doc = parseSceneConfig(record.sceneConfig);
|
|
1048
1222
|
return asText({
|
|
1049
1223
|
artworkId: record.artworkId,
|
|
@@ -1068,10 +1242,12 @@ function registerTools(server2, client2, options) {
|
|
|
1068
1242
|
);
|
|
1069
1243
|
server2.tool(
|
|
1070
1244
|
"list_assets",
|
|
1071
|
-
"List the asset files (images, videos, 3D models) available on
|
|
1072
|
-
|
|
1245
|
+
"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.",
|
|
1246
|
+
artworkIdParam,
|
|
1247
|
+
async (args) => {
|
|
1073
1248
|
try {
|
|
1074
|
-
const
|
|
1249
|
+
const id = await resolveArtworkId(args.artworkId);
|
|
1250
|
+
const record = await client2.agent.getScene(id);
|
|
1075
1251
|
const doc = parseSceneConfig(record.sceneConfig);
|
|
1076
1252
|
const usedIds = new Set((doc?.scenes?.[0]?.children || []).map((c) => c.assetID));
|
|
1077
1253
|
return asText(
|
|
@@ -1089,8 +1265,55 @@ function registerTools(server2, client2, options) {
|
|
|
1089
1265
|
}
|
|
1090
1266
|
}
|
|
1091
1267
|
);
|
|
1268
|
+
const createArtworkParams = {
|
|
1269
|
+
name: z.string().min(1).max(120).optional().describe('Project name (default "Untitled panels").'),
|
|
1270
|
+
description: z.string().max(2e3).optional().describe("Optional description.")
|
|
1271
|
+
};
|
|
1272
|
+
server2.tool(
|
|
1273
|
+
"create_artwork",
|
|
1274
|
+
"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.",
|
|
1275
|
+
createArtworkParams,
|
|
1276
|
+
async (args) => {
|
|
1277
|
+
const { name, description } = args;
|
|
1278
|
+
try {
|
|
1279
|
+
const created = await client2.agent.createArtwork({ name, description });
|
|
1280
|
+
return asText({
|
|
1281
|
+
id: created.id,
|
|
1282
|
+
status: created.status,
|
|
1283
|
+
message: `Created "${name ?? "Untitled panels"}" (${created.id}). Use open_artwork to show it in the user's editor.`
|
|
1284
|
+
});
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
return asError(err);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
const openArtworkParams = {
|
|
1291
|
+
artworkId: z.string().describe('The panels artwork to open ("Artwork-\u2026").')
|
|
1292
|
+
};
|
|
1293
|
+
server2.tool(
|
|
1294
|
+
"open_artwork",
|
|
1295
|
+
"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.",
|
|
1296
|
+
openArtworkParams,
|
|
1297
|
+
async (args) => {
|
|
1298
|
+
const { artworkId } = args;
|
|
1299
|
+
try {
|
|
1300
|
+
await ensureResultListener();
|
|
1301
|
+
const result = await requestOnce("OPEN_ARTWORK", JSON.stringify({ artworkId }), 15e3);
|
|
1302
|
+
if (!result) {
|
|
1303
|
+
return asText("No response from the editor tab \u2014 is it open and visible?");
|
|
1304
|
+
}
|
|
1305
|
+
if (!result.ok) {
|
|
1306
|
+
return asText(`Could not open the artwork: ${result.error || "unknown error"}`);
|
|
1307
|
+
}
|
|
1308
|
+
return asText({ opened: artworkId });
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
return asError(err);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1092
1314
|
const updateSceneParams = {
|
|
1093
|
-
|
|
1315
|
+
artworkId: z.string().optional().describe("Target artwork id. Omit to write the one open in the editor tab."),
|
|
1316
|
+
expectedRevision: z.number().int().min(0).describe("The sceneRevision your edit is based on, from your latest read of THIS artwork."),
|
|
1094
1317
|
scene: z.record(z.any()).describe(
|
|
1095
1318
|
"The complete scene document to store: { version, settings?, assets, scenes }. Top-level keys are frozen to these; per-child additive keys are allowed."
|
|
1096
1319
|
),
|
|
@@ -1100,13 +1323,14 @@ function registerTools(server2, client2, options) {
|
|
|
1100
1323
|
};
|
|
1101
1324
|
server2.tool(
|
|
1102
1325
|
"update_scene",
|
|
1103
|
-
`Write
|
|
1326
|
+
`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}`,
|
|
1104
1327
|
updateSceneParams,
|
|
1105
1328
|
async (args) => {
|
|
1106
|
-
const { expectedRevision, scene, ops } = args;
|
|
1329
|
+
const { artworkId, expectedRevision, scene, ops } = args;
|
|
1107
1330
|
try {
|
|
1331
|
+
const id = await resolveArtworkId(artworkId);
|
|
1108
1332
|
const result = await client2.agent.saveScene({
|
|
1109
|
-
artworkId:
|
|
1333
|
+
artworkId: id,
|
|
1110
1334
|
doc: scene,
|
|
1111
1335
|
expectedRevision,
|
|
1112
1336
|
ops,
|
|
@@ -1115,6 +1339,7 @@ function registerTools(server2, client2, options) {
|
|
|
1115
1339
|
if (result.conflict) {
|
|
1116
1340
|
return asText({
|
|
1117
1341
|
conflict: true,
|
|
1342
|
+
artworkId: id,
|
|
1118
1343
|
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
1344
|
currentRevision: result.currentRevision,
|
|
1120
1345
|
currentScene: result.current ? parseSceneConfig(result.current.sceneConfig) : null
|
|
@@ -1122,8 +1347,9 @@ function registerTools(server2, client2, options) {
|
|
|
1122
1347
|
}
|
|
1123
1348
|
return asText({
|
|
1124
1349
|
conflict: false,
|
|
1350
|
+
artworkId: id,
|
|
1125
1351
|
revision: result.revision,
|
|
1126
|
-
message: `Saved (revision ${result.revision})
|
|
1352
|
+
message: `Saved (revision ${result.revision}).`
|
|
1127
1353
|
});
|
|
1128
1354
|
} catch (err) {
|
|
1129
1355
|
return asError(err);
|
|
@@ -1132,10 +1358,11 @@ function registerTools(server2, client2, options) {
|
|
|
1132
1358
|
);
|
|
1133
1359
|
server2.tool(
|
|
1134
1360
|
"publish_artwork",
|
|
1135
|
-
`Publish
|
|
1136
|
-
|
|
1361
|
+
`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.`,
|
|
1362
|
+
artworkIdParam,
|
|
1363
|
+
async (args) => {
|
|
1137
1364
|
try {
|
|
1138
|
-
const id = await artworkId
|
|
1365
|
+
const id = await resolveArtworkId(args.artworkId);
|
|
1139
1366
|
const artwork = await client2.agent.publish(id);
|
|
1140
1367
|
return asText({
|
|
1141
1368
|
id: artwork.id,
|
|
@@ -1145,67 +1372,106 @@ function registerTools(server2, client2, options) {
|
|
|
1145
1372
|
} catch (err) {
|
|
1146
1373
|
if (err instanceof GraphQLRequestError && `${err.errorType} ${err.message}`.includes("ConditionalCheckFailed")) {
|
|
1147
1374
|
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."
|
|
1375
|
+
"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."
|
|
1149
1376
|
);
|
|
1150
1377
|
}
|
|
1151
1378
|
return asError(err);
|
|
1152
1379
|
}
|
|
1153
1380
|
}
|
|
1154
1381
|
);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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));
|
|
1382
|
+
const IMPORT_MIME = {
|
|
1383
|
+
".png": "image/png",
|
|
1384
|
+
".jpg": "image/jpeg",
|
|
1385
|
+
".jpeg": "image/jpeg",
|
|
1386
|
+
".webp": "image/webp",
|
|
1387
|
+
".gif": "image/gif",
|
|
1388
|
+
".mp4": "video/mp4",
|
|
1389
|
+
".mov": "video/quicktime",
|
|
1390
|
+
".glb": "model/gltf-binary"
|
|
1172
1391
|
};
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
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;
|
|
1392
|
+
const importAssetParams = {
|
|
1393
|
+
filePath: z.string().describe("Absolute path to a local media file: .png/.jpg/.jpeg/.webp image, .gif/.mp4/.mov video, or .glb model."),
|
|
1394
|
+
artworkId: z.string().optional().describe("Target artwork. Omit for the one open in the editor tab; if another id is given, the tab is steered there first (imports run in the open editor).")
|
|
1192
1395
|
};
|
|
1396
|
+
server2.tool(
|
|
1397
|
+
"import_asset",
|
|
1398
|
+
"Bring NEW media into a project: upload a local file, which the user's open editor ingests through its normal asset pipeline (transcode, size limits, thumbnails) and auto-places in the scene. Returns the new asset's fileID \u2014 follow up with get_scene/update_scene to position it, and screenshot to verify. Requires the editor tab open; videos can take a minute or two to transcode. Per-project asset budget is ~3MB total, so prefer small optimized files.",
|
|
1399
|
+
importAssetParams,
|
|
1400
|
+
async (args) => {
|
|
1401
|
+
const { filePath, artworkId } = args;
|
|
1402
|
+
try {
|
|
1403
|
+
const ext = extname(filePath).toLowerCase();
|
|
1404
|
+
const contentType = IMPORT_MIME[ext];
|
|
1405
|
+
if (!contentType) {
|
|
1406
|
+
return asText(`Unsupported file type "${ext}". Supported: ${Object.keys(IMPORT_MIME).join(", ")}`);
|
|
1407
|
+
}
|
|
1408
|
+
const bytes = await readFile(filePath);
|
|
1409
|
+
if (bytes.length > 100 * 1024 * 1024) {
|
|
1410
|
+
return asText("File is larger than 100MB \u2014 far beyond the panels asset budget. Pick a smaller file.");
|
|
1411
|
+
}
|
|
1412
|
+
const filename = basename(filePath);
|
|
1413
|
+
const active = await queryActiveArtwork();
|
|
1414
|
+
let target = artworkId ?? active;
|
|
1415
|
+
if (!target) return asText(NO_ARTWORK_HINT);
|
|
1416
|
+
if (target !== active) {
|
|
1417
|
+
const opened = await requestOnce("OPEN_ARTWORK", JSON.stringify({ artworkId: target }), 15e3);
|
|
1418
|
+
if (!opened?.ok) {
|
|
1419
|
+
return asText(`Could not open ${target} in the editor first: ${opened?.error || "no response from the tab"}`);
|
|
1420
|
+
}
|
|
1421
|
+
await new Promise((resolve) => setTimeout(resolve, 4e3));
|
|
1422
|
+
}
|
|
1423
|
+
const staging = await client2.agent.createUploadUrl(filename, contentType);
|
|
1424
|
+
const put = await fetch(staging.uploadUrl, {
|
|
1425
|
+
method: "PUT",
|
|
1426
|
+
headers: { "content-type": contentType },
|
|
1427
|
+
body: new Uint8Array(bytes)
|
|
1428
|
+
});
|
|
1429
|
+
if (!put.ok) {
|
|
1430
|
+
return asText(`Staging upload failed (HTTP ${put.status}).`);
|
|
1431
|
+
}
|
|
1432
|
+
const result = await requestOnce(
|
|
1433
|
+
"IMPORT_ASSET",
|
|
1434
|
+
JSON.stringify({ artworkId: target, downloadUrl: staging.downloadUrl, filename, contentType }),
|
|
1435
|
+
24e4
|
|
1436
|
+
);
|
|
1437
|
+
if (!result) {
|
|
1438
|
+
return asText("No response from the editor tab \u2014 is it open and visible? (Large videos can also exceed the wait; check the editor and re-read the scene.)");
|
|
1439
|
+
}
|
|
1440
|
+
if (!result.ok) {
|
|
1441
|
+
return asText(`Import failed: ${result.error || "unknown error"}`);
|
|
1442
|
+
}
|
|
1443
|
+
let fileID = null;
|
|
1444
|
+
try {
|
|
1445
|
+
fileID = result.payload ? JSON.parse(result.payload).fileID ?? null : null;
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
return asText({
|
|
1449
|
+
ok: true,
|
|
1450
|
+
artworkId: target,
|
|
1451
|
+
fileID,
|
|
1452
|
+
message: "Imported through the editor pipeline and auto-placed in the scene. Use get_scene to see it (assetID == fileID) and update_scene to reposition; screenshot to verify visually."
|
|
1453
|
+
});
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
return asError(err);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
);
|
|
1193
1459
|
const screenshotParams = {
|
|
1194
1460
|
width: z.number().int().min(64).max(2048).optional().describe("Image width in px (default 1280; height follows the editor aspect)."),
|
|
1195
1461
|
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
1462
|
};
|
|
1197
1463
|
server2.tool(
|
|
1198
1464
|
"screenshot",
|
|
1199
|
-
"See the scene: capture a rendered JPEG of
|
|
1465
|
+
"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.",
|
|
1200
1466
|
screenshotParams,
|
|
1201
1467
|
async (args) => {
|
|
1202
1468
|
const { width, quality } = args;
|
|
1203
1469
|
try {
|
|
1204
1470
|
await ensureResultListener();
|
|
1205
1471
|
const payload = JSON.stringify({ width: width ?? 1280, quality: quality ?? 0.8 });
|
|
1206
|
-
let result = await requestOnce(payload, 2e4);
|
|
1472
|
+
let result = await requestOnce("SCREENSHOT", payload, 2e4);
|
|
1207
1473
|
if (!result) {
|
|
1208
|
-
result = await requestOnce(payload, 2e4);
|
|
1474
|
+
result = await requestOnce("SCREENSHOT", payload, 2e4);
|
|
1209
1475
|
}
|
|
1210
1476
|
if (!result) {
|
|
1211
1477
|
return asText(
|
|
@@ -1231,14 +1497,23 @@ function registerTools(server2, client2, options) {
|
|
|
1231
1497
|
);
|
|
1232
1498
|
server2.tool(
|
|
1233
1499
|
"get_session_status",
|
|
1234
|
-
"Inspect the agent session
|
|
1500
|
+
"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).",
|
|
1235
1501
|
async () => {
|
|
1236
1502
|
try {
|
|
1237
1503
|
const session = await client2.agent.getSession();
|
|
1238
1504
|
const now = Math.floor(Date.now() / 1e3);
|
|
1505
|
+
let activeArtworkId = null;
|
|
1506
|
+
try {
|
|
1507
|
+
await ensureResultListener();
|
|
1508
|
+
const pong = await requestOnce("PING", null, 5e3);
|
|
1509
|
+
if (pong?.ok && pong.payload) {
|
|
1510
|
+
activeArtworkId = JSON.parse(pong.payload).activeArtworkId ?? null;
|
|
1511
|
+
}
|
|
1512
|
+
} catch {
|
|
1513
|
+
}
|
|
1239
1514
|
return asText({
|
|
1240
|
-
artworkId: session.artworkId,
|
|
1241
1515
|
status: session.status,
|
|
1516
|
+
activeArtworkId,
|
|
1242
1517
|
expiresAt: session.expiresAt,
|
|
1243
1518
|
expiresInSeconds: session.expiresAt ? session.expiresAt - now : null,
|
|
1244
1519
|
lastHeartbeat: session.lastHeartbeat
|
|
@@ -1261,7 +1536,7 @@ var client = createCreatorClient({
|
|
|
1261
1536
|
});
|
|
1262
1537
|
var server = new McpServer({
|
|
1263
1538
|
name: "eyejack-creator",
|
|
1264
|
-
version: "0.
|
|
1539
|
+
version: "0.5.0"
|
|
1265
1540
|
});
|
|
1266
1541
|
registerTools(server, client, {
|
|
1267
1542
|
launchBaseUrl: config.launchBaseUrl,
|
|
@@ -1270,4 +1545,4 @@ registerTools(server, client, {
|
|
|
1270
1545
|
});
|
|
1271
1546
|
var transport = new StdioServerTransport();
|
|
1272
1547
|
await server.connect(transport);
|
|
1273
|
-
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014
|
|
1548
|
+
console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014 workspace + asset-import tools ready.`);
|
package/package.json
CHANGED