@marlinjai/studio-mcp 0.1.0 → 0.1.1

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 CHANGED
@@ -101,7 +101,7 @@ The token is held in memory only. It is never logged, never serialized into a re
101
101
  | `studio_workflow_screenshot` | Render a saved workflow's document as a diagram (labelled boxes and wires, positioned by the document's own layout), the Figma `get_screenshot` equivalent for a workflow graph. Not a pixel-exact canvas preview, a diagnostic diagram for checking topology. Returns `{ url, width, height }` (a 24h signed link). Free, read-only. |
102
102
  | `studio_list_published` | List every published recipe: `{ recipes: [{ slug, label, publishedRevision, updatedAt, hasEffects }] }`. `hasEffects` tells you whether running it creates/attaches an entity record (a character, a brand reference) on success, versus a pure generation recipe. Free, read-only. |
103
103
  | `studio_describe_published` | Describe one published recipe by slug: `{ slug, definition, hasEffects }`. `definition` is the FROZEN workflow (params, node shapes) exactly as it was at publish time, never the live authoring document. Read this before `studio_run_published` to know what params it needs. Free, read-only. |
104
- | `studio_run_published` | Run a published recipe by slug against its frozen definition; `params` fill the definition's own param bindings. On success, the recipe's declared post-run effects fire automatically (e.g. minting creates the character and attaches its reference views); effects never fire on a failed or partial run. Each node is a paid job. |
104
+ | `studio_run_published` | Run a published recipe by slug against its frozen definition; `params` fill the definition's own param bindings. On success, the recipe's declared post-run effects fire automatically (e.g. minting creates the character and attaches its reference views); effects never fire on a failed, partial or cancelled run. Each node is a paid job. |
105
105
 
106
106
  All waiting tools accept `waitSeconds` (default 600) and back off between polls (2s doubling up to 15s). A timeout never cancels anything server-side: the tool error carries the id so the agent can poll again.
107
107
 
@@ -159,7 +159,7 @@ Two workflows behave differently from a document built through this contract:
159
159
 
160
160
  A **recipe** is a workflow that has been *published* under a stable public slug: its document and an optional declared list of post-run effects (create a character, attach a reference image, create a brand) are frozen together at publish time. `studio_list_published` / `studio_describe_published` / `studio_run_published` are the derived, read-only-until-you-run surface over every recipe that exists: the same three tools serve every recipe ever published, so the surface never grows as recipes are added.
161
161
 
162
- Running a recipe (`studio_run_published`) always executes the FROZEN definition, never the live authoring document (which may have moved on since publish). If every node in the run succeeds, the recipe's declared effects fire automatically and exactly once; a failed or partial run never triggers them, so a broken run can never leave a half-created character or a reference attached to nothing. Publishing a workflow (setting the slug and the effect list) is not yet exposed as an MCP tool: it is done through the HTTP API (`POST /api/v1/workflows/:savedId/publish`) or on the canvas.
162
+ Running a recipe (`studio_run_published`) always executes the FROZEN definition, never the live authoring document (which may have moved on since publish). If every node in the run succeeds, the recipe's declared effects fire automatically and exactly once; a failed, partial or cancelled run (one somebody stopped) never triggers them, so a broken run can never leave a half-created character or a reference attached to nothing. Publishing a workflow (setting the slug and the effect list) is not yet exposed as an MCP tool: it is done through the HTTP API (`POST /api/v1/workflows/:savedId/publish`) or on the canvas.
163
163
 
164
164
  `studio_mint_character` and `studio_replace_character_in_video` (above) are today still hand-written tools, not recipes; migrating them onto published recipe twins is planned but not yet done, so both surfaces currently coexist.
165
165
 
package/dist/cli.js CHANGED
@@ -213,8 +213,8 @@ var StudioClient = class {
213
213
  }
214
214
  /** S9: a rendered diagram of a saved workflow's document, so an agent can
215
215
  * see the graph it just built. */
216
- getWorkflowScreenshot(savedId2) {
217
- return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot`);
216
+ getWorkflowScreenshot(savedId2, opts = {}) {
217
+ return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot${projectQuery(opts.projectId)}`);
218
218
  }
219
219
  // ── Recipes (S10): published workflow + closed post-run effect list ──
220
220
  publishWorkflow(savedId2, body) {
@@ -239,9 +239,10 @@ var StudioClient = class {
239
239
  describeNodeTypes() {
240
240
  return this.json("GET", "/api/v1/node-types");
241
241
  }
242
- /** `savedId` carries a `saved:` prefix; the colon is URL-encoded. */
243
- getWorkflowDocument(savedId2) {
244
- return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}`);
242
+ /** `savedId` carries a `saved:` prefix; the colon is URL-encoded. `projectId`
243
+ * names the project the workflow lives in; absent, the Studio's default. */
244
+ getWorkflowDocument(savedId2, opts = {}) {
245
+ return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}${projectQuery(opts.projectId)}`);
245
246
  }
246
247
  /** Create a document by applying `ops` to an empty one (the same code path as apply). */
247
248
  createWorkflow(body) {
@@ -412,6 +413,9 @@ function mimeForFilename(name) {
412
413
  return "application/octet-stream";
413
414
  }
414
415
  }
416
+ function projectQuery(projectId) {
417
+ return projectId === void 0 ? "" : `?projectId=${encodeURIComponent(projectId)}`;
418
+ }
415
419
 
416
420
  // src/tools.ts
417
421
  import { z as z8 } from "zod";
@@ -634,6 +638,7 @@ var AssetIdSchema = z3.string().max(128);
634
638
  var PortTypeSchema = z3.enum(PORT_TYPES);
635
639
  var InputTypeSchema = z3.enum(INPUT_PORT_TYPES);
636
640
  var PositionSchema = z3.object({ x: z3.number().finite(), y: z3.number().finite() });
641
+ var CollapsedSchema = z3.boolean();
637
642
  var TaskNodeSchema = z3.object({
638
643
  id: IdSchema,
639
644
  kind: z3.literal("task"),
@@ -654,13 +659,15 @@ var TaskNodeSchema = z3.object({
654
659
  bakedInputs: z3.record(KeySchema, LiteralBindingSchema).default({}),
655
660
  /** The generation id that feeds downstream. Absent = latest (D4). */
656
661
  activeGeneration: IdSchema.optional(),
657
- label: LabelSchema.optional()
662
+ label: LabelSchema.optional(),
663
+ collapsed: CollapsedSchema.optional()
658
664
  });
659
665
  var PromptSourceNodeSchema = z3.object({
660
666
  id: IdSchema,
661
667
  kind: z3.literal("prompt-source"),
662
668
  text: PromptTextSchema.default(""),
663
- label: LabelSchema.optional()
669
+ label: LabelSchema.optional(),
670
+ collapsed: CollapsedSchema.optional()
664
671
  });
665
672
  var ImageSourceNodeSchema = z3.object({
666
673
  id: IdSchema,
@@ -671,7 +678,8 @@ var ImageSourceNodeSchema = z3.object({
671
678
  /** The Storage Brain asset id, when known (E18: every image input resolves to
672
679
  * an asset id). */
673
680
  assetId: AssetIdSchema.optional(),
674
- label: LabelSchema.optional()
681
+ label: LabelSchema.optional(),
682
+ collapsed: CollapsedSchema.optional()
675
683
  });
676
684
  var InputNodeSchema = z3.object({
677
685
  id: IdSchema,
@@ -686,7 +694,8 @@ var InputNodeSchema = z3.object({
686
694
  /** Used when a run omits the input. Never null: absent means no default. */
687
695
  default: JsonNonNullValueSchema.optional(),
688
696
  label: LabelSchema.optional(),
689
- description: DescriptionSchema.optional()
697
+ description: DescriptionSchema.optional(),
698
+ collapsed: CollapsedSchema.optional()
690
699
  });
691
700
  var DocNodeSchema = z3.discriminatedUnion("kind", [
692
701
  TaskNodeSchema,
@@ -704,6 +713,18 @@ var DocGroupSchema = z3.object({
704
713
  label: LabelSchema.optional(),
705
714
  nodeIds: z3.array(IdSchema).min(1)
706
715
  });
716
+ var EDGE_ROUTINGS = ["curved", "orthogonal"];
717
+ var EdgeRoutingSchema = z3.enum(EDGE_ROUTINGS);
718
+ var WireRouteSchema = z3.object({
719
+ from: PositionSchema,
720
+ to: PositionSchema,
721
+ points: z3.array(PositionSchema).max(64)
722
+ });
723
+ var DocumentViewSchema = z3.object({
724
+ edgeRouting: EdgeRoutingSchema.optional(),
725
+ /** Routed wires from the last Arrange, keyed by `edgeKey`. */
726
+ wires: z3.record(z3.string().max(512), WireRouteSchema).optional()
727
+ });
707
728
  var WorkflowDocumentSchema = z3.object({
708
729
  schema: z3.number().int().positive(),
709
730
  id: IdSchema,
@@ -711,7 +732,10 @@ var WorkflowDocumentSchema = z3.object({
711
732
  nodes: z3.array(DocNodeSchema).max(2e3),
712
733
  edges: z3.array(DocEdgeSchema).max(8e3),
713
734
  layout: z3.record(IdSchema, PositionSchema).default({}),
714
- groups: z3.array(DocGroupSchema).max(500).default([])
735
+ groups: z3.array(DocGroupSchema).max(500).default([]),
736
+ /** Display preferences; absent on every document written before they
737
+ * existed (additive, so `DOCUMENT_SCHEMA_VERSION` is unchanged). */
738
+ view: DocumentViewSchema.optional()
715
739
  });
716
740
  function edgeKey(e) {
717
741
  return JSON.stringify([e.source, e.target, e.port]);
@@ -1480,16 +1504,19 @@ var TaskNodePropsSchema = z4.object({
1480
1504
  task: z4.enum(MODEL_TASKS),
1481
1505
  model: ModelIdSchema,
1482
1506
  prompt: PromptTextSchema,
1483
- label: LabelSchema.nullable()
1507
+ label: LabelSchema.nullable(),
1508
+ collapsed: CollapsedSchema.nullable()
1484
1509
  }).partial().strict();
1485
1510
  var PromptSourcePropsSchema = z4.object({
1486
1511
  text: PromptTextSchema,
1487
- label: LabelSchema.nullable()
1512
+ label: LabelSchema.nullable(),
1513
+ collapsed: CollapsedSchema.nullable()
1488
1514
  }).partial().strict();
1489
1515
  var ImageSourcePropsSchema = z4.object({
1490
1516
  imageUrl: UrlSchema,
1491
1517
  assetId: AssetIdSchema.nullable(),
1492
- label: LabelSchema.nullable()
1518
+ label: LabelSchema.nullable(),
1519
+ collapsed: CollapsedSchema.nullable()
1493
1520
  }).partial().strict();
1494
1521
  var InputNodePropsSchema = z4.object({
1495
1522
  name: InputNameSchema,
@@ -1498,14 +1525,19 @@ var InputNodePropsSchema = z4.object({
1498
1525
  cardinality: z4.enum(["one", "many"]).nullable(),
1499
1526
  default: JsonNonNullValueSchema.nullable(),
1500
1527
  label: LabelSchema.nullable(),
1501
- description: DescriptionSchema.nullable()
1528
+ description: DescriptionSchema.nullable(),
1529
+ collapsed: CollapsedSchema.nullable()
1502
1530
  }).partial().strict();
1503
1531
  var GroupPropsSchema = z4.object({
1504
1532
  label: LabelSchema.nullable(),
1505
1533
  nodeIds: z4.array(IdSchema).min(1)
1506
1534
  }).partial().strict();
1507
1535
  var DocumentPropsSchema = z4.object({
1508
- label: LabelSchema
1536
+ label: LabelSchema,
1537
+ /** Display preferences (`view.edgeRouting`); null clears the whole
1538
+ * object. The object is replaced, not merged: pass every field you want
1539
+ * kept. */
1540
+ view: DocumentViewSchema.strict().nullable()
1509
1541
  }).partial().strict();
1510
1542
  var AddNodeOpSchema = z4.object({
1511
1543
  type: z4.literal("addNode"),
@@ -2076,7 +2108,7 @@ async function pollUntil(what, fetchState, isDone, opts = {}) {
2076
2108
  }
2077
2109
  }
2078
2110
  var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled"]);
2079
- var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial"]);
2111
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial", "cancelled"]);
2080
2112
  function isJobTerminal(res) {
2081
2113
  return TERMINAL_JOB_STATUSES.has(res.job.status);
2082
2114
  }
@@ -2409,11 +2441,12 @@ var studioWorkflowGet = defineTool({
2409
2441
  description: "Read a saved workflow. By default returns a summary: { savedId, revision, label, readOnly, nodes: [{ id, kind, label?, task?, model?, prompt? (first 80 chars, plus promptLength), imageUrl?, name?, type?, required?, wires: { in, out } }], edgeCount, groupCount, projection: { ok, issues } } where revision is the baseRevision for studio_workflow_apply and projection says whether the workflow can run and why not. A workflow the canvas cannot represent comes back as { readOnly: true, definitionNodeCount, projection, hint } with no nodes: it is readable and runnable (studio_run_workflow with its savedId) but not patchable. needsLossyAcknowledgement: true means its first patch must carry acknowledgeLossy: true. full: true returns the whole response instead; use it only when you need every field.",
2410
2442
  inputSchema: {
2411
2443
  savedId,
2444
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
2412
2445
  full: z7.boolean().default(false).describe("Return the whole response (the full document, or the stored definition of a read-only workflow) instead of the summary.")
2413
2446
  },
2414
2447
  annotations: { readOnlyHint: true },
2415
2448
  handler: async (args, ctx) => {
2416
- const res = await ctx.client.getWorkflowDocument(args.savedId);
2449
+ const res = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
2417
2450
  if (args.full) return res;
2418
2451
  if (res.readOnly === true || res.document === null) {
2419
2452
  const definition = isRecord(res.definition) ? res.definition : null;
@@ -2486,6 +2519,7 @@ var studioWorkflowApply = defineTool({
2486
2519
  description: 'Change a saved workflow atomically: the ops are applied all-or-nothing on top of baseRevision (the revision from studio_workflow_get, studio_workflow_create or your last apply). Success returns { ok: true, savedId, revision, appliedCount, inverseCount, affectedNodes (summaries of the nodes the patch touched, removed ones as { id, removed: true }), projection: { ok, issues }, idempotencyKey, replayed }. Every refusal comes back as { ok: false, code, hint } you can act on rather than an error: "stale_revision" (re-read, rebase your ops, apply again on the revision returned), "invalid_patch" (error.opIndex names the offending op), "forbidden" (opIndex plus the missingScope your credential needs), "not_editable" (this workflow is readable and runnable but cannot be patched), "lossy_import" (read notes, then resend with acknowledgeLossy: true and a NEW idempotencyKey), "idempotency_mismatch" (that key belongs to a different patch: use a fresh one), and "network_error" / "server_error" which you retry with the SAME idempotencyKey so the patch can never apply twice.',
2487
2520
  inputSchema: {
2488
2521
  savedId,
2522
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
2489
2523
  baseRevision: PatchSchema.shape.baseRevision.describe("The revision the ops were written against. The Studio refuses a stale one with stale_revision."),
2490
2524
  ops: opsSchema,
2491
2525
  idempotencyKey: PatchSchema.shape.idempotencyKey.describe(
@@ -2500,6 +2534,7 @@ var studioWorkflowApply = defineTool({
2500
2534
  let res;
2501
2535
  try {
2502
2536
  res = await ctx.client.applyWorkflowPatch(args.savedId, {
2537
+ ...args.projectId !== void 0 ? { projectId: args.projectId } : {},
2503
2538
  baseRevision: args.baseRevision,
2504
2539
  ops: args.ops,
2505
2540
  idempotencyKey,
@@ -2534,16 +2569,18 @@ var studioWorkflowValidate = defineTool({
2534
2569
  inputSchema: {
2535
2570
  document: WorkflowDocumentSchema.optional().describe("The document to validate. Either this or savedId."),
2536
2571
  savedId: savedId.optional().describe("Fetch this saved workflow first and validate against its current document. Either this or document, not both."),
2572
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the saved workflow lives in (only with savedId); omit for the default project."),
2537
2573
  ops: PatchSchema.shape.ops.optional().describe("Ops to apply to the document before projecting it (the same vocabulary and limits as studio_workflow_apply)."),
2538
2574
  params: paramsSchema.optional()
2539
2575
  },
2540
2576
  annotations: { readOnlyHint: true },
2541
2577
  handler: async (args, ctx) => {
2542
2578
  if (args.document && args.savedId) throw new ToolInputError("pass either `document` or `savedId`, not both");
2579
+ if (args.projectId !== void 0 && !args.savedId) throw new ToolInputError("`projectId` only applies when fetching by `savedId`");
2543
2580
  let document = args.document;
2544
2581
  if (!document) {
2545
2582
  if (!args.savedId) throw new ToolInputError("provide a `document` or a `savedId` to validate");
2546
- const saved = await ctx.client.getWorkflowDocument(args.savedId);
2583
+ const saved = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
2547
2584
  if (saved.readOnly === true || saved.document === null) {
2548
2585
  throw new ToolInputError(
2549
2586
  `workflow ${args.savedId} has no document to validate: the canvas cannot represent its stored definition (studio_workflow_get shows readOnly: true and why). It is still runnable with studio_run_workflow.`
@@ -2562,11 +2599,12 @@ var studioWorkflowScreenshot = defineTool({
2562
2599
  name: "studio_workflow_screenshot",
2563
2600
  description: "Render a saved workflow's document as a diagram (labelled boxes and wires, positioned by the document's own layout) so you can see the graph you just built, the Figma get_screenshot equivalent. NOT a pixel-exact preview of the real canvas: a plain diagnostic diagram for checking topology (is everything wired the way I meant, did that addNode land where I expected). Returns { url, width, height }; the url is a signed Storage Brain link, valid for 24h. Free, read-only.",
2564
2601
  inputSchema: {
2565
- savedId
2602
+ savedId,
2603
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project.")
2566
2604
  },
2567
2605
  annotations: { readOnlyHint: true },
2568
2606
  handler: async (args, ctx) => {
2569
- return ctx.client.getWorkflowScreenshot(args.savedId);
2607
+ return ctx.client.getWorkflowScreenshot(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
2570
2608
  }
2571
2609
  });
2572
2610
  var WORKFLOW_TOOLS = [
@@ -2599,6 +2637,8 @@ function projectJob(res) {
2599
2637
  function projectNodeRun(nr) {
2600
2638
  return {
2601
2639
  nodeId: nr.nodeId,
2640
+ // The node-run (generation) id: what `upstreamGenerations` pins.
2641
+ id: nr.id ?? null,
2602
2642
  status: nr.status,
2603
2643
  url: nr.resultUrl ?? nr.results?.[0]?.url ?? null,
2604
2644
  kind: nr.results?.[0]?.kind ?? null,
@@ -2856,13 +2896,16 @@ var workflowDefinitionSchema = WorkflowDefinitionSchema.describe(
2856
2896
  );
2857
2897
  var studioRunWorkflow = defineTool({
2858
2898
  name: "studio_run_workflow",
2859
- description: `Run a generation workflow DAG. Pass an inline definition (nodes of provider calls wired by ref bindings; this is what the hosted Studio accepts) or the savedId of a workflow saved in the Studio (the Studio runs that workflow's current document, exactly what the canvas would run). Params fill {kind:"param"} bindings. Returns the runId plus per-node status, URLs and cost; with wait=true (default) it blocks until the run is terminal (succeeded, failed, partial). Each node becomes a paid job.`,
2899
+ description: `Run a generation workflow DAG. Pass an inline definition (nodes of provider calls wired by ref bindings; this is what the hosted Studio accepts) or the savedId of a workflow saved in the Studio (the Studio runs that workflow's current document, exactly what the canvas would run). Params fill {kind:"param"} bindings. Returns the runId plus per-node status, URLs and cost; with wait=true (default) it blocks until the run is terminal (succeeded, failed, partial, cancelled). Each node becomes a paid job. Use an inline definition ONLY for the first batch of a new deliverable (one workflow, a node per item, a stable label); a follow-up on an existing item belongs in that saved workflow as a new node (studio_workflow_apply: addNode plus connect from the node it refines) run here with savedId and nodeIds, because every listed inline run is saved as its own workflow.`,
2860
2900
  inputSchema: {
2861
2901
  projectId: z8.string().min(1).describe("Project id from studio_list_projects."),
2862
2902
  definition: workflowDefinitionSchema.optional(),
2863
2903
  savedId: z8.string().optional().describe("Id of a saved workflow (from studio_workflow_create, studio_workflow_get or a listed run) to run instead of an inline definition. An unknown id is a 404 naming it; a workflow that is not runnable yet comes back as not_runnable with per-node issues."),
2864
2904
  params: z8.record(z8.string(), z8.unknown()).optional().describe("Values for the definition's params."),
2865
2905
  nodeIds: z8.array(z8.string()).optional().describe("Run only this subset of nodes (requires an inline `definition` or a `savedId`)."),
2906
+ upstreamGenerations: z8.record(z8.string(), z8.string()).optional().describe(
2907
+ 'Pin WHICH generation of an upstream node feeds this subset run: { "<upstream node id>": "<node-run id>" } (the per-node `id` that studio_get_run returns). A verified pin is reused even when the planner has no matching cache key for it (an inline first batch, or a node edited and reverted), so a follow-up node wired from an earlier generation runs against exactly that image. An unknown id is refused naming the node, never silently replaced by the latest.'
2908
+ ),
2866
2909
  runs: z8.number().int().min(1).max(8).optional().describe("Repeat a node subset N times (1 to 8)."),
2867
2910
  visibility: z8.enum(["listed", "unlisted"]).optional().describe("Whether the run shows in the Studio workflow list."),
2868
2911
  budgetUsd: z8.number().nonnegative().optional().describe(
@@ -2886,6 +2929,7 @@ var studioRunWorkflow = defineTool({
2886
2929
  ...args.definition ? { definition: args.definition } : {},
2887
2930
  ...args.params ? { params: args.params } : {},
2888
2931
  ...args.nodeIds && args.nodeIds.length > 0 ? { nodeIds: args.nodeIds } : {},
2932
+ ...args.upstreamGenerations ? { upstreamGenerations: args.upstreamGenerations } : {},
2889
2933
  ...args.runs !== void 0 ? { runs: args.runs } : {},
2890
2934
  ...args.savedId ? { savedId: args.savedId } : {},
2891
2935
  ...args.visibility ? { visibility: args.visibility } : {},
@@ -2899,7 +2943,7 @@ var studioRunWorkflow = defineTool({
2899
2943
  });
2900
2944
  var studioGetRun = defineTool({
2901
2945
  name: "studio_get_run",
2902
- description: "Fetch a workflow run by runId: overall status (pending, running, succeeded, failed, partial, awaiting_decision), total cost, and per-node status, result URL, cost and error message. Set wait=true to block until the run settles. A run at awaiting_decision is parked on a person at a judge node: waiting returns immediately with a `waiting` block naming the node and its candidates, because polling can never clear it; ask the user to choose and submit their answer with studio_decide_judge.",
2946
+ description: "Fetch a workflow run by runId: overall status (pending, running, succeeded, failed, partial, awaiting_decision, cancelled; cancelled means somebody stopped it), total cost, and per-node status, result URL, cost and error message. Set wait=true to block until the run settles. A run at awaiting_decision is parked on a person at a judge node: waiting returns immediately with a `waiting` block naming the node and its candidates, because polling can never clear it; ask the user to choose and submit their answer with studio_decide_judge.",
2903
2947
  inputSchema: {
2904
2948
  runId: z8.string().min(1),
2905
2949
  wait: z8.boolean().default(false),
@@ -3146,7 +3190,7 @@ function registerTools(server, ctx) {
3146
3190
 
3147
3191
  // src/server.ts
3148
3192
  var SERVER_NAME = "lumitra-studio";
3149
- var SERVER_VERSION = "0.1.0";
3193
+ var SERVER_VERSION = "0.1.1";
3150
3194
  function createStudioMcpServer(opts = {}) {
3151
3195
  const config = opts.config ?? resolveConfig(opts.env ?? process.env);
3152
3196
  const client = new StudioClient(config, {
@@ -3160,7 +3204,8 @@ function createStudioMcpServer(opts = {}) {
3160
3204
  "Lumitra Studio: brand-consistent image generation, character minting, workflow runs and the still-swap video recipe over the hosted Studio API.",
3161
3205
  "Start with studio_list_projects to get a projectId. Every generation costs real money (roughly $0.04 per image, $0.30 per character mint, $0.70 per video take); check studio_get_spend when in doubt.",
3162
3206
  "Long jobs: tools wait by default (up to timeoutSeconds, 600 by default) and return ids you can poll with studio_get_job / studio_get_run if they time out.",
3163
- "Authoring a workflow is free and revisioned: studio_workflow_describe for the vocabulary, studio_workflow_create to build one from ops, studio_workflow_apply to change it on top of a baseRevision (a stale_revision result means re-read, rebase, retry), studio_workflow_validate to dry-run before spending, then studio_run_workflow with its savedId."
3207
+ "Authoring a workflow is free and revisioned: studio_workflow_describe for the vocabulary, studio_workflow_create to build one from ops, studio_workflow_apply to change it on top of a baseRevision (a stale_revision result means re-read, rebase, retry), studio_workflow_validate to dry-run before spending, then studio_run_workflow with its savedId.",
3208
+ "Workflow hygiene: one deliverable is ONE saved workflow with a node per item, and a refinement of an item (edit, upscale, variation) is a NEW NODE appended to that workflow with studio_workflow_apply (addNode plus a connect from the node it refines, so the earlier generation is its input), then studio_run_workflow with savedId and nodeIds limited to the new node. Never send a fresh inline definition for a follow-up: every listed inline run is saved as its own workflow, which scatters one deliverable across duplicates."
3164
3209
  ].join(" ")
3165
3210
  }
3166
3211
  );
package/dist/index.d.ts CHANGED
@@ -54,17 +54,20 @@ declare const DocNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
54
54
  }, z.core.$strip>>>;
55
55
  activeGeneration: z.ZodOptional<z.ZodString>;
56
56
  label: z.ZodOptional<z.ZodString>;
57
+ collapsed: z.ZodOptional<z.ZodBoolean>;
57
58
  }, z.core.$strip>, z.ZodObject<{
58
59
  id: z.ZodString;
59
60
  kind: z.ZodLiteral<"prompt-source">;
60
61
  text: z.ZodDefault<z.ZodString>;
61
62
  label: z.ZodOptional<z.ZodString>;
63
+ collapsed: z.ZodOptional<z.ZodBoolean>;
62
64
  }, z.core.$strip>, z.ZodObject<{
63
65
  id: z.ZodString;
64
66
  kind: z.ZodLiteral<"image-source">;
65
67
  imageUrl: z.ZodDefault<z.ZodString>;
66
68
  assetId: z.ZodOptional<z.ZodString>;
67
69
  label: z.ZodOptional<z.ZodString>;
70
+ collapsed: z.ZodOptional<z.ZodBoolean>;
68
71
  }, z.core.$strip>, z.ZodObject<{
69
72
  id: z.ZodString;
70
73
  kind: z.ZodLiteral<"input">;
@@ -85,6 +88,7 @@ declare const DocNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
85
88
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
86
89
  label: z.ZodOptional<z.ZodString>;
87
90
  description: z.ZodOptional<z.ZodString>;
91
+ collapsed: z.ZodOptional<z.ZodBoolean>;
88
92
  }, z.core.$strip>], "kind">;
89
93
  type DocNode = z.infer<typeof DocNodeSchema>;
90
94
  type DocNodeKind = DocNode["kind"];
@@ -130,17 +134,20 @@ declare const WorkflowDocumentSchema: z.ZodObject<{
130
134
  }, z.core.$strip>>>;
131
135
  activeGeneration: z.ZodOptional<z.ZodString>;
132
136
  label: z.ZodOptional<z.ZodString>;
137
+ collapsed: z.ZodOptional<z.ZodBoolean>;
133
138
  }, z.core.$strip>, z.ZodObject<{
134
139
  id: z.ZodString;
135
140
  kind: z.ZodLiteral<"prompt-source">;
136
141
  text: z.ZodDefault<z.ZodString>;
137
142
  label: z.ZodOptional<z.ZodString>;
143
+ collapsed: z.ZodOptional<z.ZodBoolean>;
138
144
  }, z.core.$strip>, z.ZodObject<{
139
145
  id: z.ZodString;
140
146
  kind: z.ZodLiteral<"image-source">;
141
147
  imageUrl: z.ZodDefault<z.ZodString>;
142
148
  assetId: z.ZodOptional<z.ZodString>;
143
149
  label: z.ZodOptional<z.ZodString>;
150
+ collapsed: z.ZodOptional<z.ZodBoolean>;
144
151
  }, z.core.$strip>, z.ZodObject<{
145
152
  id: z.ZodString;
146
153
  kind: z.ZodLiteral<"input">;
@@ -161,6 +168,7 @@ declare const WorkflowDocumentSchema: z.ZodObject<{
161
168
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
162
169
  label: z.ZodOptional<z.ZodString>;
163
170
  description: z.ZodOptional<z.ZodString>;
171
+ collapsed: z.ZodOptional<z.ZodBoolean>;
164
172
  }, z.core.$strip>], "kind">>;
165
173
  edges: z.ZodArray<z.ZodObject<{
166
174
  source: z.ZodString;
@@ -184,6 +192,26 @@ declare const WorkflowDocumentSchema: z.ZodObject<{
184
192
  label: z.ZodOptional<z.ZodString>;
185
193
  nodeIds: z.ZodArray<z.ZodString>;
186
194
  }, z.core.$strip>>>;
195
+ view: z.ZodOptional<z.ZodObject<{
196
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
197
+ curved: "curved";
198
+ orthogonal: "orthogonal";
199
+ }>>;
200
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
201
+ from: z.ZodObject<{
202
+ x: z.ZodNumber;
203
+ y: z.ZodNumber;
204
+ }, z.core.$strip>;
205
+ to: z.ZodObject<{
206
+ x: z.ZodNumber;
207
+ y: z.ZodNumber;
208
+ }, z.core.$strip>;
209
+ points: z.ZodArray<z.ZodObject<{
210
+ x: z.ZodNumber;
211
+ y: z.ZodNumber;
212
+ }, z.core.$strip>>;
213
+ }, z.core.$strip>>>;
214
+ }, z.core.$strip>>;
187
215
  }, z.core.$strip>;
188
216
  type WorkflowDocument = z.infer<typeof WorkflowDocumentSchema>;
189
217
  declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -226,17 +254,20 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
226
254
  }, z.core.$strip>>>;
227
255
  activeGeneration: z.ZodOptional<z.ZodString>;
228
256
  label: z.ZodOptional<z.ZodString>;
257
+ collapsed: z.ZodOptional<z.ZodBoolean>;
229
258
  }, z.core.$strip>, z.ZodObject<{
230
259
  id: z.ZodString;
231
260
  kind: z.ZodLiteral<"prompt-source">;
232
261
  text: z.ZodDefault<z.ZodString>;
233
262
  label: z.ZodOptional<z.ZodString>;
263
+ collapsed: z.ZodOptional<z.ZodBoolean>;
234
264
  }, z.core.$strip>, z.ZodObject<{
235
265
  id: z.ZodString;
236
266
  kind: z.ZodLiteral<"image-source">;
237
267
  imageUrl: z.ZodDefault<z.ZodString>;
238
268
  assetId: z.ZodOptional<z.ZodString>;
239
269
  label: z.ZodOptional<z.ZodString>;
270
+ collapsed: z.ZodOptional<z.ZodBoolean>;
240
271
  }, z.core.$strip>, z.ZodObject<{
241
272
  id: z.ZodString;
242
273
  kind: z.ZodLiteral<"input">;
@@ -257,6 +288,7 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
257
288
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
258
289
  label: z.ZodOptional<z.ZodString>;
259
290
  description: z.ZodOptional<z.ZodString>;
291
+ collapsed: z.ZodOptional<z.ZodBoolean>;
260
292
  }, z.core.$strip>], "kind">;
261
293
  position: z.ZodOptional<z.ZodObject<{
262
294
  x: z.ZodNumber;
@@ -300,6 +332,7 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
300
332
  model: z.ZodOptional<z.ZodString>;
301
333
  prompt: z.ZodOptional<z.ZodString>;
302
334
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
335
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
303
336
  }, z.core.$strict>;
304
337
  }, z.core.$strip>, z.ZodObject<{
305
338
  type: z.ZodLiteral<"setNodeProps">;
@@ -308,6 +341,7 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
308
341
  props: z.ZodObject<{
309
342
  text: z.ZodOptional<z.ZodString>;
310
343
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
344
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
311
345
  }, z.core.$strict>;
312
346
  }, z.core.$strip>, z.ZodObject<{
313
347
  type: z.ZodLiteral<"setNodeProps">;
@@ -317,6 +351,7 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
317
351
  imageUrl: z.ZodOptional<z.ZodString>;
318
352
  assetId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
319
353
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
354
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
320
355
  }, z.core.$strict>;
321
356
  }, z.core.$strip>, z.ZodObject<{
322
357
  type: z.ZodLiteral<"setNodeProps">;
@@ -340,6 +375,7 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
340
375
  default: z.ZodOptional<z.ZodNullable<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>>;
341
376
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
342
377
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
378
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
343
379
  }, z.core.$strict>;
344
380
  }, z.core.$strip>], "kind">, z.ZodObject<{
345
381
  type: z.ZodLiteral<"setNodeSetting">;
@@ -426,6 +462,26 @@ declare const OpSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
426
462
  type: z.ZodLiteral<"setDocumentProps">;
427
463
  props: z.ZodObject<{
428
464
  label: z.ZodOptional<z.ZodString>;
465
+ view: z.ZodOptional<z.ZodNullable<z.ZodObject<{
466
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
467
+ curved: "curved";
468
+ orthogonal: "orthogonal";
469
+ }>>;
470
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
471
+ from: z.ZodObject<{
472
+ x: z.ZodNumber;
473
+ y: z.ZodNumber;
474
+ }, z.core.$strip>;
475
+ to: z.ZodObject<{
476
+ x: z.ZodNumber;
477
+ y: z.ZodNumber;
478
+ }, z.core.$strip>;
479
+ points: z.ZodArray<z.ZodObject<{
480
+ x: z.ZodNumber;
481
+ y: z.ZodNumber;
482
+ }, z.core.$strip>>;
483
+ }, z.core.$strip>>>;
484
+ }, z.core.$strict>>>;
429
485
  }, z.core.$strict>;
430
486
  }, z.core.$strip>], "type">;
431
487
  type Op = z.infer<typeof OpSchema>;
@@ -473,17 +529,20 @@ declare const PatchSchema: z.ZodObject<{
473
529
  }, z.core.$strip>>>;
474
530
  activeGeneration: z.ZodOptional<z.ZodString>;
475
531
  label: z.ZodOptional<z.ZodString>;
532
+ collapsed: z.ZodOptional<z.ZodBoolean>;
476
533
  }, z.core.$strip>, z.ZodObject<{
477
534
  id: z.ZodString;
478
535
  kind: z.ZodLiteral<"prompt-source">;
479
536
  text: z.ZodDefault<z.ZodString>;
480
537
  label: z.ZodOptional<z.ZodString>;
538
+ collapsed: z.ZodOptional<z.ZodBoolean>;
481
539
  }, z.core.$strip>, z.ZodObject<{
482
540
  id: z.ZodString;
483
541
  kind: z.ZodLiteral<"image-source">;
484
542
  imageUrl: z.ZodDefault<z.ZodString>;
485
543
  assetId: z.ZodOptional<z.ZodString>;
486
544
  label: z.ZodOptional<z.ZodString>;
545
+ collapsed: z.ZodOptional<z.ZodBoolean>;
487
546
  }, z.core.$strip>, z.ZodObject<{
488
547
  id: z.ZodString;
489
548
  kind: z.ZodLiteral<"input">;
@@ -504,6 +563,7 @@ declare const PatchSchema: z.ZodObject<{
504
563
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
505
564
  label: z.ZodOptional<z.ZodString>;
506
565
  description: z.ZodOptional<z.ZodString>;
566
+ collapsed: z.ZodOptional<z.ZodBoolean>;
507
567
  }, z.core.$strip>], "kind">;
508
568
  position: z.ZodOptional<z.ZodObject<{
509
569
  x: z.ZodNumber;
@@ -547,6 +607,7 @@ declare const PatchSchema: z.ZodObject<{
547
607
  model: z.ZodOptional<z.ZodString>;
548
608
  prompt: z.ZodOptional<z.ZodString>;
549
609
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
610
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
550
611
  }, z.core.$strict>;
551
612
  }, z.core.$strip>, z.ZodObject<{
552
613
  type: z.ZodLiteral<"setNodeProps">;
@@ -555,6 +616,7 @@ declare const PatchSchema: z.ZodObject<{
555
616
  props: z.ZodObject<{
556
617
  text: z.ZodOptional<z.ZodString>;
557
618
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
619
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
558
620
  }, z.core.$strict>;
559
621
  }, z.core.$strip>, z.ZodObject<{
560
622
  type: z.ZodLiteral<"setNodeProps">;
@@ -564,6 +626,7 @@ declare const PatchSchema: z.ZodObject<{
564
626
  imageUrl: z.ZodOptional<z.ZodString>;
565
627
  assetId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
566
628
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
629
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
567
630
  }, z.core.$strict>;
568
631
  }, z.core.$strip>, z.ZodObject<{
569
632
  type: z.ZodLiteral<"setNodeProps">;
@@ -587,6 +650,7 @@ declare const PatchSchema: z.ZodObject<{
587
650
  default: z.ZodOptional<z.ZodNullable<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>>;
588
651
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
589
652
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
653
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
590
654
  }, z.core.$strict>;
591
655
  }, z.core.$strip>], "kind">, z.ZodObject<{
592
656
  type: z.ZodLiteral<"setNodeSetting">;
@@ -673,6 +737,26 @@ declare const PatchSchema: z.ZodObject<{
673
737
  type: z.ZodLiteral<"setDocumentProps">;
674
738
  props: z.ZodObject<{
675
739
  label: z.ZodOptional<z.ZodString>;
740
+ view: z.ZodOptional<z.ZodNullable<z.ZodObject<{
741
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
742
+ curved: "curved";
743
+ orthogonal: "orthogonal";
744
+ }>>;
745
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
746
+ from: z.ZodObject<{
747
+ x: z.ZodNumber;
748
+ y: z.ZodNumber;
749
+ }, z.core.$strip>;
750
+ to: z.ZodObject<{
751
+ x: z.ZodNumber;
752
+ y: z.ZodNumber;
753
+ }, z.core.$strip>;
754
+ points: z.ZodArray<z.ZodObject<{
755
+ x: z.ZodNumber;
756
+ y: z.ZodNumber;
757
+ }, z.core.$strip>>;
758
+ }, z.core.$strip>>>;
759
+ }, z.core.$strict>>>;
676
760
  }, z.core.$strict>;
677
761
  }, z.core.$strip>], "type">>;
678
762
  idempotencyKey: z.ZodOptional<z.ZodString>;
@@ -801,7 +885,7 @@ interface CharacterReference {
801
885
  }
802
886
  /** `awaiting_decision` (P3 J2) is a run PARKED on a human decision at a judge
803
887
  * node: not terminal, not a failure, and polling will never clear it. */
804
- type RunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'partial' | 'awaiting_decision' | (string & {});
888
+ type RunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'partial' | 'awaiting_decision' | 'cancelled' | (string & {});
805
889
  type NodeStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'awaiting_decision' | (string & {});
806
890
  /** One candidate a parked judge node is waiting on a decision about. The url
807
891
  * is signed per response; `generationId` is what a decision names. */
@@ -816,6 +900,8 @@ interface JudgeCandidate {
816
900
  }
817
901
  interface NodeRun {
818
902
  nodeId: string;
903
+ /** The node-run (generation) id; pinnable via `upstreamGenerations`. */
904
+ id?: string;
819
905
  status: NodeStatus;
820
906
  jobId?: string | null;
821
907
  resultUrl?: string | null;
@@ -1109,6 +1195,7 @@ declare class StudioClient {
1109
1195
  definitionId?: string;
1110
1196
  params?: Record<string, unknown>;
1111
1197
  nodeIds?: string[];
1198
+ upstreamGenerations?: Record<string, string>;
1112
1199
  runs?: number;
1113
1200
  savedId?: string;
1114
1201
  visibility?: 'listed' | 'unlisted';
@@ -1136,7 +1223,9 @@ declare class StudioClient {
1136
1223
  getRunNodeUrl(runId: string, nodeId: string): Promise<NodeResultResponse>;
1137
1224
  /** S9: a rendered diagram of a saved workflow's document, so an agent can
1138
1225
  * see the graph it just built. */
1139
- getWorkflowScreenshot(savedId: string): Promise<{
1226
+ getWorkflowScreenshot(savedId: string, opts?: {
1227
+ projectId?: string;
1228
+ }): Promise<{
1140
1229
  url: string;
1141
1230
  width: number;
1142
1231
  height: number;
@@ -1185,8 +1274,11 @@ declare class StudioClient {
1185
1274
  savedId?: string;
1186
1275
  }): Promise<Record<string, unknown>>;
1187
1276
  describeNodeTypes(): Promise<NodeTypesResponse>;
1188
- /** `savedId` carries a `saved:` prefix; the colon is URL-encoded. */
1189
- getWorkflowDocument(savedId: string): Promise<WorkflowDocumentResponse>;
1277
+ /** `savedId` carries a `saved:` prefix; the colon is URL-encoded. `projectId`
1278
+ * names the project the workflow lives in; absent, the Studio's default. */
1279
+ getWorkflowDocument(savedId: string, opts?: {
1280
+ projectId?: string;
1281
+ }): Promise<WorkflowDocumentResponse>;
1190
1282
  /** Create a document by applying `ops` to an empty one (the same code path as apply). */
1191
1283
  createWorkflow(body: {
1192
1284
  ops: Op[];
@@ -1200,6 +1292,7 @@ declare class StudioClient {
1200
1292
  */
1201
1293
  applyWorkflowPatch(savedId: string, patch: Patch & {
1202
1294
  acknowledgeLossy?: boolean;
1295
+ projectId?: string;
1203
1296
  }): Promise<WorkflowApplyResponse>;
1204
1297
  /** Stateless dry-run of ops and run params against a document. No side effects. */
1205
1298
  validateWorkflow(body: {
@@ -1311,7 +1404,7 @@ declare function registerTools(server: McpServer, ctx: ToolContext): void;
1311
1404
  */
1312
1405
 
1313
1406
  declare const SERVER_NAME = "lumitra-studio";
1314
- declare const SERVER_VERSION = "0.1.0";
1407
+ declare const SERVER_VERSION = "0.1.1";
1315
1408
  interface CreateServerOptions {
1316
1409
  config?: StudioConfig;
1317
1410
  env?: NodeJS.ProcessEnv;
@@ -1360,6 +1453,7 @@ declare const studioWorkflowDescribe: ToolDef<{
1360
1453
  }>;
1361
1454
  declare const studioWorkflowGet: ToolDef<{
1362
1455
  savedId: z.ZodString;
1456
+ projectId: z.ZodOptional<z.ZodString>;
1363
1457
  full: z.ZodDefault<z.ZodBoolean>;
1364
1458
  }>;
1365
1459
  declare const studioWorkflowCreate: ToolDef<{
@@ -1403,17 +1497,20 @@ declare const studioWorkflowCreate: ToolDef<{
1403
1497
  }, z.core.$strip>>>;
1404
1498
  activeGeneration: z.ZodOptional<z.ZodString>;
1405
1499
  label: z.ZodOptional<z.ZodString>;
1500
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1406
1501
  }, z.core.$strip>, z.ZodObject<{
1407
1502
  id: z.ZodString;
1408
1503
  kind: z.ZodLiteral<"prompt-source">;
1409
1504
  text: z.ZodDefault<z.ZodString>;
1410
1505
  label: z.ZodOptional<z.ZodString>;
1506
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1411
1507
  }, z.core.$strip>, z.ZodObject<{
1412
1508
  id: z.ZodString;
1413
1509
  kind: z.ZodLiteral<"image-source">;
1414
1510
  imageUrl: z.ZodDefault<z.ZodString>;
1415
1511
  assetId: z.ZodOptional<z.ZodString>;
1416
1512
  label: z.ZodOptional<z.ZodString>;
1513
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1417
1514
  }, z.core.$strip>, z.ZodObject<{
1418
1515
  id: z.ZodString;
1419
1516
  kind: z.ZodLiteral<"input">;
@@ -1434,6 +1531,7 @@ declare const studioWorkflowCreate: ToolDef<{
1434
1531
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
1435
1532
  label: z.ZodOptional<z.ZodString>;
1436
1533
  description: z.ZodOptional<z.ZodString>;
1534
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1437
1535
  }, z.core.$strip>], "kind">;
1438
1536
  position: z.ZodOptional<z.ZodObject<{
1439
1537
  x: z.ZodNumber;
@@ -1477,6 +1575,7 @@ declare const studioWorkflowCreate: ToolDef<{
1477
1575
  model: z.ZodOptional<z.ZodString>;
1478
1576
  prompt: z.ZodOptional<z.ZodString>;
1479
1577
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1578
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1480
1579
  }, z.core.$strict>;
1481
1580
  }, z.core.$strip>, z.ZodObject<{
1482
1581
  type: z.ZodLiteral<"setNodeProps">;
@@ -1485,6 +1584,7 @@ declare const studioWorkflowCreate: ToolDef<{
1485
1584
  props: z.ZodObject<{
1486
1585
  text: z.ZodOptional<z.ZodString>;
1487
1586
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1587
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1488
1588
  }, z.core.$strict>;
1489
1589
  }, z.core.$strip>, z.ZodObject<{
1490
1590
  type: z.ZodLiteral<"setNodeProps">;
@@ -1494,6 +1594,7 @@ declare const studioWorkflowCreate: ToolDef<{
1494
1594
  imageUrl: z.ZodOptional<z.ZodString>;
1495
1595
  assetId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1496
1596
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1597
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1497
1598
  }, z.core.$strict>;
1498
1599
  }, z.core.$strip>, z.ZodObject<{
1499
1600
  type: z.ZodLiteral<"setNodeProps">;
@@ -1517,6 +1618,7 @@ declare const studioWorkflowCreate: ToolDef<{
1517
1618
  default: z.ZodOptional<z.ZodNullable<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>>;
1518
1619
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1519
1620
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1621
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1520
1622
  }, z.core.$strict>;
1521
1623
  }, z.core.$strip>], "kind">, z.ZodObject<{
1522
1624
  type: z.ZodLiteral<"setNodeSetting">;
@@ -1603,6 +1705,26 @@ declare const studioWorkflowCreate: ToolDef<{
1603
1705
  type: z.ZodLiteral<"setDocumentProps">;
1604
1706
  props: z.ZodObject<{
1605
1707
  label: z.ZodOptional<z.ZodString>;
1708
+ view: z.ZodOptional<z.ZodNullable<z.ZodObject<{
1709
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
1710
+ curved: "curved";
1711
+ orthogonal: "orthogonal";
1712
+ }>>;
1713
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
1714
+ from: z.ZodObject<{
1715
+ x: z.ZodNumber;
1716
+ y: z.ZodNumber;
1717
+ }, z.core.$strip>;
1718
+ to: z.ZodObject<{
1719
+ x: z.ZodNumber;
1720
+ y: z.ZodNumber;
1721
+ }, z.core.$strip>;
1722
+ points: z.ZodArray<z.ZodObject<{
1723
+ x: z.ZodNumber;
1724
+ y: z.ZodNumber;
1725
+ }, z.core.$strip>>;
1726
+ }, z.core.$strip>>>;
1727
+ }, z.core.$strict>>>;
1606
1728
  }, z.core.$strict>;
1607
1729
  }, z.core.$strip>], "type">>;
1608
1730
  label: z.ZodOptional<z.ZodString>;
@@ -1610,6 +1732,7 @@ declare const studioWorkflowCreate: ToolDef<{
1610
1732
  }>;
1611
1733
  declare const studioWorkflowApply: ToolDef<{
1612
1734
  savedId: z.ZodString;
1735
+ projectId: z.ZodOptional<z.ZodString>;
1613
1736
  baseRevision: z.ZodNumber;
1614
1737
  ops: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
1615
1738
  type: z.ZodLiteral<"addNode">;
@@ -1651,17 +1774,20 @@ declare const studioWorkflowApply: ToolDef<{
1651
1774
  }, z.core.$strip>>>;
1652
1775
  activeGeneration: z.ZodOptional<z.ZodString>;
1653
1776
  label: z.ZodOptional<z.ZodString>;
1777
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1654
1778
  }, z.core.$strip>, z.ZodObject<{
1655
1779
  id: z.ZodString;
1656
1780
  kind: z.ZodLiteral<"prompt-source">;
1657
1781
  text: z.ZodDefault<z.ZodString>;
1658
1782
  label: z.ZodOptional<z.ZodString>;
1783
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1659
1784
  }, z.core.$strip>, z.ZodObject<{
1660
1785
  id: z.ZodString;
1661
1786
  kind: z.ZodLiteral<"image-source">;
1662
1787
  imageUrl: z.ZodDefault<z.ZodString>;
1663
1788
  assetId: z.ZodOptional<z.ZodString>;
1664
1789
  label: z.ZodOptional<z.ZodString>;
1790
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1665
1791
  }, z.core.$strip>, z.ZodObject<{
1666
1792
  id: z.ZodString;
1667
1793
  kind: z.ZodLiteral<"input">;
@@ -1682,6 +1808,7 @@ declare const studioWorkflowApply: ToolDef<{
1682
1808
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
1683
1809
  label: z.ZodOptional<z.ZodString>;
1684
1810
  description: z.ZodOptional<z.ZodString>;
1811
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1685
1812
  }, z.core.$strip>], "kind">;
1686
1813
  position: z.ZodOptional<z.ZodObject<{
1687
1814
  x: z.ZodNumber;
@@ -1725,6 +1852,7 @@ declare const studioWorkflowApply: ToolDef<{
1725
1852
  model: z.ZodOptional<z.ZodString>;
1726
1853
  prompt: z.ZodOptional<z.ZodString>;
1727
1854
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1855
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1728
1856
  }, z.core.$strict>;
1729
1857
  }, z.core.$strip>, z.ZodObject<{
1730
1858
  type: z.ZodLiteral<"setNodeProps">;
@@ -1733,6 +1861,7 @@ declare const studioWorkflowApply: ToolDef<{
1733
1861
  props: z.ZodObject<{
1734
1862
  text: z.ZodOptional<z.ZodString>;
1735
1863
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1864
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1736
1865
  }, z.core.$strict>;
1737
1866
  }, z.core.$strip>, z.ZodObject<{
1738
1867
  type: z.ZodLiteral<"setNodeProps">;
@@ -1742,6 +1871,7 @@ declare const studioWorkflowApply: ToolDef<{
1742
1871
  imageUrl: z.ZodOptional<z.ZodString>;
1743
1872
  assetId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1744
1873
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1874
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1745
1875
  }, z.core.$strict>;
1746
1876
  }, z.core.$strip>, z.ZodObject<{
1747
1877
  type: z.ZodLiteral<"setNodeProps">;
@@ -1765,6 +1895,7 @@ declare const studioWorkflowApply: ToolDef<{
1765
1895
  default: z.ZodOptional<z.ZodNullable<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>>;
1766
1896
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1767
1897
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1898
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
1768
1899
  }, z.core.$strict>;
1769
1900
  }, z.core.$strip>], "kind">, z.ZodObject<{
1770
1901
  type: z.ZodLiteral<"setNodeSetting">;
@@ -1851,6 +1982,26 @@ declare const studioWorkflowApply: ToolDef<{
1851
1982
  type: z.ZodLiteral<"setDocumentProps">;
1852
1983
  props: z.ZodObject<{
1853
1984
  label: z.ZodOptional<z.ZodString>;
1985
+ view: z.ZodOptional<z.ZodNullable<z.ZodObject<{
1986
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
1987
+ curved: "curved";
1988
+ orthogonal: "orthogonal";
1989
+ }>>;
1990
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
1991
+ from: z.ZodObject<{
1992
+ x: z.ZodNumber;
1993
+ y: z.ZodNumber;
1994
+ }, z.core.$strip>;
1995
+ to: z.ZodObject<{
1996
+ x: z.ZodNumber;
1997
+ y: z.ZodNumber;
1998
+ }, z.core.$strip>;
1999
+ points: z.ZodArray<z.ZodObject<{
2000
+ x: z.ZodNumber;
2001
+ y: z.ZodNumber;
2002
+ }, z.core.$strip>>;
2003
+ }, z.core.$strip>>>;
2004
+ }, z.core.$strict>>>;
1854
2005
  }, z.core.$strict>;
1855
2006
  }, z.core.$strip>], "type">>;
1856
2007
  idempotencyKey: z.ZodOptional<z.ZodString>;
@@ -1899,17 +2050,20 @@ declare const studioWorkflowValidate: ToolDef<{
1899
2050
  }, z.core.$strip>>>;
1900
2051
  activeGeneration: z.ZodOptional<z.ZodString>;
1901
2052
  label: z.ZodOptional<z.ZodString>;
2053
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1902
2054
  }, z.core.$strip>, z.ZodObject<{
1903
2055
  id: z.ZodString;
1904
2056
  kind: z.ZodLiteral<"prompt-source">;
1905
2057
  text: z.ZodDefault<z.ZodString>;
1906
2058
  label: z.ZodOptional<z.ZodString>;
2059
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1907
2060
  }, z.core.$strip>, z.ZodObject<{
1908
2061
  id: z.ZodString;
1909
2062
  kind: z.ZodLiteral<"image-source">;
1910
2063
  imageUrl: z.ZodDefault<z.ZodString>;
1911
2064
  assetId: z.ZodOptional<z.ZodString>;
1912
2065
  label: z.ZodOptional<z.ZodString>;
2066
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1913
2067
  }, z.core.$strip>, z.ZodObject<{
1914
2068
  id: z.ZodString;
1915
2069
  kind: z.ZodLiteral<"input">;
@@ -1930,6 +2084,7 @@ declare const studioWorkflowValidate: ToolDef<{
1930
2084
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
1931
2085
  label: z.ZodOptional<z.ZodString>;
1932
2086
  description: z.ZodOptional<z.ZodString>;
2087
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1933
2088
  }, z.core.$strip>], "kind">>;
1934
2089
  edges: z.ZodArray<z.ZodObject<{
1935
2090
  source: z.ZodString;
@@ -1953,8 +2108,29 @@ declare const studioWorkflowValidate: ToolDef<{
1953
2108
  label: z.ZodOptional<z.ZodString>;
1954
2109
  nodeIds: z.ZodArray<z.ZodString>;
1955
2110
  }, z.core.$strip>>>;
2111
+ view: z.ZodOptional<z.ZodObject<{
2112
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
2113
+ curved: "curved";
2114
+ orthogonal: "orthogonal";
2115
+ }>>;
2116
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
2117
+ from: z.ZodObject<{
2118
+ x: z.ZodNumber;
2119
+ y: z.ZodNumber;
2120
+ }, z.core.$strip>;
2121
+ to: z.ZodObject<{
2122
+ x: z.ZodNumber;
2123
+ y: z.ZodNumber;
2124
+ }, z.core.$strip>;
2125
+ points: z.ZodArray<z.ZodObject<{
2126
+ x: z.ZodNumber;
2127
+ y: z.ZodNumber;
2128
+ }, z.core.$strip>>;
2129
+ }, z.core.$strip>>>;
2130
+ }, z.core.$strip>>;
1956
2131
  }, z.core.$strip>>;
1957
2132
  savedId: z.ZodOptional<z.ZodString>;
2133
+ projectId: z.ZodOptional<z.ZodString>;
1958
2134
  ops: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
1959
2135
  type: z.ZodLiteral<"addNode">;
1960
2136
  node: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -1995,17 +2171,20 @@ declare const studioWorkflowValidate: ToolDef<{
1995
2171
  }, z.core.$strip>>>;
1996
2172
  activeGeneration: z.ZodOptional<z.ZodString>;
1997
2173
  label: z.ZodOptional<z.ZodString>;
2174
+ collapsed: z.ZodOptional<z.ZodBoolean>;
1998
2175
  }, z.core.$strip>, z.ZodObject<{
1999
2176
  id: z.ZodString;
2000
2177
  kind: z.ZodLiteral<"prompt-source">;
2001
2178
  text: z.ZodDefault<z.ZodString>;
2002
2179
  label: z.ZodOptional<z.ZodString>;
2180
+ collapsed: z.ZodOptional<z.ZodBoolean>;
2003
2181
  }, z.core.$strip>, z.ZodObject<{
2004
2182
  id: z.ZodString;
2005
2183
  kind: z.ZodLiteral<"image-source">;
2006
2184
  imageUrl: z.ZodDefault<z.ZodString>;
2007
2185
  assetId: z.ZodOptional<z.ZodString>;
2008
2186
  label: z.ZodOptional<z.ZodString>;
2187
+ collapsed: z.ZodOptional<z.ZodBoolean>;
2009
2188
  }, z.core.$strip>, z.ZodObject<{
2010
2189
  id: z.ZodString;
2011
2190
  kind: z.ZodLiteral<"input">;
@@ -2026,6 +2205,7 @@ declare const studioWorkflowValidate: ToolDef<{
2026
2205
  default: z.ZodOptional<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>;
2027
2206
  label: z.ZodOptional<z.ZodString>;
2028
2207
  description: z.ZodOptional<z.ZodString>;
2208
+ collapsed: z.ZodOptional<z.ZodBoolean>;
2029
2209
  }, z.core.$strip>], "kind">;
2030
2210
  position: z.ZodOptional<z.ZodObject<{
2031
2211
  x: z.ZodNumber;
@@ -2069,6 +2249,7 @@ declare const studioWorkflowValidate: ToolDef<{
2069
2249
  model: z.ZodOptional<z.ZodString>;
2070
2250
  prompt: z.ZodOptional<z.ZodString>;
2071
2251
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2252
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
2072
2253
  }, z.core.$strict>;
2073
2254
  }, z.core.$strip>, z.ZodObject<{
2074
2255
  type: z.ZodLiteral<"setNodeProps">;
@@ -2077,6 +2258,7 @@ declare const studioWorkflowValidate: ToolDef<{
2077
2258
  props: z.ZodObject<{
2078
2259
  text: z.ZodOptional<z.ZodString>;
2079
2260
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2261
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
2080
2262
  }, z.core.$strict>;
2081
2263
  }, z.core.$strip>, z.ZodObject<{
2082
2264
  type: z.ZodLiteral<"setNodeProps">;
@@ -2086,6 +2268,7 @@ declare const studioWorkflowValidate: ToolDef<{
2086
2268
  imageUrl: z.ZodOptional<z.ZodString>;
2087
2269
  assetId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2088
2270
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2271
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
2089
2272
  }, z.core.$strict>;
2090
2273
  }, z.core.$strip>, z.ZodObject<{
2091
2274
  type: z.ZodLiteral<"setNodeProps">;
@@ -2109,6 +2292,7 @@ declare const studioWorkflowValidate: ToolDef<{
2109
2292
  default: z.ZodOptional<z.ZodNullable<z.ZodType<JsonNonNullValue, unknown, z.core.$ZodTypeInternals<JsonNonNullValue, unknown>>>>;
2110
2293
  label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2111
2294
  description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2295
+ collapsed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
2112
2296
  }, z.core.$strict>;
2113
2297
  }, z.core.$strip>], "kind">, z.ZodObject<{
2114
2298
  type: z.ZodLiteral<"setNodeSetting">;
@@ -2195,12 +2379,33 @@ declare const studioWorkflowValidate: ToolDef<{
2195
2379
  type: z.ZodLiteral<"setDocumentProps">;
2196
2380
  props: z.ZodObject<{
2197
2381
  label: z.ZodOptional<z.ZodString>;
2382
+ view: z.ZodOptional<z.ZodNullable<z.ZodObject<{
2383
+ edgeRouting: z.ZodOptional<z.ZodEnum<{
2384
+ curved: "curved";
2385
+ orthogonal: "orthogonal";
2386
+ }>>;
2387
+ wires: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
2388
+ from: z.ZodObject<{
2389
+ x: z.ZodNumber;
2390
+ y: z.ZodNumber;
2391
+ }, z.core.$strip>;
2392
+ to: z.ZodObject<{
2393
+ x: z.ZodNumber;
2394
+ y: z.ZodNumber;
2395
+ }, z.core.$strip>;
2396
+ points: z.ZodArray<z.ZodObject<{
2397
+ x: z.ZodNumber;
2398
+ y: z.ZodNumber;
2399
+ }, z.core.$strip>>;
2400
+ }, z.core.$strip>>>;
2401
+ }, z.core.$strict>>>;
2198
2402
  }, z.core.$strict>;
2199
2403
  }, z.core.$strip>], "type">>>;
2200
2404
  params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2201
2405
  }>;
2202
2406
  declare const studioWorkflowScreenshot: ToolDef<{
2203
2407
  savedId: z.ZodString;
2408
+ projectId: z.ZodOptional<z.ZodString>;
2204
2409
  }>;
2205
2410
  /** The six primitives, in registration order. */
2206
2411
  declare const WORKFLOW_TOOLS: readonly ToolDef<z.ZodRawShape>[];
package/dist/index.js CHANGED
@@ -162,8 +162,8 @@ var StudioClient = class {
162
162
  }
163
163
  /** S9: a rendered diagram of a saved workflow's document, so an agent can
164
164
  * see the graph it just built. */
165
- getWorkflowScreenshot(savedId2) {
166
- return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot`);
165
+ getWorkflowScreenshot(savedId2, opts = {}) {
166
+ return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}/screenshot${projectQuery(opts.projectId)}`);
167
167
  }
168
168
  // ── Recipes (S10): published workflow + closed post-run effect list ──
169
169
  publishWorkflow(savedId2, body) {
@@ -188,9 +188,10 @@ var StudioClient = class {
188
188
  describeNodeTypes() {
189
189
  return this.json("GET", "/api/v1/node-types");
190
190
  }
191
- /** `savedId` carries a `saved:` prefix; the colon is URL-encoded. */
192
- getWorkflowDocument(savedId2) {
193
- return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}`);
191
+ /** `savedId` carries a `saved:` prefix; the colon is URL-encoded. `projectId`
192
+ * names the project the workflow lives in; absent, the Studio's default. */
193
+ getWorkflowDocument(savedId2, opts = {}) {
194
+ return this.json("GET", `/api/v1/workflows/${encodeURIComponent(savedId2)}${projectQuery(opts.projectId)}`);
194
195
  }
195
196
  /** Create a document by applying `ops` to an empty one (the same code path as apply). */
196
197
  createWorkflow(body) {
@@ -361,6 +362,9 @@ function mimeForFilename(name) {
361
362
  return "application/octet-stream";
362
363
  }
363
364
  }
365
+ function projectQuery(projectId) {
366
+ return projectId === void 0 ? "" : `?projectId=${encodeURIComponent(projectId)}`;
367
+ }
364
368
 
365
369
  // src/config.ts
366
370
  var DEFAULT_BASE_URL = "https://studio.lumitra.co";
@@ -626,6 +630,7 @@ var AssetIdSchema = z3.string().max(128);
626
630
  var PortTypeSchema = z3.enum(PORT_TYPES);
627
631
  var InputTypeSchema = z3.enum(INPUT_PORT_TYPES);
628
632
  var PositionSchema = z3.object({ x: z3.number().finite(), y: z3.number().finite() });
633
+ var CollapsedSchema = z3.boolean();
629
634
  var TaskNodeSchema = z3.object({
630
635
  id: IdSchema,
631
636
  kind: z3.literal("task"),
@@ -646,13 +651,15 @@ var TaskNodeSchema = z3.object({
646
651
  bakedInputs: z3.record(KeySchema, LiteralBindingSchema).default({}),
647
652
  /** The generation id that feeds downstream. Absent = latest (D4). */
648
653
  activeGeneration: IdSchema.optional(),
649
- label: LabelSchema.optional()
654
+ label: LabelSchema.optional(),
655
+ collapsed: CollapsedSchema.optional()
650
656
  });
651
657
  var PromptSourceNodeSchema = z3.object({
652
658
  id: IdSchema,
653
659
  kind: z3.literal("prompt-source"),
654
660
  text: PromptTextSchema.default(""),
655
- label: LabelSchema.optional()
661
+ label: LabelSchema.optional(),
662
+ collapsed: CollapsedSchema.optional()
656
663
  });
657
664
  var ImageSourceNodeSchema = z3.object({
658
665
  id: IdSchema,
@@ -663,7 +670,8 @@ var ImageSourceNodeSchema = z3.object({
663
670
  /** The Storage Brain asset id, when known (E18: every image input resolves to
664
671
  * an asset id). */
665
672
  assetId: AssetIdSchema.optional(),
666
- label: LabelSchema.optional()
673
+ label: LabelSchema.optional(),
674
+ collapsed: CollapsedSchema.optional()
667
675
  });
668
676
  var InputNodeSchema = z3.object({
669
677
  id: IdSchema,
@@ -678,7 +686,8 @@ var InputNodeSchema = z3.object({
678
686
  /** Used when a run omits the input. Never null: absent means no default. */
679
687
  default: JsonNonNullValueSchema.optional(),
680
688
  label: LabelSchema.optional(),
681
- description: DescriptionSchema.optional()
689
+ description: DescriptionSchema.optional(),
690
+ collapsed: CollapsedSchema.optional()
682
691
  });
683
692
  var DocNodeSchema = z3.discriminatedUnion("kind", [
684
693
  TaskNodeSchema,
@@ -696,6 +705,18 @@ var DocGroupSchema = z3.object({
696
705
  label: LabelSchema.optional(),
697
706
  nodeIds: z3.array(IdSchema).min(1)
698
707
  });
708
+ var EDGE_ROUTINGS = ["curved", "orthogonal"];
709
+ var EdgeRoutingSchema = z3.enum(EDGE_ROUTINGS);
710
+ var WireRouteSchema = z3.object({
711
+ from: PositionSchema,
712
+ to: PositionSchema,
713
+ points: z3.array(PositionSchema).max(64)
714
+ });
715
+ var DocumentViewSchema = z3.object({
716
+ edgeRouting: EdgeRoutingSchema.optional(),
717
+ /** Routed wires from the last Arrange, keyed by `edgeKey`. */
718
+ wires: z3.record(z3.string().max(512), WireRouteSchema).optional()
719
+ });
699
720
  var WorkflowDocumentSchema = z3.object({
700
721
  schema: z3.number().int().positive(),
701
722
  id: IdSchema,
@@ -703,7 +724,10 @@ var WorkflowDocumentSchema = z3.object({
703
724
  nodes: z3.array(DocNodeSchema).max(2e3),
704
725
  edges: z3.array(DocEdgeSchema).max(8e3),
705
726
  layout: z3.record(IdSchema, PositionSchema).default({}),
706
- groups: z3.array(DocGroupSchema).max(500).default([])
727
+ groups: z3.array(DocGroupSchema).max(500).default([]),
728
+ /** Display preferences; absent on every document written before they
729
+ * existed (additive, so `DOCUMENT_SCHEMA_VERSION` is unchanged). */
730
+ view: DocumentViewSchema.optional()
707
731
  });
708
732
  function edgeKey(e) {
709
733
  return JSON.stringify([e.source, e.target, e.port]);
@@ -1472,16 +1496,19 @@ var TaskNodePropsSchema = z4.object({
1472
1496
  task: z4.enum(MODEL_TASKS),
1473
1497
  model: ModelIdSchema,
1474
1498
  prompt: PromptTextSchema,
1475
- label: LabelSchema.nullable()
1499
+ label: LabelSchema.nullable(),
1500
+ collapsed: CollapsedSchema.nullable()
1476
1501
  }).partial().strict();
1477
1502
  var PromptSourcePropsSchema = z4.object({
1478
1503
  text: PromptTextSchema,
1479
- label: LabelSchema.nullable()
1504
+ label: LabelSchema.nullable(),
1505
+ collapsed: CollapsedSchema.nullable()
1480
1506
  }).partial().strict();
1481
1507
  var ImageSourcePropsSchema = z4.object({
1482
1508
  imageUrl: UrlSchema,
1483
1509
  assetId: AssetIdSchema.nullable(),
1484
- label: LabelSchema.nullable()
1510
+ label: LabelSchema.nullable(),
1511
+ collapsed: CollapsedSchema.nullable()
1485
1512
  }).partial().strict();
1486
1513
  var InputNodePropsSchema = z4.object({
1487
1514
  name: InputNameSchema,
@@ -1490,14 +1517,19 @@ var InputNodePropsSchema = z4.object({
1490
1517
  cardinality: z4.enum(["one", "many"]).nullable(),
1491
1518
  default: JsonNonNullValueSchema.nullable(),
1492
1519
  label: LabelSchema.nullable(),
1493
- description: DescriptionSchema.nullable()
1520
+ description: DescriptionSchema.nullable(),
1521
+ collapsed: CollapsedSchema.nullable()
1494
1522
  }).partial().strict();
1495
1523
  var GroupPropsSchema = z4.object({
1496
1524
  label: LabelSchema.nullable(),
1497
1525
  nodeIds: z4.array(IdSchema).min(1)
1498
1526
  }).partial().strict();
1499
1527
  var DocumentPropsSchema = z4.object({
1500
- label: LabelSchema
1528
+ label: LabelSchema,
1529
+ /** Display preferences (`view.edgeRouting`); null clears the whole
1530
+ * object. The object is replaced, not merged: pass every field you want
1531
+ * kept. */
1532
+ view: DocumentViewSchema.strict().nullable()
1501
1533
  }).partial().strict();
1502
1534
  var AddNodeOpSchema = z4.object({
1503
1535
  type: z4.literal("addNode"),
@@ -2071,7 +2103,7 @@ async function pollUntil(what, fetchState, isDone, opts = {}) {
2071
2103
  }
2072
2104
  }
2073
2105
  var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "canceled"]);
2074
- var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial"]);
2106
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "partial", "cancelled"]);
2075
2107
  function isJobTerminal(res) {
2076
2108
  return TERMINAL_JOB_STATUSES.has(res.job.status);
2077
2109
  }
@@ -2404,11 +2436,12 @@ var studioWorkflowGet = defineTool({
2404
2436
  description: "Read a saved workflow. By default returns a summary: { savedId, revision, label, readOnly, nodes: [{ id, kind, label?, task?, model?, prompt? (first 80 chars, plus promptLength), imageUrl?, name?, type?, required?, wires: { in, out } }], edgeCount, groupCount, projection: { ok, issues } } where revision is the baseRevision for studio_workflow_apply and projection says whether the workflow can run and why not. A workflow the canvas cannot represent comes back as { readOnly: true, definitionNodeCount, projection, hint } with no nodes: it is readable and runnable (studio_run_workflow with its savedId) but not patchable. needsLossyAcknowledgement: true means its first patch must carry acknowledgeLossy: true. full: true returns the whole response instead; use it only when you need every field.",
2405
2437
  inputSchema: {
2406
2438
  savedId,
2439
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
2407
2440
  full: z7.boolean().default(false).describe("Return the whole response (the full document, or the stored definition of a read-only workflow) instead of the summary.")
2408
2441
  },
2409
2442
  annotations: { readOnlyHint: true },
2410
2443
  handler: async (args, ctx) => {
2411
- const res = await ctx.client.getWorkflowDocument(args.savedId);
2444
+ const res = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
2412
2445
  if (args.full) return res;
2413
2446
  if (res.readOnly === true || res.document === null) {
2414
2447
  const definition = isRecord(res.definition) ? res.definition : null;
@@ -2481,6 +2514,7 @@ var studioWorkflowApply = defineTool({
2481
2514
  description: 'Change a saved workflow atomically: the ops are applied all-or-nothing on top of baseRevision (the revision from studio_workflow_get, studio_workflow_create or your last apply). Success returns { ok: true, savedId, revision, appliedCount, inverseCount, affectedNodes (summaries of the nodes the patch touched, removed ones as { id, removed: true }), projection: { ok, issues }, idempotencyKey, replayed }. Every refusal comes back as { ok: false, code, hint } you can act on rather than an error: "stale_revision" (re-read, rebase your ops, apply again on the revision returned), "invalid_patch" (error.opIndex names the offending op), "forbidden" (opIndex plus the missingScope your credential needs), "not_editable" (this workflow is readable and runnable but cannot be patched), "lossy_import" (read notes, then resend with acknowledgeLossy: true and a NEW idempotencyKey), "idempotency_mismatch" (that key belongs to a different patch: use a fresh one), and "network_error" / "server_error" which you retry with the SAME idempotencyKey so the patch can never apply twice.',
2482
2515
  inputSchema: {
2483
2516
  savedId,
2517
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project."),
2484
2518
  baseRevision: PatchSchema.shape.baseRevision.describe("The revision the ops were written against. The Studio refuses a stale one with stale_revision."),
2485
2519
  ops: opsSchema,
2486
2520
  idempotencyKey: PatchSchema.shape.idempotencyKey.describe(
@@ -2495,6 +2529,7 @@ var studioWorkflowApply = defineTool({
2495
2529
  let res;
2496
2530
  try {
2497
2531
  res = await ctx.client.applyWorkflowPatch(args.savedId, {
2532
+ ...args.projectId !== void 0 ? { projectId: args.projectId } : {},
2498
2533
  baseRevision: args.baseRevision,
2499
2534
  ops: args.ops,
2500
2535
  idempotencyKey,
@@ -2529,16 +2564,18 @@ var studioWorkflowValidate = defineTool({
2529
2564
  inputSchema: {
2530
2565
  document: WorkflowDocumentSchema.optional().describe("The document to validate. Either this or savedId."),
2531
2566
  savedId: savedId.optional().describe("Fetch this saved workflow first and validate against its current document. Either this or document, not both."),
2567
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the saved workflow lives in (only with savedId); omit for the default project."),
2532
2568
  ops: PatchSchema.shape.ops.optional().describe("Ops to apply to the document before projecting it (the same vocabulary and limits as studio_workflow_apply)."),
2533
2569
  params: paramsSchema.optional()
2534
2570
  },
2535
2571
  annotations: { readOnlyHint: true },
2536
2572
  handler: async (args, ctx) => {
2537
2573
  if (args.document && args.savedId) throw new ToolInputError("pass either `document` or `savedId`, not both");
2574
+ if (args.projectId !== void 0 && !args.savedId) throw new ToolInputError("`projectId` only applies when fetching by `savedId`");
2538
2575
  let document = args.document;
2539
2576
  if (!document) {
2540
2577
  if (!args.savedId) throw new ToolInputError("provide a `document` or a `savedId` to validate");
2541
- const saved = await ctx.client.getWorkflowDocument(args.savedId);
2578
+ const saved = await ctx.client.getWorkflowDocument(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
2542
2579
  if (saved.readOnly === true || saved.document === null) {
2543
2580
  throw new ToolInputError(
2544
2581
  `workflow ${args.savedId} has no document to validate: the canvas cannot represent its stored definition (studio_workflow_get shows readOnly: true and why). It is still runnable with studio_run_workflow.`
@@ -2557,11 +2594,12 @@ var studioWorkflowScreenshot = defineTool({
2557
2594
  name: "studio_workflow_screenshot",
2558
2595
  description: "Render a saved workflow's document as a diagram (labelled boxes and wires, positioned by the document's own layout) so you can see the graph you just built, the Figma get_screenshot equivalent. NOT a pixel-exact preview of the real canvas: a plain diagnostic diagram for checking topology (is everything wired the way I meant, did that addNode land where I expected). Returns { url, width, height }; the url is a signed Storage Brain link, valid for 24h. Free, read-only.",
2559
2596
  inputSchema: {
2560
- savedId
2597
+ savedId,
2598
+ projectId: z7.string().min(1).optional().describe("Project id from studio_list_projects the workflow lives in; omit for the default project.")
2561
2599
  },
2562
2600
  annotations: { readOnlyHint: true },
2563
2601
  handler: async (args, ctx) => {
2564
- return ctx.client.getWorkflowScreenshot(args.savedId);
2602
+ return ctx.client.getWorkflowScreenshot(args.savedId, args.projectId !== void 0 ? { projectId: args.projectId } : {});
2565
2603
  }
2566
2604
  });
2567
2605
  var WORKFLOW_TOOLS = [
@@ -2594,6 +2632,8 @@ function projectJob(res) {
2594
2632
  function projectNodeRun(nr) {
2595
2633
  return {
2596
2634
  nodeId: nr.nodeId,
2635
+ // The node-run (generation) id: what `upstreamGenerations` pins.
2636
+ id: nr.id ?? null,
2597
2637
  status: nr.status,
2598
2638
  url: nr.resultUrl ?? nr.results?.[0]?.url ?? null,
2599
2639
  kind: nr.results?.[0]?.kind ?? null,
@@ -2851,13 +2891,16 @@ var workflowDefinitionSchema = WorkflowDefinitionSchema.describe(
2851
2891
  );
2852
2892
  var studioRunWorkflow = defineTool({
2853
2893
  name: "studio_run_workflow",
2854
- description: `Run a generation workflow DAG. Pass an inline definition (nodes of provider calls wired by ref bindings; this is what the hosted Studio accepts) or the savedId of a workflow saved in the Studio (the Studio runs that workflow's current document, exactly what the canvas would run). Params fill {kind:"param"} bindings. Returns the runId plus per-node status, URLs and cost; with wait=true (default) it blocks until the run is terminal (succeeded, failed, partial). Each node becomes a paid job.`,
2894
+ description: `Run a generation workflow DAG. Pass an inline definition (nodes of provider calls wired by ref bindings; this is what the hosted Studio accepts) or the savedId of a workflow saved in the Studio (the Studio runs that workflow's current document, exactly what the canvas would run). Params fill {kind:"param"} bindings. Returns the runId plus per-node status, URLs and cost; with wait=true (default) it blocks until the run is terminal (succeeded, failed, partial, cancelled). Each node becomes a paid job. Use an inline definition ONLY for the first batch of a new deliverable (one workflow, a node per item, a stable label); a follow-up on an existing item belongs in that saved workflow as a new node (studio_workflow_apply: addNode plus connect from the node it refines) run here with savedId and nodeIds, because every listed inline run is saved as its own workflow.`,
2855
2895
  inputSchema: {
2856
2896
  projectId: z8.string().min(1).describe("Project id from studio_list_projects."),
2857
2897
  definition: workflowDefinitionSchema.optional(),
2858
2898
  savedId: z8.string().optional().describe("Id of a saved workflow (from studio_workflow_create, studio_workflow_get or a listed run) to run instead of an inline definition. An unknown id is a 404 naming it; a workflow that is not runnable yet comes back as not_runnable with per-node issues."),
2859
2899
  params: z8.record(z8.string(), z8.unknown()).optional().describe("Values for the definition's params."),
2860
2900
  nodeIds: z8.array(z8.string()).optional().describe("Run only this subset of nodes (requires an inline `definition` or a `savedId`)."),
2901
+ upstreamGenerations: z8.record(z8.string(), z8.string()).optional().describe(
2902
+ 'Pin WHICH generation of an upstream node feeds this subset run: { "<upstream node id>": "<node-run id>" } (the per-node `id` that studio_get_run returns). A verified pin is reused even when the planner has no matching cache key for it (an inline first batch, or a node edited and reverted), so a follow-up node wired from an earlier generation runs against exactly that image. An unknown id is refused naming the node, never silently replaced by the latest.'
2903
+ ),
2861
2904
  runs: z8.number().int().min(1).max(8).optional().describe("Repeat a node subset N times (1 to 8)."),
2862
2905
  visibility: z8.enum(["listed", "unlisted"]).optional().describe("Whether the run shows in the Studio workflow list."),
2863
2906
  budgetUsd: z8.number().nonnegative().optional().describe(
@@ -2881,6 +2924,7 @@ var studioRunWorkflow = defineTool({
2881
2924
  ...args.definition ? { definition: args.definition } : {},
2882
2925
  ...args.params ? { params: args.params } : {},
2883
2926
  ...args.nodeIds && args.nodeIds.length > 0 ? { nodeIds: args.nodeIds } : {},
2927
+ ...args.upstreamGenerations ? { upstreamGenerations: args.upstreamGenerations } : {},
2884
2928
  ...args.runs !== void 0 ? { runs: args.runs } : {},
2885
2929
  ...args.savedId ? { savedId: args.savedId } : {},
2886
2930
  ...args.visibility ? { visibility: args.visibility } : {},
@@ -2894,7 +2938,7 @@ var studioRunWorkflow = defineTool({
2894
2938
  });
2895
2939
  var studioGetRun = defineTool({
2896
2940
  name: "studio_get_run",
2897
- description: "Fetch a workflow run by runId: overall status (pending, running, succeeded, failed, partial, awaiting_decision), total cost, and per-node status, result URL, cost and error message. Set wait=true to block until the run settles. A run at awaiting_decision is parked on a person at a judge node: waiting returns immediately with a `waiting` block naming the node and its candidates, because polling can never clear it; ask the user to choose and submit their answer with studio_decide_judge.",
2941
+ description: "Fetch a workflow run by runId: overall status (pending, running, succeeded, failed, partial, awaiting_decision, cancelled; cancelled means somebody stopped it), total cost, and per-node status, result URL, cost and error message. Set wait=true to block until the run settles. A run at awaiting_decision is parked on a person at a judge node: waiting returns immediately with a `waiting` block naming the node and its candidates, because polling can never clear it; ask the user to choose and submit their answer with studio_decide_judge.",
2898
2942
  inputSchema: {
2899
2943
  runId: z8.string().min(1),
2900
2944
  wait: z8.boolean().default(false),
@@ -3141,7 +3185,7 @@ function registerTools(server, ctx) {
3141
3185
 
3142
3186
  // src/server.ts
3143
3187
  var SERVER_NAME = "lumitra-studio";
3144
- var SERVER_VERSION = "0.1.0";
3188
+ var SERVER_VERSION = "0.1.1";
3145
3189
  function createStudioMcpServer(opts = {}) {
3146
3190
  const config = opts.config ?? resolveConfig(opts.env ?? process.env);
3147
3191
  const client = new StudioClient(config, {
@@ -3155,7 +3199,8 @@ function createStudioMcpServer(opts = {}) {
3155
3199
  "Lumitra Studio: brand-consistent image generation, character minting, workflow runs and the still-swap video recipe over the hosted Studio API.",
3156
3200
  "Start with studio_list_projects to get a projectId. Every generation costs real money (roughly $0.04 per image, $0.30 per character mint, $0.70 per video take); check studio_get_spend when in doubt.",
3157
3201
  "Long jobs: tools wait by default (up to timeoutSeconds, 600 by default) and return ids you can poll with studio_get_job / studio_get_run if they time out.",
3158
- "Authoring a workflow is free and revisioned: studio_workflow_describe for the vocabulary, studio_workflow_create to build one from ops, studio_workflow_apply to change it on top of a baseRevision (a stale_revision result means re-read, rebase, retry), studio_workflow_validate to dry-run before spending, then studio_run_workflow with its savedId."
3202
+ "Authoring a workflow is free and revisioned: studio_workflow_describe for the vocabulary, studio_workflow_create to build one from ops, studio_workflow_apply to change it on top of a baseRevision (a stale_revision result means re-read, rebase, retry), studio_workflow_validate to dry-run before spending, then studio_run_workflow with its savedId.",
3203
+ "Workflow hygiene: one deliverable is ONE saved workflow with a node per item, and a refinement of an item (edit, upscale, variation) is a NEW NODE appended to that workflow with studio_workflow_apply (addNode plus a connect from the node it refines, so the earlier generation is its input), then studio_run_workflow with savedId and nodeIds limited to the new node. Never send a fresh inline definition for a follow-up: every listed inline run is saved as its own workflow, which scatters one deliverable across duplicates."
3159
3204
  ].join(" ")
3160
3205
  }
3161
3206
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marlinjai/studio-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Model Context Protocol (MCP) server over the Lumitra Studio HTTP API: generate images, mint characters, run workflows, swap characters into video.",
6
6
  "license": "MIT",