@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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +349 -74
  3. 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($artworkId: ID!) {
621
- createAgentSession(artworkId: $artworkId) {
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
- async createSession(artworkId) {
746
- const data = await this.client.execute(CREATE_AGENT_SESSION, { artworkId });
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
- async getScene() {
773
- const data = await this.client.execute(AGENT_GET_SCENE);
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: ${err.errorType || err.message}`;
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 cachedArtworkId = null;
1035
- const artworkId = async () => {
1036
- if (!cachedArtworkId) {
1037
- cachedArtworkId = (await client2.agent.getSession()).artworkId;
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 cachedArtworkId;
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
- "get_scene",
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}`,
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 record = await client2.agent.getScene();
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 the connected artwork \u2014 the palette the scene's panels can reference via assetID == fileID. Read-only.",
1072
- async () => {
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 record = await client2.agent.getScene();
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
- expectedRevision: z.number().int().min(0).describe("The sceneRevision your edit is based on, from your latest read."),
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 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}`,
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: await 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}). The user's editor has applied your change live.`
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 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 () => {
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
- 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));
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 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;
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 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.",
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 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).",
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.3.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 scene read/write + screenshot tools ready.`);
1548
+ console.error(`eyejack-mcp connected (stage: ${config.stage}) \u2014 workspace + asset-import tools ready.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyejack-creator/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",