@sanity/workflow-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -150,6 +150,11 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
150
150
  action: {
151
151
  type: "string",
152
152
  description: "The id of the action on that activity. Must be one of the actions listed as allowed=true on the activity."
153
+ },
154
+ params: {
155
+ type: "object",
156
+ description: `Optional. Values for the action's declared params, keyed by param name (e.g. {"note": "Unsupported claim in paragraph 3."}). Required when the action declares a required param \u2014 get_workflow_state lists each action's params and whether they are required. The shape is per-action, so this is a free-form object; the engine validates the supplied values against the action's declared params and rejects the call if a required one is missing.`,
157
+ additionalProperties: !0
153
158
  }
154
159
  },
155
160
  required: ["instance_id", "activity", "action"],
@@ -235,7 +240,11 @@ function projectSummary(doc, subjectTitles) {
235
240
  ...subject !== void 0 ? { subject } : {}
236
241
  };
237
242
  }
238
- function projectState(instance, evaluation, subjectTitles) {
243
+ function projectState({
244
+ instance,
245
+ evaluation,
246
+ subjectTitles
247
+ }) {
239
248
  const workflowTitle = JSON.parse(instance.definitionSnapshot).title, stageTitle = evaluation.currentStage.stage.title, activities = evaluation.currentStage.activities.map((te) => {
240
249
  const actions = te.actions.map((ae) => {
241
250
  const verdict = workflowEngine.actionVerdict(te, ae);
@@ -279,15 +288,21 @@ function isInstanceDone(instance) {
279
288
  return !1;
280
289
  }
281
290
  }
282
- const DISABLED_REASON_TEXT = {
283
- "filter-failed": "action's filter condition did not hold for this actor",
284
- "activity-not-active": "activity is not currently active",
285
- "stage-terminal": "stage is terminal \u2014 no further actions possible",
286
- "instance-completed": "instance has already completed",
287
- "requirements-unmet": "activity's declared requirements are not yet satisfied"
288
- };
289
291
  function formatDisabledReason(reason) {
290
- return DISABLED_REASON_TEXT[reason.kind] ?? reason.kind.replace(/-/g, " ");
292
+ switch (reason.kind) {
293
+ case "filter-failed":
294
+ return "action's filter condition did not hold for this actor";
295
+ case "activity-not-active":
296
+ return "activity is not currently active";
297
+ case "stage-terminal":
298
+ return "stage is terminal \u2014 no further actions possible";
299
+ case "mutation-guard-denied":
300
+ return `blocked by mutation guard(s): ${reason.guardIds.join(", ")}`;
301
+ case "instance-completed":
302
+ return "instance has already completed";
303
+ case "requirements-unmet":
304
+ return "activity's declared requirements are not yet satisfied";
305
+ }
291
306
  }
292
307
  function summariseHistoryEntry(entry) {
293
308
  switch (entry._type) {
@@ -352,11 +367,11 @@ function buildTools(engine, options = {}) {
352
367
  },
353
368
  get_workflow_state: async (rawInput) => {
354
369
  const input = parseInstanceIdInput(rawInput, "get_workflow_state");
355
- return getInstanceState(engine, input.instance_id, accessOverride);
370
+ return getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
356
371
  },
357
372
  diagnose_workflow: async (rawInput) => {
358
373
  const input = parseInstanceIdInput(rawInput, "diagnose_workflow");
359
- return getInstanceDiagnosis(engine, input.instance_id, accessOverride);
374
+ return getInstanceDiagnosis({ engine, instanceId: input.instance_id, accessOverride });
360
375
  },
361
376
  fire_action: async (rawInput) => {
362
377
  const input = parseFireActionInput(rawInput);
@@ -364,8 +379,9 @@ function buildTools(engine, options = {}) {
364
379
  instanceId: input.instance_id,
365
380
  activity: input.activity,
366
381
  action: input.action,
382
+ ...input.params !== void 0 ? { params: input.params } : {},
367
383
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
368
- }), getInstanceState(engine, input.instance_id, accessOverride);
384
+ }), getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
369
385
  },
370
386
  // Authoring tools — engine-independent: they teach and check the DSL, they
371
387
  // don't touch the lake or an instance.
@@ -387,33 +403,44 @@ function parseListInput(raw) {
387
403
  const obj = raw, status = obj.status === "done" || obj.status === "any" ? obj.status : "in_flight", definition = typeof obj.definition == "string" ? obj.definition : void 0;
388
404
  return definition !== void 0 ? { definition, status } : { status };
389
405
  }
390
- function parseValidateInput(raw) {
406
+ function asObject(raw, tool) {
391
407
  if (raw === null || typeof raw != "object")
392
- throw new Error("validate_workflow_definition: input must be an object");
393
- const obj = raw;
408
+ throw new Error(`${tool}: input must be an object`);
409
+ return raw;
410
+ }
411
+ function requireString({
412
+ obj,
413
+ key,
414
+ tool
415
+ }) {
416
+ const value = obj[key];
417
+ if (typeof value != "string" || value === "")
418
+ throw new Error(`${tool}: ${key} is required`);
419
+ return value;
420
+ }
421
+ function parseValidateInput(raw) {
422
+ const obj = asObject(raw, "validate_workflow_definition");
394
423
  if (obj.definition === null || typeof obj.definition != "object")
395
424
  throw new Error("validate_workflow_definition: `definition` is required and must be an object");
396
425
  return { definition: obj.definition };
397
426
  }
398
427
  function parseInstanceIdInput(raw, tool) {
399
- if (raw === null || typeof raw != "object")
400
- throw new Error(`${tool}: input must be an object`);
401
- const obj = raw;
402
- if (typeof obj.instance_id != "string" || obj.instance_id === "")
403
- throw new Error(`${tool}: instance_id is required`);
404
- return { instance_id: obj.instance_id };
428
+ const obj = asObject(raw, tool);
429
+ return { instance_id: requireString({ obj, key: "instance_id", tool }) };
405
430
  }
406
431
  function parseFireActionInput(raw) {
407
- if (raw === null || typeof raw != "object")
408
- throw new Error("fire_action: input must be an object");
409
- const obj = raw;
410
- for (const key of ["instance_id", "activity", "action"])
411
- if (typeof obj[key] != "string" || obj[key] === "")
412
- throw new Error(`fire_action: ${key} is required`);
432
+ const obj = asObject(raw, "fire_action");
433
+ let params;
434
+ if (obj.params !== void 0) {
435
+ if (obj.params === null || typeof obj.params != "object" || Array.isArray(obj.params))
436
+ throw new Error("fire_action: params must be an object");
437
+ params = obj.params;
438
+ }
413
439
  return {
414
- instance_id: obj.instance_id,
415
- activity: obj.activity,
416
- action: obj.action
440
+ instance_id: requireString({ obj, key: "instance_id", tool: "fire_action" }),
441
+ activity: requireString({ obj, key: "activity", tool: "fire_action" }),
442
+ action: requireString({ obj, key: "action", tool: "fire_action" }),
443
+ ...params !== void 0 ? { params } : {}
417
444
  };
418
445
  }
419
446
  async function listInstances(engine, input) {
@@ -429,14 +456,22 @@ async function listInstances(engine, input) {
429
456
  instances: filtered.map((doc) => projectSummary(doc, subjectTitles))
430
457
  };
431
458
  }
432
- async function getInstanceState(engine, instanceId, accessOverride) {
459
+ async function getInstanceState({
460
+ engine,
461
+ instanceId,
462
+ accessOverride
463
+ }) {
433
464
  const evaluation = await engine.evaluateInstance({
434
465
  instanceId,
435
466
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
436
467
  }), instance = await engine.getInstance({ instanceId }), subjectTitles = await fetchSubjectTitles(engine, [instance]);
437
- return projectState(instance, evaluation, subjectTitles);
468
+ return projectState({ instance, evaluation, subjectTitles });
438
469
  }
439
- async function getInstanceDiagnosis(engine, instanceId, accessOverride) {
470
+ async function getInstanceDiagnosis({
471
+ engine,
472
+ instanceId,
473
+ accessOverride
474
+ }) {
440
475
  const { diagnosis, remediations } = await engine.diagnose({
441
476
  instanceId,
442
477
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../src/authoring-guide.ts","../src/descriptors.ts","../src/projection.ts","../src/tools.ts"],"sourcesContent":["/**\n * Content for the `get_workflow_authoring_guide` tool.\n *\n * The agent calling the MCP server generates the workflow definition; this\n * guide is what teaches it the DSL well enough to do that on the first pass.\n * It is the orientation the examples can't convey on their own (the GROQ\n * built-ins, the sugars, the hard rules) plus two worked examples.\n *\n * The examples are kept inline (rather than imported from\n * `@sanity/workflow-examples`) on purpose: that package exports the *desugared*\n * `WorkflowDefinition` (the post-`defineWorkflow` shape), but an agent should emit\n * the *authoring* (sugar) shape — JSON, exactly what `validate_workflow_definition`\n * accepts — so importing the examples would show the wrong shape. Keeping them\n * inline also avoids promoting `@sanity/workflow-examples` to a runtime dependency\n * of a package headed for host embedding. The companion test runs every example\n * through the real validators, so a stale snippet fails CI rather than misleading\n * an agent.\n */\n\nimport type {AuthoringWorkflow} from '@sanity/workflow-engine'\n\nconst minimalExample = {\n name: 'quick-approval',\n title: 'Quick approval',\n description: 'One review stage with a single approve action, then a terminal stage.',\n initialStage: 'review',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'review',\n title: 'Review',\n activities: [\n {\n name: 'decide',\n title: 'Decide',\n activation: 'auto',\n actions: [{name: 'approve', title: 'Approve', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-approved', title: 'Approve and finish', to: 'approved'}],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\nconst reviewLoopExample = {\n name: 'doc-review',\n title: 'Document review',\n description: 'Draft, then editorial review that approves or rejects back to drafting.',\n initialStage: 'drafting',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'drafting',\n title: 'Drafting',\n activities: [\n {\n name: 'write',\n title: 'Write the draft',\n activation: 'auto',\n actions: [{name: 'submit', title: 'Submit for review', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-review', title: 'Send to review', to: 'review'}],\n },\n {\n name: 'review',\n title: 'Editorial review',\n activities: [\n {\n name: 'review',\n title: 'Review the draft',\n activation: 'auto',\n actions: [\n {name: 'approve', title: 'Approve', status: 'done'},\n {name: 'reject', title: 'Reject', status: 'failed'},\n ],\n },\n ],\n transitions: [\n {name: 'to-approved', title: 'Approve and finish', to: 'approved'},\n // Reject flips the activity to `failed`; this routes the instance back.\n {\n name: 'to-drafting',\n title: 'Send back to drafting',\n to: 'drafting',\n filter: '$anyActivityFailed',\n },\n ],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\n/**\n * Worked examples included verbatim in {@link AUTHORING_GUIDE}, and exercised by\n * the drift-guard test. Authoring (sugar) shape — what an agent passes to\n * `validate_workflow_definition`.\n */\nexport const GUIDE_EXAMPLES: AuthoringWorkflow[] = [minimalExample, reviewLoopExample]\n\nconst ORIENTATION = `# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (filters, predicates, guards) is a GROQ *string*. Generate the JSON,\nthen call \\`validate_workflow_definition\\` to check it; fix the reported errors\nand re-validate until it returns \\`valid: true\\`. The validator returns the\n*desugared* definition under \\`definition\\` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: \\`{ name, title, description?, initialStage, fields?, stages[], predicates? }\\`.\n \\`initialStage\\` must be the \\`name\\` of a declared stage. Do NOT include a\n \\`version\\` — definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: \\`{ name, title?, description?, activities?, transitions? }\\`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: \\`{ name, title?, activation?, actions? }\\`. \\`activation\\` is \\`'auto'\\`\n (starts when the stage is entered) or \\`'manual'\\` (default — a human starts it).\n- **Action**: \\`{ name, title?, status?, params?, ops? }\\`.\n \\`status: 'done' | 'skipped' | 'failed'\\` is sugar that flips the *firing activity* to\n that status when the action fires. \\`ops\\` are mutations applied when the action\n fires (e.g. \\`{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}\\`).\n \\`params\\` are values the action collects from the caller (referenced by\n \\`{type:'param', param:'<name>'}\\` sources).\n- **Transition**: \\`{ name, title?, to, filter? }\\`. \\`to\\` must name a declared\n stage. \\`filter\\` is a GROQ condition gating the exit; omit it and it defaults\n to \\`$allActivitiesDone\\`.\n- **Field** (workflow- or stage-scoped persistent state): \\`{ type, name, title?, initialValue? }\\`.\n Scalar \\`type\\`s mirror Sanity: \\`string\\`, \\`text\\` (multiline), \\`number\\`,\n \\`boolean\\`, \\`date\\` (YYYY-MM-DD), \\`datetime\\` (ISO), \\`url\\`. Plus references\n (\\`doc.ref\\`, \\`doc.refs\\`, \\`release.ref\\`), identities (\\`actor\\` — one concrete\n principal; \\`assignee\\` / \\`assignees\\` — one or many user-or-role assignees), and\n the compositional kinds \\`object\\` (\\`{ type:'object', name, fields: [...] }\\`) and\n \\`array\\` (\\`{ type:'array', name, of: [...] }\\`) — \\`fields\\`/\\`of\\` are themselves\n field shapes, so any structure composes. \\`initialValue\\` seeds the slot once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: \\`{type:'input'}\\` (the caller supplies it when the\n instance starts), \\`{type:'query', query:'<groq>'}\\` (computed from the lake),\n \\`{type:'literal', value:<json>}\\`, or \\`{type:'fieldRead', field:'<name>'}\\`.\n By convention a workflow has an \\`input\\`-sourced \\`doc.ref\\` named \\`subject\\` — the\n headline document it is about.\n Two list sugars desugar to \\`array\\`: \\`{type:'todoList', name}\\` (ad-hoc\n status-tracked work — rows \\`{ label, status, assignee?, dueDate? }\\`) and\n \\`{type:'notes', name}\\` (an append-only audit/comment log — rows\n \\`{ body, actor, at }\\`; pairs with the \\`audit\\` op, which stamps \\`actor\\`/\\`at\\`).\n\n## GROQ in conditions\n\nBuilt-in variables available in filters/predicates: \\`$allActivitiesDone\\`,\n\\`$anyActivityFailed\\` (booleans over the current stage's activities), \\`$activities\\` (the activity\nlist), \\`$fields\\` (field values), \\`$now\\`, and the caller-scoped vars \\`$actor\\`\n(the acting user), \\`$assigned\\` (whether the caller is the activity's assignee — the\nidiomatic permission gate, used as \\`filter: '$assigned'\\`), and \\`$can\\`. Define\nreusable named conditions under top-level \\`predicates: { name: '<groq>' }\\` and\nreference them as \\`$name\\` (e.g.\n\\`predicates: { ready: \"count($activities[status != 'done']) == 0\" }\\` → \\`$ready\\`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by \\`_type\\` (e.g. \\`*[_type==\"article\"]\\`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a \\`doc.ref\\` field for it.\n\n## Sugars worth knowing\n\n- Action \\`status: 'done' | 'skipped' | 'failed'\\` — flips the firing activity (shown above).\n- Omitted transition \\`filter\\` — defaults to \\`$allActivitiesDone\\`.\n- Omitted activity \\`activation\\` — defaults to \\`'manual'\\`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. \\`review → drafting\\` gated\n on \\`$anyActivityFailed\\`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment — a workflow should not\n dead-end just because something was declined.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition \\`to\\` and \\`initialStage\\` names a declared stage.\n- Custom \\`predicates\\` must not shadow a built-in (e.g. \\`allActivitiesDone\\`).\n- Every GROQ string must parse and must not be a \\`_type\\` discovery scan.`\n\n/**\n * The full text returned by `get_workflow_authoring_guide`: orientation plus the\n * two worked examples rendered as JSON.\n */\nexport const AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\\`\\`\\`json\n${JSON.stringify(minimalExample, null, 2)}\n\\`\\`\\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\\`\\`\\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\\`\\`\\`\n`\n","/**\n * The MCP tool surface for workflow operations — the static descriptors an\n * agent reads to decide what to call. The implementations live in `tools.ts`;\n * these are pure data (descriptions + JSON schemas) and safe to cache.\n *\n * Two jobs: operate running instances, and author new definitions.\n *\n * Operate an instance:\n *\n * - `list_workflow_instances` is the only way to *discover* what's\n * running without already knowing an id. Filterable, capped.\n * - `get_workflow_state` is the \"tell me everything actionable about\n * one instance\" call. It composes the engine's `evaluate` projection\n * into a flat shape an LLM can reason over without piecing together\n * raw `WorkflowInstance` documents.\n * - `diagnose_workflow` answers \"*why* isn't this moving?\" — it\n * classifies an instance as healthy (waiting/progressing) or genuinely\n * stuck (a failed effect/activity, a dead-end transition) and names the\n * remediations that would unstick it. A pure read; the remediations are\n * advisory (none execute through this server).\n * - `fire_action` is the only write. Mirrors the engine's universal\n * \"something happened\" entry point — everything other than admin\n * override flows through it.\n *\n * Author a definition (both pure, engine-independent, neither deploys):\n *\n * - `get_workflow_authoring_guide` returns the DSL guide an agent reads\n * before writing a definition — shape, GROQ built-ins, sugars, examples.\n * - `validate_workflow_definition` runs the deploy-time checks (structure +\n * invariants + GROQ) and returns the desugared definition or a\n * path-prefixed error list. A human deploys it via the CLI.\n *\n * Surface choices that matter for an LLM consumer:\n *\n * - Descriptions are written for an agent, not a human reader.\n * Each one says (a) what the tool does, (b) when to use it,\n * (c) what *not* to use it for.\n * - Parameter names use snake_case, matching MCP convention.\n * - The write tool's description spells out that this is the way\n * to advance workflow state — not a side door. Without that line,\n * an LLM tends to look for a `complete_activity` or `set_stage` tool.\n * - Read tools cap their output. A 50-page `recent_history` would\n * burn context and is rarely informative; 8 is enough to spot\n * loops or stuck-in-stage patterns.\n */\n\n/**\n * The descriptor shape the Anthropic Messages API + the MCP SDK both\n * accept. Kept here as a small, transport-neutral interface so we can\n * feed the same descriptors to either consumer.\n */\nexport interface ToolDescriptor {\n name: string\n description: string\n /** JSON Schema describing the tool's parameters. */\n input_schema: {\n type: 'object'\n properties: Record<string, unknown>\n required?: string[]\n additionalProperties?: boolean\n }\n}\n\n/** Maximum instances returned by list_workflow_instances. */\nexport const LIST_CAP = 25\n\n/**\n * The static tool descriptors. Independent of any engine — the bound\n * implementations that close over an engine live in {@link buildTools}.\n */\nexport const TOOL_DESCRIPTORS: ToolDescriptor[] = [\n {\n name: 'list_workflow_instances',\n description:\n 'List workflow instances in the configured workspace. Use this when you need ' +\n \"to find a workflow but don't already know its instance id, or to survey \" +\n \"what's in flight. Returns a compact summary — id, workflow type id and \" +\n 'human-readable `workflowTitle`, current stage, whether the instance is done, ' +\n 'and (when the workflow has a conventional subject document) a `subject` ' +\n \"field with the subject doc's ref and title. Use `workflowTitle` when the \" +\n 'user names the workflow by type (e.g. \"article reviews\") and `subject.title` ' +\n \"when they name a specific in-flight instance by what it's about (e.g. \" +\n '\"the article-review about pricing\"). ' +\n 'Capped at ' +\n String(LIST_CAP) +\n ' results. ' +\n 'Do NOT use this to inspect a single known instance — use get_workflow_state ' +\n 'for that, the response will be richer.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'string',\n description:\n \"Optional. Restrict to instances of this workflow definition (e.g. 'article-review').\",\n },\n status: {\n type: 'string',\n enum: ['in_flight', 'done', 'any'],\n description:\n \"Optional. 'in_flight' returns instances not in a terminal stage; \" +\n \"'done' returns completed/aborted; 'any' returns both. Defaults to 'in_flight'.\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_state',\n description:\n 'Get the current state of a single workflow instance, projected for action. ' +\n \"Returns the workflow's id and human-readable `workflowTitle`, the current \" +\n 'stage, the subject document the workflow is about (when the workflow has a ' +\n 'conventional subject — its ref and title), every activity on the current stage ' +\n 'with its available actions (and whether each action is currently allowed), ' +\n 'and the most recent history entries. Use this whenever you need to understand ' +\n \"what's possible on an instance before deciding to act. \" +\n 'This is a pure read — it does not change anything. ' +\n 'If you only need to discover what instances exist, use list_workflow_instances ' +\n 'instead; this tool requires you to know the instance id. ' +\n 'list_workflow_instances also returns the same `workflowTitle` and `subject` ' +\n 'fields, so prefer it for fan-out discovery rather than polling ' +\n 'get_workflow_state per instance.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'diagnose_workflow',\n description:\n \"Explain why a single workflow instance is or isn't progressing. Returns a \" +\n 'verdict (`state`: progressing, waiting, blocked, completed, aborted, or ' +\n 'stuck), a one-line `summary`, and — when stuck — a structured `cause` plus ' +\n 'the `remediations` that would unstick it. Use this when an instance seems ' +\n 'stalled or the user asks \"why isn\\'t this moving?\": it distinguishes a healthy ' +\n 'instance (waiting on a human, or will advance on its own) from a genuinely ' +\n 'stuck one (a failed effect or activity, a dead-end transition). ' +\n 'This is a pure read — it changes nothing, and the remediations it names are ' +\n 'advisory: none can be executed through this server. To actually advance a ' +\n 'healthy waiting instance use fire_action; for per-activity action detail use ' +\n 'get_workflow_state.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'fire_action',\n description:\n 'Advance a workflow instance by firing an action on one of its activities. ' +\n 'This is the only way to advance workflow state from the outside — there is no ' +\n \"separate 'complete activity' or 'transition stage' tool. To find the right \" +\n '(activity, action) pair, call get_workflow_state first and pick from the ' +\n \"allowed actions listed on the current stage's activities. \" +\n 'After firing, the engine cascades any auto-transitions that become eligible ' +\n \"(so an 'approve' action on a review activity may transition the workflow to a \" +\n 'terminal stage in one shot). Returns the resulting state, same shape as ' +\n 'get_workflow_state. ' +\n 'If the action is not currently allowed (e.g. the activity is already done, or a ' +\n 'guard fails), this returns an error describing why.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n activity: {\n type: 'string',\n description:\n 'The id of the activity on the current stage. Must be one of the activities ' +\n 'returned by get_workflow_state.',\n },\n action: {\n type: 'string',\n description:\n 'The id of the action on that activity. Must be one of the actions listed ' +\n 'as allowed=true on the activity.',\n },\n },\n required: ['instance_id', 'activity', 'action'],\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_authoring_guide',\n description:\n 'Get the guide for authoring a workflow definition: the DSL shape, the ' +\n 'GROQ condition built-ins, the sugars, the rules the validator enforces, ' +\n 'and two worked JSON examples. Call this BEFORE writing a definition from ' +\n 'a description, then generate the JSON and check it with ' +\n 'validate_workflow_definition. Pure read; takes no arguments.',\n input_schema: {\n type: 'object',\n properties: {},\n additionalProperties: false,\n },\n },\n {\n name: 'validate_workflow_definition',\n description:\n 'Validate a workflow definition you have authored. Runs the same checks as ' +\n 'deploy — structural shape, cross-field invariants (e.g. every transition ' +\n 'target is a declared stage), and GROQ syntax — without writing anything. ' +\n 'Returns `{valid:true, definition}` where `definition` is the desugared form ' +\n 'that would deploy, or `{valid:false, error}` with every problem listed and ' +\n 'path-prefixed. This does NOT deploy — a human deploys the validated ' +\n 'definition via the CLI. Call get_workflow_authoring_guide first for the ' +\n 'shape; on `valid:false`, fix the reported problems and validate again.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'object',\n description:\n 'The workflow definition to validate, as a JSON object in authoring ' +\n 'shape. See get_workflow_authoring_guide for the shape and examples.',\n },\n },\n required: ['definition'],\n additionalProperties: false,\n },\n },\n]\n","/**\n * Projection helpers — engine output shaped for an LLM consumer.\n *\n * Why a hand-written projection rather than returning the engine's\n * `WorkflowEvaluation` raw: the LLM only needs (a) which activities exist,\n * (b) which actions are allowed on each, (c) why an action is denied\n * when it is. The evaluation includes per-stage field-entry resolution\n * and snapshot bookkeeping that bloats the payload without helping\n * the model pick the right next move.\n *\n * The engine reads that feed these live in `tools.ts`; this module is\n * pure shaping plus the one batched subject-title lookup they all share.\n */\n\nimport {\n actionVerdict,\n extractDocumentId,\n isTerminalStage,\n type Diagnosis,\n type Engine,\n type StuckCause,\n type WorkflowDefinition,\n type WorkflowEvaluation,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport type {\n ProjectedActionVerdict,\n ProjectedHistoryEntry,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ProjectedSubject,\n ProjectedActivity,\n} from './types.ts'\n\n/**\n * The field-entry name we treat as the workflow's \"subject\" — the headline\n * doc the workflow is about. Convention-only (see ProjectedSubject docs);\n * a workflow without an entry with this name won't have `subject` populated\n * on the projection.\n */\nconst SUBJECT_ENTRY_NAME = 'subject'\n\n/** Maximum history entries returned in get_workflow_state. */\nconst HISTORY_CAP = 8\n\n// Subject doc resolution\n//\n// The conventional \"subject\" entry holds a doc.ref to the thing the\n// workflow is about. The entry value is a GDR URI (e.g.\n// `dataset:proj:ds:article-1`) — what `_id` in the store actually\n// uses is the bare `article-1` part. Extract, batch-fetch by id list,\n// build a Map keyed by GDR URI.\n//\n// Known gap: cross-resource subject docs. If the subject lives in a\n// different Sanity resource than the engine's own workflowResource,\n// this lookup misses — the engine's `query` runs against the workflow\n// resource's client. Production-shape fix is to thread the engine's\n// `resourceClients` resolver through here; out of scope for v0.1.\n\n/**\n * Look up the conventional subject entry on a workflow instance's\n * workflow-scope fields. Returns the GDR URI when present, undefined\n * when the workflow has no subject entry or the entry is null.\n */\nfunction getSubjectGdr(instance: WorkflowInstance): string | undefined {\n const entry = instance.fields.find((s) => s._type === 'doc.ref' && s.name === SUBJECT_ENTRY_NAME)\n if (entry === undefined || entry._type !== 'doc.ref') return undefined\n return entry.value?.id\n}\n\n/**\n * Batch-fetch the `title` field for every subject doc referenced by\n * the supplied instances. Returns a `Map<gdrUri, title>` — missing\n * keys mean the doc had no title (or didn't exist / was unreadable).\n * The caller projects `ref` from the GDR either way; this map only\n * controls whether `title` is populated.\n *\n * Single GROQ call regardless of how many instances are supplied. At\n * `LIST_CAP=25` this is cheap; per-instance lookups would be N+1.\n */\nexport async function fetchSubjectTitles(\n engine: Engine,\n instances: WorkflowInstance[],\n): Promise<Map<string, string>> {\n // Build docId → gdrUri so we can attribute results back. Skip any\n // malformed GDRs silently — the projection still surfaces `ref`,\n // the LLM falls back to that as an identifier.\n const docIdToGdr = new Map<string, string>()\n for (const inst of instances) {\n const gdr = getSubjectGdr(inst)\n if (gdr === undefined) continue\n try {\n docIdToGdr.set(extractDocumentId(gdr), gdr)\n } catch {\n // Malformed GDR — leave it out of the title lookup.\n }\n }\n if (docIdToGdr.size === 0) return new Map()\n\n // Subject docs are ordinary content, not workflow docs — they carry no\n // engine tag, so we read them straight off the engine's client rather\n // than through the tag-scoped `engine.query` (which mandates a\n // `$tag` filter). Access is the client credential's concern: a\n // doc the caller can't read simply comes back absent.\n let docs: {_id: string; title?: string}[]\n try {\n docs = await engine.client.fetch<{_id: string; title?: string}[]>(\n '*[_id in $docIds]{_id, title}',\n {docIds: Array.from(docIdToGdr.keys())},\n )\n } catch (err) {\n // Titles are optional enrichment — degrade to bare refs rather than\n // failing the whole tool call when the lookup can't be served.\n process.stderr.write(\n `workflow-mcp: subject-title lookup failed; returning refs without titles: ${\n err instanceof Error ? err.message : String(err)\n }\\n`,\n )\n return new Map()\n }\n\n const out = new Map<string, string>()\n for (const doc of docs) {\n const gdr = docIdToGdr.get(doc._id)\n if (gdr !== undefined && typeof doc.title === 'string' && doc.title !== '') {\n out.set(gdr, doc.title)\n }\n }\n return out\n}\n\n/**\n * Build a `ProjectedSubject` for one instance, given the title map\n * produced by `fetchSubjectTitles`. Returns undefined when the\n * workflow has no subject entry; returns `{ ref }` without `title`\n * when the entry is present but the doc has no title field.\n */\nfunction buildSubject(\n instance: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedSubject | undefined {\n const gdr = getSubjectGdr(instance)\n if (gdr === undefined) return undefined\n const title = subjectTitles.get(gdr)\n return title !== undefined ? {ref: gdr, title} : {ref: gdr}\n}\n\nexport function projectSummary(\n doc: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedInstanceSummary {\n const definition = JSON.parse(doc.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === doc.currentStage)\n const stageTitle = stage?.title\n const workflowTitle = definition.title\n const subject = buildSubject(doc, subjectTitles)\n return {\n instanceId: doc._id,\n definition: doc.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: doc.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n lastChangedAt: doc.lastChangedAt,\n done: isInstanceDone(doc),\n ...(subject !== undefined ? {subject} : {}),\n }\n}\n\nexport function projectState(\n instance: WorkflowInstance,\n evaluation: WorkflowEvaluation,\n subjectTitles: Map<string, string>,\n): ProjectedInstanceState {\n // The evaluation already exposes the current stage's title from the\n // snapshot, but not the workflow's own title — parse the snapshot\n // once here to read it.\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const workflowTitle = definition.title\n const stageTitle = evaluation.currentStage.stage.title\n\n const activities: ProjectedActivity[] = evaluation.currentStage.activities.map((te) => {\n // Each action's verdict comes from the engine's shared `actionVerdict` —\n // the same \"is this firable, and why not\" the CLI reads — so the verdict\n // shape has one source; we keep the LLM-shaped per-activity nesting.\n const actions: ProjectedActionVerdict[] = te.actions.map((ae) => {\n const verdict = actionVerdict(te, ae)\n return {\n action: verdict.action,\n ...(verdict.title !== undefined ? {title: verdict.title} : {}),\n allowed: verdict.allowed,\n ...(verdict.allowed === false && verdict.disabledReason !== undefined\n ? {disabledReason: formatDisabledReason(verdict.disabledReason)}\n : {}),\n }\n })\n\n return {\n activity: te.activity.name,\n ...(te.activity.title !== undefined ? {title: te.activity.title} : {}),\n ...(te.activity.description !== undefined ? {description: te.activity.description} : {}),\n status: te.status,\n actions,\n }\n })\n\n const recentHistory: ProjectedHistoryEntry[] = instance.history\n .slice(-HISTORY_CAP)\n .toReversed()\n .map((entry) => ({\n at: entry.at,\n type: entry._type,\n summary: summariseHistoryEntry(entry),\n }))\n\n const subject = buildSubject(instance, subjectTitles)\n return {\n instanceId: instance._id,\n definition: instance.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: instance.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n done: isInstanceDone(instance),\n ...(subject !== undefined ? {subject} : {}),\n activities,\n recentHistory,\n }\n}\n\nexport function isInstanceDone(instance: WorkflowInstance): boolean {\n // The engine sets `completedAt` when an instance enters a terminal\n // stage; falling back to the structural check covers any edge case\n // where the definition snapshot is the source of truth.\n if (instance.completedAt !== undefined) return true\n try {\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === instance.currentStage)\n return stage !== undefined && isTerminalStage(stage)\n } catch {\n return false\n }\n}\n\n/**\n * Engine `disabledReason` is a structured tagged union (see\n * `DisabledReason` in @sanity/workflow-engine). For an LLM\n * we want a short English phrase rather than the tagged shape — the\n * tagged shape is still available on the raw evaluation if a richer\n * consumer wants it.\n */\nconst DISABLED_REASON_TEXT: Record<string, string> = {\n 'filter-failed': \"action's filter condition did not hold for this actor\",\n 'activity-not-active': 'activity is not currently active',\n 'stage-terminal': 'stage is terminal — no further actions possible',\n 'instance-completed': 'instance has already completed',\n 'requirements-unmet': \"activity's declared requirements are not yet satisfied\",\n}\n\nfunction formatDisabledReason(reason: {kind: string}): string {\n // Fall back to a humanised form of the kind for reasons we don't have\n // bespoke copy for, so a new engine reason never renders as a raw enum.\n return DISABLED_REASON_TEXT[reason.kind] ?? reason.kind.replace(/-/g, ' ')\n}\n\nfunction summariseHistoryEntry(entry: {_type: string} & Record<string, unknown>): string {\n switch (entry._type) {\n case 'stageEntered':\n return `entered stage \"${String(entry.stage)}\"`\n case 'stageExited':\n return `exited stage \"${String(entry.stage)}\" → \"${String(entry.toStage)}\"`\n case 'activityActivated':\n return `activity \"${String(entry.activity)}\" activated`\n case 'activityStatusChanged':\n return `activity \"${String(entry.activity)}\" status: ${String(entry.from)} → ${String(entry.to)}`\n case 'actionFired':\n return `action \"${String(entry.action)}\" fired on activity \"${String(entry.activity)}\"`\n case 'transitionFired':\n return `transition fired: \"${String(entry.fromStage)}\" → \"${String(entry.toStage)}\"`\n case 'effectQueued':\n return `effect \"${String(entry.effect)}\" queued`\n case 'effectCompleted':\n return `effect \"${String(entry.effect)}\" completed (${String(entry.status)})`\n case 'spawned':\n return `spawned child workflow from activity \"${String(entry.activity)}\"`\n default:\n return entry._type\n }\n}\n\n// Diagnosis summary — a plain-English one-liner per verdict, the LLM-facing\n// counterpart to the CLI's terminal-decorated rendering. The engine's\n// `diagnostics` module deliberately does no rendering, so each consumer writes\n// its own; this one folds the key identifiers (the stuck effect/activity, the\n// waiting actions) into one undecorated line.\n\n/** Exported for unit coverage of every verdict branch — see `tools.test.ts`.\n * Used internally by the diagnose_workflow read in `tools.ts`. */\nexport function diagnosisSummary(diagnosis: Diagnosis): string {\n switch (diagnosis.state) {\n case 'progressing':\n return 'This instance will advance on its own.'\n case 'waiting':\n return `Waiting for action on activity \"${diagnosis.activity}\": ${diagnosis.actions.join(' or ')}. This is the normal in-flight state — it advances when someone acts.`\n case 'blocked':\n return `Activity \"${diagnosis.activity}\" is visible but not yet executable — unmet requirement(s): ${diagnosis.requirements.join(', ')}. It will not advance until those are satisfied.`\n case 'completed':\n return `Completed at ${diagnosis.at}.`\n case 'aborted':\n return diagnosis.reason !== undefined\n ? `Aborted at ${diagnosis.at}: ${diagnosis.reason}.`\n : `Aborted at ${diagnosis.at}.`\n case 'stuck':\n return stuckSummary(diagnosis.cause)\n }\n}\n\nfunction stuckSummary(cause: StuckCause): string {\n switch (cause.kind) {\n case 'failed-effect':\n return `Stuck: a failed effect \"${cause.effect.name}\" is blocking activity \"${cause.effect.origin.name}\", which can't complete until the effect succeeds.`\n case 'hung-effect':\n return `Stuck: effect \"${cause.effect.name}\" was claimed but never completed — the drainer likely died mid-dispatch, so it won't drain on its own.`\n case 'failed-activity':\n return `Stuck: activity \"${cause.activity}\" is in a terminal failed state, so any exit transition gated on it can never fire.`\n case 'no-transition-fires':\n return `Stuck: every activity is resolved but no exit transition's filter is satisfied — likely a routing state value a filter reads was never written.`\n case 'transition-unevaluable':\n return `Stuck (recoverable): exit transition(s) ${cause.transitions.join(', ')} read an operand that is missing or unreadable (GROQ null). It advances on its own once that data becomes readable — no manual fix needed.`\n }\n}\n","/**\n * Builds the MCP tool surface: binds the static descriptors (`descriptors.ts`)\n * to implementations that close over a single engine, parses tool input, and\n * runs the engine reads that the projection helpers (`projection.ts`) shape.\n */\n\nimport {\n validateDefinition,\n WORKFLOW_INSTANCE_TYPE,\n tagScopeFilter,\n type AuthoringWorkflow,\n type Engine,\n type WorkflowAccessOverride,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\nimport {defineWorkflow} from '@sanity/workflow-engine/define'\n\nimport {AUTHORING_GUIDE} from './authoring-guide.ts'\nimport {LIST_CAP, TOOL_DESCRIPTORS, type ToolDescriptor} from './descriptors.ts'\nimport {\n diagnosisSummary,\n fetchSubjectTitles,\n isInstanceDone,\n projectState,\n projectSummary,\n} from './projection.ts'\nimport type {\n ProjectedDiagnosis,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ValidateDefinitionResult,\n} from './types.ts'\n\n/**\n * A tool implementation is a function from validated input → JSON-able\n * output. The runner is responsible for serialising the output and for\n * surfacing thrown errors as `is_error` tool results.\n */\nexport type ToolImpl = (input: unknown) => Promise<unknown>\n\nexport interface BuiltTools {\n descriptors: ToolDescriptor[]\n impls: Record<string, ToolImpl>\n}\n\n/**\n * Optional knobs for `buildTools`.\n *\n * `access` — override the engine's actor + grants resolution for every\n * call the tools make. In production, leave this unset: the engine\n * resolves the actor from the supplied client's token via `/users/me`.\n * In tests, pass `ALL_ACCESS` (from `@sanity/workflow-engine-test`)\n * because the TestClient has no token and would otherwise throw on\n * the resolution path.\n */\nexport interface BuildToolsOptions {\n access?: WorkflowAccessOverride\n}\n\n/**\n * Build the MCP tool descriptors and bound implementations against a\n * single engine. Pass the engine the tools should operate on; the\n * returned descriptors are static (safe to cache), the implementations\n * close over the engine.\n *\n * In production this is called once at server boot. In tests it's\n * called per-bench so each eval case has an isolated engine.\n */\nexport function buildTools(engine: Engine, options: BuildToolsOptions = {}): BuiltTools {\n const accessOverride = options.access\n\n const impls: Record<string, ToolImpl> = {\n list_workflow_instances: async (rawInput) => {\n const input = parseListInput(rawInput)\n return listInstances(engine, input)\n },\n get_workflow_state: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'get_workflow_state')\n return getInstanceState(engine, input.instance_id, accessOverride)\n },\n diagnose_workflow: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'diagnose_workflow')\n return getInstanceDiagnosis(engine, input.instance_id, accessOverride)\n },\n fire_action: async (rawInput) => {\n const input = parseFireActionInput(rawInput)\n await engine.fireAction({\n instanceId: input.instance_id,\n activity: input.activity,\n action: input.action,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n // Re-use the same projection so the LLM sees consistent shapes\n // between get_workflow_state and fire_action responses.\n return getInstanceState(engine, input.instance_id, accessOverride)\n },\n // Authoring tools — engine-independent: they teach and check the DSL, they\n // don't touch the lake or an instance.\n get_workflow_authoring_guide: async () => AUTHORING_GUIDE,\n validate_workflow_definition: async (rawInput): Promise<ValidateDefinitionResult> => {\n const {definition} = parseValidateInput(rawInput)\n try {\n // `defineWorkflow` is the gate (valibot structural parse + desugar +\n // cross-field invariants); `validateDefinition` adds GROQ syntax checks.\n // The cast only satisfies the compiler — `defineWorkflow` validates the\n // value at runtime and throws an aggregated, path-prefixed error.\n const stored = defineWorkflow(definition as AuthoringWorkflow)\n validateDefinition(stored)\n return {valid: true, definition: stored}\n } catch (err) {\n return {valid: false, error: err instanceof Error ? err.message : String(err)}\n }\n },\n }\n\n return {descriptors: TOOL_DESCRIPTORS, impls}\n}\n\n// Input parsers — lightweight, no zod dependency\n//\n// Reasoning: pulling in zod would inflate the package for this handful of tools.\n// The MCP server and the eval harness already constrain the shape via\n// the JSON schema in the descriptor; these parsers exist for runtime\n// type-narrowing in TypeScript, not for deep validation.\n\ninterface ListInput {\n definition?: string\n status: 'in_flight' | 'done' | 'any'\n}\n\nfunction parseListInput(raw: unknown): ListInput {\n if (raw === null || typeof raw !== 'object') {\n return {status: 'in_flight'}\n }\n const obj = raw as Record<string, unknown>\n const status = obj.status === 'done' || obj.status === 'any' ? obj.status : ('in_flight' as const)\n const definition = typeof obj.definition === 'string' ? obj.definition : undefined\n return definition !== undefined ? {definition, status} : {status}\n}\n\n/** Narrow the validate input to `{definition: object}`. The deep validation\n * is `defineWorkflow`'s job — this only guards the envelope. */\nfunction parseValidateInput(raw: unknown): {definition: object} {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('validate_workflow_definition: input must be an object')\n }\n const obj = raw as Record<string, unknown>\n if (obj.definition === null || typeof obj.definition !== 'object') {\n throw new Error('validate_workflow_definition: `definition` is required and must be an object')\n }\n return {definition: obj.definition}\n}\n\n/** Shared by every single-instance tool — `tool` names the caller so the\n * thrown error points at the right tool. */\nfunction parseInstanceIdInput(raw: unknown, tool: string): {instance_id: string} {\n if (raw === null || typeof raw !== 'object') {\n throw new Error(`${tool}: input must be an object`)\n }\n const obj = raw as Record<string, unknown>\n if (typeof obj.instance_id !== 'string' || obj.instance_id === '') {\n throw new Error(`${tool}: instance_id is required`)\n }\n return {instance_id: obj.instance_id}\n}\n\nfunction parseFireActionInput(raw: unknown): {\n instance_id: string\n activity: string\n action: string\n} {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('fire_action: input must be an object')\n }\n const obj = raw as Record<string, unknown>\n for (const key of ['instance_id', 'activity', 'action'] as const) {\n if (typeof obj[key] !== 'string' || obj[key] === '') {\n throw new Error(`fire_action: ${key} is required`)\n }\n }\n return {\n instance_id: obj.instance_id as string,\n activity: obj.activity as string,\n action: obj.action as string,\n }\n}\n\n// Engine reads, projected for an LLM\n\nasync function listInstances(\n engine: Engine,\n input: ListInput,\n): Promise<{instances: ProjectedInstanceSummary[]}> {\n // Tag-scoped via the engine's own query helper — every instance\n // returned is one this engine is allowed to see.\n const groqParts = [`_type == \"${WORKFLOW_INSTANCE_TYPE}\"`]\n if (input.definition !== undefined) {\n groqParts.push('definition == $definition')\n }\n const groq =\n `*[${groqParts.join(' && ')} && ${tagScopeFilter()}] ` +\n `| order(lastChangedAt desc)[0...${LIST_CAP}]`\n\n const params: Record<string, unknown> = {}\n if (input.definition !== undefined) params.definition = input.definition\n\n const docs = await engine.query<WorkflowInstance[]>({groq, params})\n const filtered = docs.filter((doc) => {\n const done = isInstanceDone(doc)\n if (input.status === 'done') return done\n if (input.status === 'in_flight') return !done\n return true // \"any\"\n })\n const subjectTitles = await fetchSubjectTitles(engine, filtered)\n return {\n instances: filtered.map((doc) => projectSummary(doc, subjectTitles)),\n }\n}\n\nasync function getInstanceState(\n engine: Engine,\n instanceId: string,\n accessOverride: WorkflowAccessOverride | undefined,\n): Promise<ProjectedInstanceState> {\n const evaluation = await engine.evaluateInstance({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n const instance = await engine.getInstance({instanceId})\n const subjectTitles = await fetchSubjectTitles(engine, [instance])\n return projectState(instance, evaluation, subjectTitles)\n}\n\nasync function getInstanceDiagnosis(\n engine: Engine,\n instanceId: string,\n accessOverride: WorkflowAccessOverride | undefined,\n): Promise<ProjectedDiagnosis> {\n const {diagnosis, remediations} = await engine.diagnose({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n return {\n instanceId,\n state: diagnosis.state,\n summary: diagnosisSummary(diagnosis),\n ...(diagnosis.state === 'stuck' ? {cause: diagnosis.cause.kind} : {}),\n remediations,\n }\n}\n"],"names":["extractDocumentId","actionVerdict","isTerminalStage","defineWorkflow","validateDefinition","WORKFLOW_INSTANCE_TYPE","tagScopeFilter"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","sources":["../src/authoring-guide.ts","../src/descriptors.ts","../src/projection.ts","../src/tools.ts"],"sourcesContent":["/**\n * Content for the `get_workflow_authoring_guide` tool.\n *\n * The agent calling the MCP server generates the workflow definition; this\n * guide is what teaches it the DSL well enough to do that on the first pass.\n * It is the orientation the examples can't convey on their own (the GROQ\n * built-ins, the sugars, the hard rules) plus two worked examples.\n *\n * The examples are kept inline (rather than imported from\n * `@sanity/workflow-examples`) on purpose: that package exports the *desugared*\n * `WorkflowDefinition` (the post-`defineWorkflow` shape), but an agent should emit\n * the *authoring* (sugar) shape — JSON, exactly what `validate_workflow_definition`\n * accepts — so importing the examples would show the wrong shape. Keeping them\n * inline also avoids promoting `@sanity/workflow-examples` to a runtime dependency\n * of a package headed for host embedding. The companion test runs every example\n * through the real validators, so a stale snippet fails CI rather than misleading\n * an agent.\n */\n\nimport type {AuthoringWorkflow} from '@sanity/workflow-engine'\n\nconst minimalExample = {\n name: 'quick-approval',\n title: 'Quick approval',\n description: 'One review stage with a single approve action, then a terminal stage.',\n initialStage: 'review',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'review',\n title: 'Review',\n activities: [\n {\n name: 'decide',\n title: 'Decide',\n activation: 'auto',\n actions: [{name: 'approve', title: 'Approve', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-approved', title: 'Approve and finish', to: 'approved'}],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\nconst reviewLoopExample = {\n name: 'doc-review',\n title: 'Document review',\n description: 'Draft, then editorial review that approves or rejects back to drafting.',\n initialStage: 'drafting',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'drafting',\n title: 'Drafting',\n activities: [\n {\n name: 'write',\n title: 'Write the draft',\n activation: 'auto',\n actions: [{name: 'submit', title: 'Submit for review', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-review', title: 'Send to review', to: 'review'}],\n },\n {\n name: 'review',\n title: 'Editorial review',\n activities: [\n {\n name: 'review',\n title: 'Review the draft',\n activation: 'auto',\n actions: [\n {name: 'approve', title: 'Approve', status: 'done'},\n {name: 'reject', title: 'Reject', status: 'failed'},\n ],\n },\n ],\n transitions: [\n {name: 'to-approved', title: 'Approve and finish', to: 'approved'},\n // Reject flips the activity to `failed`; this routes the instance back.\n {\n name: 'to-drafting',\n title: 'Send back to drafting',\n to: 'drafting',\n filter: '$anyActivityFailed',\n },\n ],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\n/**\n * Worked examples included verbatim in {@link AUTHORING_GUIDE}, and exercised by\n * the drift-guard test. Authoring (sugar) shape — what an agent passes to\n * `validate_workflow_definition`.\n */\nexport const GUIDE_EXAMPLES: AuthoringWorkflow[] = [minimalExample, reviewLoopExample]\n\nconst ORIENTATION = `# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (filters, predicates, guards) is a GROQ *string*. Generate the JSON,\nthen call \\`validate_workflow_definition\\` to check it; fix the reported errors\nand re-validate until it returns \\`valid: true\\`. The validator returns the\n*desugared* definition under \\`definition\\` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: \\`{ name, title, description?, initialStage, fields?, stages[], predicates? }\\`.\n \\`initialStage\\` must be the \\`name\\` of a declared stage. Do NOT include a\n \\`version\\` — definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: \\`{ name, title?, description?, activities?, transitions? }\\`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: \\`{ name, title?, activation?, actions? }\\`. \\`activation\\` is \\`'auto'\\`\n (starts when the stage is entered) or \\`'manual'\\` (default — a human starts it).\n- **Action**: \\`{ name, title?, status?, params?, ops? }\\`.\n \\`status: 'done' | 'skipped' | 'failed'\\` is sugar that flips the *firing activity* to\n that status when the action fires. \\`ops\\` are mutations applied when the action\n fires (e.g. \\`{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}\\`).\n \\`params\\` are values the action collects from the caller (referenced by\n \\`{type:'param', param:'<name>'}\\` sources).\n- **Transition**: \\`{ name, title?, to, filter? }\\`. \\`to\\` must name a declared\n stage. \\`filter\\` is a GROQ condition gating the exit; omit it and it defaults\n to \\`$allActivitiesDone\\`.\n- **Field** (workflow- or stage-scoped persistent state): \\`{ type, name, title?, initialValue? }\\`.\n Scalar \\`type\\`s mirror Sanity: \\`string\\`, \\`text\\` (multiline), \\`number\\`,\n \\`boolean\\`, \\`date\\` (YYYY-MM-DD), \\`datetime\\` (ISO), \\`url\\`. Plus references\n (\\`doc.ref\\`, \\`doc.refs\\`, \\`release.ref\\`), identities (\\`actor\\` — one concrete\n principal; \\`assignee\\` / \\`assignees\\` — one or many user-or-role assignees), and\n the compositional kinds \\`object\\` (\\`{ type:'object', name, fields: [...] }\\`) and\n \\`array\\` (\\`{ type:'array', name, of: [...] }\\`) — \\`fields\\`/\\`of\\` are themselves\n field shapes, so any structure composes. \\`initialValue\\` seeds the slot once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: \\`{type:'input'}\\` (the caller supplies it when the\n instance starts), \\`{type:'query', query:'<groq>'}\\` (computed from the lake),\n \\`{type:'literal', value:<json>}\\`, or \\`{type:'fieldRead', field:'<name>'}\\`.\n By convention a workflow has an \\`input\\`-sourced \\`doc.ref\\` named \\`subject\\` — the\n headline document it is about.\n Two list sugars desugar to \\`array\\`: \\`{type:'todoList', name}\\` (ad-hoc\n status-tracked work — rows \\`{ label, status, assignee?, dueDate? }\\`) and\n \\`{type:'notes', name}\\` (an append-only audit/comment log — rows\n \\`{ body, actor, at }\\`; pairs with the \\`audit\\` op, which stamps \\`actor\\`/\\`at\\`).\n\n## GROQ in conditions\n\nBuilt-in variables available in filters/predicates: \\`$allActivitiesDone\\`,\n\\`$anyActivityFailed\\` (booleans over the current stage's activities), \\`$activities\\` (the activity\nlist), \\`$fields\\` (field values), \\`$now\\`, and the caller-scoped vars \\`$actor\\`\n(the acting user), \\`$assigned\\` (whether the caller is the activity's assignee — the\nidiomatic permission gate, used as \\`filter: '$assigned'\\`), and \\`$can\\`. Define\nreusable named conditions under top-level \\`predicates: { name: '<groq>' }\\` and\nreference them as \\`$name\\` (e.g.\n\\`predicates: { ready: \"count($activities[status != 'done']) == 0\" }\\` → \\`$ready\\`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by \\`_type\\` (e.g. \\`*[_type==\"article\"]\\`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a \\`doc.ref\\` field for it.\n\n## Sugars worth knowing\n\n- Action \\`status: 'done' | 'skipped' | 'failed'\\` — flips the firing activity (shown above).\n- Omitted transition \\`filter\\` — defaults to \\`$allActivitiesDone\\`.\n- Omitted activity \\`activation\\` — defaults to \\`'manual'\\`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. \\`review → drafting\\` gated\n on \\`$anyActivityFailed\\`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment — a workflow should not\n dead-end just because something was declined.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition \\`to\\` and \\`initialStage\\` names a declared stage.\n- Custom \\`predicates\\` must not shadow a built-in (e.g. \\`allActivitiesDone\\`).\n- Every GROQ string must parse and must not be a \\`_type\\` discovery scan.`\n\n/**\n * The full text returned by `get_workflow_authoring_guide`: orientation plus the\n * two worked examples rendered as JSON.\n */\nexport const AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\\`\\`\\`json\n${JSON.stringify(minimalExample, null, 2)}\n\\`\\`\\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\\`\\`\\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\\`\\`\\`\n`\n","/**\n * The MCP tool surface for workflow operations — the static descriptors an\n * agent reads to decide what to call. The implementations live in `tools.ts`;\n * these are pure data (descriptions + JSON schemas) and safe to cache.\n *\n * Two jobs: operate running instances, and author new definitions.\n *\n * Operate an instance:\n *\n * - `list_workflow_instances` is the only way to *discover* what's\n * running without already knowing an id. Filterable, capped.\n * - `get_workflow_state` is the \"tell me everything actionable about\n * one instance\" call. It composes the engine's `evaluate` projection\n * into a flat shape an LLM can reason over without piecing together\n * raw `WorkflowInstance` documents.\n * - `diagnose_workflow` answers \"*why* isn't this moving?\" — it\n * classifies an instance as healthy (waiting/progressing) or genuinely\n * stuck (a failed effect/activity, a dead-end transition) and names the\n * remediations that would unstick it. A pure read; the remediations are\n * advisory (none execute through this server).\n * - `fire_action` is the only write. Mirrors the engine's universal\n * \"something happened\" entry point — everything other than admin\n * override flows through it.\n *\n * Author a definition (both pure, engine-independent, neither deploys):\n *\n * - `get_workflow_authoring_guide` returns the DSL guide an agent reads\n * before writing a definition — shape, GROQ built-ins, sugars, examples.\n * - `validate_workflow_definition` runs the deploy-time checks (structure +\n * invariants + GROQ) and returns the desugared definition or a\n * path-prefixed error list. A human deploys it via the CLI.\n *\n * Surface choices that matter for an LLM consumer:\n *\n * - Descriptions are written for an agent, not a human reader.\n * Each one says (a) what the tool does, (b) when to use it,\n * (c) what *not* to use it for.\n * - Parameter names use snake_case, matching MCP convention.\n * - The write tool's description spells out that this is the way\n * to advance workflow state — not a side door. Without that line,\n * an LLM tends to look for a `complete_activity` or `set_stage` tool.\n * - Read tools cap their output. A 50-page `recent_history` would\n * burn context and is rarely informative; 8 is enough to spot\n * loops or stuck-in-stage patterns.\n */\n\n/**\n * The descriptor shape the Anthropic Messages API + the MCP SDK both\n * accept. Kept here as a small, transport-neutral interface so we can\n * feed the same descriptors to either consumer.\n */\nexport interface ToolDescriptor {\n name: string\n description: string\n /** JSON Schema describing the tool's parameters. */\n input_schema: {\n type: 'object'\n properties: Record<string, unknown>\n required?: string[]\n additionalProperties?: boolean\n }\n}\n\n/** Maximum instances returned by list_workflow_instances. */\nexport const LIST_CAP = 25\n\n/**\n * The static tool descriptors. Independent of any engine — the bound\n * implementations that close over an engine live in {@link buildTools}.\n */\nexport const TOOL_DESCRIPTORS: ToolDescriptor[] = [\n {\n name: 'list_workflow_instances',\n description:\n 'List workflow instances in the configured workspace. Use this when you need ' +\n \"to find a workflow but don't already know its instance id, or to survey \" +\n \"what's in flight. Returns a compact summary — id, workflow type id and \" +\n 'human-readable `workflowTitle`, current stage, whether the instance is done, ' +\n 'and (when the workflow has a conventional subject document) a `subject` ' +\n \"field with the subject doc's ref and title. Use `workflowTitle` when the \" +\n 'user names the workflow by type (e.g. \"article reviews\") and `subject.title` ' +\n \"when they name a specific in-flight instance by what it's about (e.g. \" +\n '\"the article-review about pricing\"). ' +\n 'Capped at ' +\n String(LIST_CAP) +\n ' results. ' +\n 'Do NOT use this to inspect a single known instance — use get_workflow_state ' +\n 'for that, the response will be richer.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'string',\n description:\n \"Optional. Restrict to instances of this workflow definition (e.g. 'article-review').\",\n },\n status: {\n type: 'string',\n enum: ['in_flight', 'done', 'any'],\n description:\n \"Optional. 'in_flight' returns instances not in a terminal stage; \" +\n \"'done' returns completed/aborted; 'any' returns both. Defaults to 'in_flight'.\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_state',\n description:\n 'Get the current state of a single workflow instance, projected for action. ' +\n \"Returns the workflow's id and human-readable `workflowTitle`, the current \" +\n 'stage, the subject document the workflow is about (when the workflow has a ' +\n 'conventional subject — its ref and title), every activity on the current stage ' +\n 'with its available actions (and whether each action is currently allowed), ' +\n 'and the most recent history entries. Use this whenever you need to understand ' +\n \"what's possible on an instance before deciding to act. \" +\n 'This is a pure read — it does not change anything. ' +\n 'If you only need to discover what instances exist, use list_workflow_instances ' +\n 'instead; this tool requires you to know the instance id. ' +\n 'list_workflow_instances also returns the same `workflowTitle` and `subject` ' +\n 'fields, so prefer it for fan-out discovery rather than polling ' +\n 'get_workflow_state per instance.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'diagnose_workflow',\n description:\n \"Explain why a single workflow instance is or isn't progressing. Returns a \" +\n 'verdict (`state`: progressing, waiting, blocked, completed, aborted, or ' +\n 'stuck), a one-line `summary`, and — when stuck — a structured `cause` plus ' +\n 'the `remediations` that would unstick it. Use this when an instance seems ' +\n 'stalled or the user asks \"why isn\\'t this moving?\": it distinguishes a healthy ' +\n 'instance (waiting on a human, or will advance on its own) from a genuinely ' +\n 'stuck one (a failed effect or activity, a dead-end transition). ' +\n 'This is a pure read — it changes nothing, and the remediations it names are ' +\n 'advisory: none can be executed through this server. To actually advance a ' +\n 'healthy waiting instance use fire_action; for per-activity action detail use ' +\n 'get_workflow_state.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'fire_action',\n description:\n 'Advance a workflow instance by firing an action on one of its activities. ' +\n 'This is the only way to advance workflow state from the outside — there is no ' +\n \"separate 'complete activity' or 'transition stage' tool. To find the right \" +\n '(activity, action) pair, call get_workflow_state first and pick from the ' +\n \"allowed actions listed on the current stage's activities. \" +\n 'After firing, the engine cascades any auto-transitions that become eligible ' +\n \"(so an 'approve' action on a review activity may transition the workflow to a \" +\n 'terminal stage in one shot). Returns the resulting state, same shape as ' +\n 'get_workflow_state. ' +\n 'If the action is not currently allowed (e.g. the activity is already done, or a ' +\n 'guard fails), this returns an error describing why.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n activity: {\n type: 'string',\n description:\n 'The id of the activity on the current stage. Must be one of the activities ' +\n 'returned by get_workflow_state.',\n },\n action: {\n type: 'string',\n description:\n 'The id of the action on that activity. Must be one of the actions listed ' +\n 'as allowed=true on the activity.',\n },\n params: {\n type: 'object',\n description:\n \"Optional. Values for the action's declared params, keyed by param name \" +\n '(e.g. {\"note\": \"Unsupported claim in paragraph 3.\"}). Required when the ' +\n \"action declares a required param — get_workflow_state lists each action's \" +\n 'params and whether they are required. The shape is per-action, so this is a ' +\n 'free-form object; the engine validates the supplied values against the ' +\n \"action's declared params and rejects the call if a required one is missing.\",\n additionalProperties: true,\n },\n },\n required: ['instance_id', 'activity', 'action'],\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_authoring_guide',\n description:\n 'Get the guide for authoring a workflow definition: the DSL shape, the ' +\n 'GROQ condition built-ins, the sugars, the rules the validator enforces, ' +\n 'and two worked JSON examples. Call this BEFORE writing a definition from ' +\n 'a description, then generate the JSON and check it with ' +\n 'validate_workflow_definition. Pure read; takes no arguments.',\n input_schema: {\n type: 'object',\n properties: {},\n additionalProperties: false,\n },\n },\n {\n name: 'validate_workflow_definition',\n description:\n 'Validate a workflow definition you have authored. Runs the same checks as ' +\n 'deploy — structural shape, cross-field invariants (e.g. every transition ' +\n 'target is a declared stage), and GROQ syntax — without writing anything. ' +\n 'Returns `{valid:true, definition}` where `definition` is the desugared form ' +\n 'that would deploy, or `{valid:false, error}` with every problem listed and ' +\n 'path-prefixed. This does NOT deploy — a human deploys the validated ' +\n 'definition via the CLI. Call get_workflow_authoring_guide first for the ' +\n 'shape; on `valid:false`, fix the reported problems and validate again.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'object',\n description:\n 'The workflow definition to validate, as a JSON object in authoring ' +\n 'shape. See get_workflow_authoring_guide for the shape and examples.',\n },\n },\n required: ['definition'],\n additionalProperties: false,\n },\n },\n]\n","/**\n * Projection helpers — engine output shaped for an LLM consumer.\n *\n * Why a hand-written projection rather than returning the engine's\n * `WorkflowEvaluation` raw: the LLM only needs (a) which activities exist,\n * (b) which actions are allowed on each, (c) why an action is denied\n * when it is. The evaluation includes per-stage field-entry resolution\n * and snapshot bookkeeping that bloats the payload without helping\n * the model pick the right next move.\n *\n * The engine reads that feed these live in `tools.ts`; this module is\n * pure shaping plus the one batched subject-title lookup they all share.\n */\n\nimport {\n actionVerdict,\n extractDocumentId,\n isTerminalStage,\n type Diagnosis,\n type DisabledReason,\n type Engine,\n type StuckCause,\n type WorkflowDefinition,\n type WorkflowEvaluation,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport type {\n ProjectedActionVerdict,\n ProjectedHistoryEntry,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ProjectedSubject,\n ProjectedActivity,\n} from './types.ts'\n\n/**\n * The field-entry name we treat as the workflow's \"subject\" — the headline\n * doc the workflow is about. Convention-only (see ProjectedSubject docs);\n * a workflow without an entry with this name won't have `subject` populated\n * on the projection.\n */\nconst SUBJECT_ENTRY_NAME = 'subject'\n\n/** Maximum history entries returned in get_workflow_state. */\nconst HISTORY_CAP = 8\n\n// Subject doc resolution\n//\n// The conventional \"subject\" entry holds a doc.ref to the thing the\n// workflow is about. The entry value is a GDR URI (e.g.\n// `dataset:proj:ds:article-1`) — what `_id` in the store actually\n// uses is the bare `article-1` part. Extract, batch-fetch by id list,\n// build a Map keyed by GDR URI.\n//\n// Known gap: cross-resource subject docs. If the subject lives in a\n// different Sanity resource than the engine's own workflowResource,\n// this lookup misses — the engine's `query` runs against the workflow\n// resource's client. Production-shape fix is to thread the engine's\n// `resourceClients` resolver through here; out of scope for v0.1.\n\n/**\n * Look up the conventional subject entry on a workflow instance's\n * workflow-scope fields. Returns the GDR URI when present, undefined\n * when the workflow has no subject entry or the entry is null.\n */\nfunction getSubjectGdr(instance: WorkflowInstance): string | undefined {\n const entry = instance.fields.find((s) => s._type === 'doc.ref' && s.name === SUBJECT_ENTRY_NAME)\n if (entry === undefined || entry._type !== 'doc.ref') return undefined\n return entry.value?.id\n}\n\n/**\n * Batch-fetch the `title` field for every subject doc referenced by\n * the supplied instances. Returns a `Map<gdrUri, title>` — missing\n * keys mean the doc had no title (or didn't exist / was unreadable).\n * The caller projects `ref` from the GDR either way; this map only\n * controls whether `title` is populated.\n *\n * Single GROQ call regardless of how many instances are supplied. At\n * `LIST_CAP=25` this is cheap; per-instance lookups would be N+1.\n */\nexport async function fetchSubjectTitles(\n engine: Engine,\n instances: WorkflowInstance[],\n): Promise<Map<string, string>> {\n // Build docId → gdrUri so we can attribute results back. Skip any\n // malformed GDRs silently — the projection still surfaces `ref`,\n // the LLM falls back to that as an identifier.\n const docIdToGdr = new Map<string, string>()\n for (const inst of instances) {\n const gdr = getSubjectGdr(inst)\n if (gdr === undefined) continue\n try {\n docIdToGdr.set(extractDocumentId(gdr), gdr)\n } catch {\n // Malformed GDR — leave it out of the title lookup.\n }\n }\n if (docIdToGdr.size === 0) return new Map()\n\n // Subject docs are ordinary content, not workflow docs — they carry no\n // engine tag, so we read them straight off the engine's client rather\n // than through the tag-scoped `engine.query` (which mandates a\n // `$tag` filter). Access is the client credential's concern: a\n // doc the caller can't read simply comes back absent.\n let docs: {_id: string; title?: string}[]\n try {\n docs = await engine.client.fetch<{_id: string; title?: string}[]>(\n '*[_id in $docIds]{_id, title}',\n {docIds: Array.from(docIdToGdr.keys())},\n )\n } catch (err) {\n // Titles are optional enrichment — degrade to bare refs rather than\n // failing the whole tool call when the lookup can't be served.\n process.stderr.write(\n `workflow-mcp: subject-title lookup failed; returning refs without titles: ${\n err instanceof Error ? err.message : String(err)\n }\\n`,\n )\n return new Map()\n }\n\n const out = new Map<string, string>()\n for (const doc of docs) {\n const gdr = docIdToGdr.get(doc._id)\n if (gdr !== undefined && typeof doc.title === 'string' && doc.title !== '') {\n out.set(gdr, doc.title)\n }\n }\n return out\n}\n\n/**\n * Build a `ProjectedSubject` for one instance, given the title map\n * produced by `fetchSubjectTitles`. Returns undefined when the\n * workflow has no subject entry; returns `{ ref }` without `title`\n * when the entry is present but the doc has no title field.\n */\nfunction buildSubject(\n instance: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedSubject | undefined {\n const gdr = getSubjectGdr(instance)\n if (gdr === undefined) return undefined\n const title = subjectTitles.get(gdr)\n return title !== undefined ? {ref: gdr, title} : {ref: gdr}\n}\n\nexport function projectSummary(\n doc: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedInstanceSummary {\n const definition = JSON.parse(doc.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === doc.currentStage)\n const stageTitle = stage?.title\n const workflowTitle = definition.title\n const subject = buildSubject(doc, subjectTitles)\n return {\n instanceId: doc._id,\n definition: doc.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: doc.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n lastChangedAt: doc.lastChangedAt,\n done: isInstanceDone(doc),\n ...(subject !== undefined ? {subject} : {}),\n }\n}\n\nexport function projectState({\n instance,\n evaluation,\n subjectTitles,\n}: {\n instance: WorkflowInstance\n evaluation: WorkflowEvaluation\n subjectTitles: Map<string, string>\n}): ProjectedInstanceState {\n // The evaluation already exposes the current stage's title from the\n // snapshot, but not the workflow's own title — parse the snapshot\n // once here to read it.\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const workflowTitle = definition.title\n const stageTitle = evaluation.currentStage.stage.title\n\n const activities: ProjectedActivity[] = evaluation.currentStage.activities.map((te) => {\n // Each action's verdict comes from the engine's shared `actionVerdict` —\n // the same \"is this firable, and why not\" the CLI reads — so the verdict\n // shape has one source; we keep the LLM-shaped per-activity nesting.\n const actions: ProjectedActionVerdict[] = te.actions.map((ae) => {\n const verdict = actionVerdict(te, ae)\n return {\n action: verdict.action,\n ...(verdict.title !== undefined ? {title: verdict.title} : {}),\n allowed: verdict.allowed,\n ...(verdict.allowed === false && verdict.disabledReason !== undefined\n ? {disabledReason: formatDisabledReason(verdict.disabledReason)}\n : {}),\n }\n })\n\n return {\n activity: te.activity.name,\n ...(te.activity.title !== undefined ? {title: te.activity.title} : {}),\n ...(te.activity.description !== undefined ? {description: te.activity.description} : {}),\n status: te.status,\n actions,\n }\n })\n\n const recentHistory: ProjectedHistoryEntry[] = instance.history\n .slice(-HISTORY_CAP)\n .toReversed()\n .map((entry) => ({\n at: entry.at,\n type: entry._type,\n summary: summariseHistoryEntry(entry),\n }))\n\n const subject = buildSubject(instance, subjectTitles)\n return {\n instanceId: instance._id,\n definition: instance.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: instance.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n done: isInstanceDone(instance),\n ...(subject !== undefined ? {subject} : {}),\n activities,\n recentHistory,\n }\n}\n\nexport function isInstanceDone(instance: WorkflowInstance): boolean {\n // The engine sets `completedAt` when an instance enters a terminal\n // stage; falling back to the structural check covers any edge case\n // where the definition snapshot is the source of truth.\n if (instance.completedAt !== undefined) return true\n try {\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === instance.currentStage)\n return stage !== undefined && isTerminalStage(stage)\n } catch {\n return false\n }\n}\n\n/**\n * Render the engine's structured {@link DisabledReason} tagged union as a short\n * English phrase for an LLM (the tagged shape stays on the raw evaluation for a\n * richer consumer). Exhaustive by switch: adding a `DisabledReason` kind fails\n * the build here rather than silently degrading to the raw enum string — and the\n * `mutation-guard-denied` arm surfaces its `guardIds` instead of dropping them.\n * Deliberately distinct, softer phrasing from the engine's own detail renderer\n * (a different audience).\n */\nexport function formatDisabledReason(reason: DisabledReason): string {\n switch (reason.kind) {\n case 'filter-failed':\n return \"action's filter condition did not hold for this actor\"\n case 'activity-not-active':\n return 'activity is not currently active'\n case 'stage-terminal':\n return 'stage is terminal — no further actions possible'\n case 'mutation-guard-denied':\n return `blocked by mutation guard(s): ${reason.guardIds.join(', ')}`\n case 'instance-completed':\n return 'instance has already completed'\n case 'requirements-unmet':\n return \"activity's declared requirements are not yet satisfied\"\n }\n}\n\nfunction summariseHistoryEntry(entry: {_type: string} & Record<string, unknown>): string {\n switch (entry._type) {\n case 'stageEntered':\n return `entered stage \"${String(entry.stage)}\"`\n case 'stageExited':\n return `exited stage \"${String(entry.stage)}\" → \"${String(entry.toStage)}\"`\n case 'activityActivated':\n return `activity \"${String(entry.activity)}\" activated`\n case 'activityStatusChanged':\n return `activity \"${String(entry.activity)}\" status: ${String(entry.from)} → ${String(entry.to)}`\n case 'actionFired':\n return `action \"${String(entry.action)}\" fired on activity \"${String(entry.activity)}\"`\n case 'transitionFired':\n return `transition fired: \"${String(entry.fromStage)}\" → \"${String(entry.toStage)}\"`\n case 'effectQueued':\n return `effect \"${String(entry.effect)}\" queued`\n case 'effectCompleted':\n return `effect \"${String(entry.effect)}\" completed (${String(entry.status)})`\n case 'spawned':\n return `spawned child workflow from activity \"${String(entry.activity)}\"`\n default:\n return entry._type\n }\n}\n\n// Diagnosis summary — a plain-English one-liner per verdict, the LLM-facing\n// counterpart to the CLI's terminal-decorated rendering. The engine's\n// `diagnostics` module deliberately does no rendering, so each consumer writes\n// its own; this one folds the key identifiers (the stuck effect/activity, the\n// waiting actions) into one undecorated line.\n\n/** Exported for unit coverage of every verdict branch — see `tools.test.ts`.\n * Used internally by the diagnose_workflow read in `tools.ts`. */\nexport function diagnosisSummary(diagnosis: Diagnosis): string {\n switch (diagnosis.state) {\n case 'progressing':\n return 'This instance will advance on its own.'\n case 'waiting':\n return `Waiting for action on activity \"${diagnosis.activity}\": ${diagnosis.actions.join(' or ')}. This is the normal in-flight state — it advances when someone acts.`\n case 'blocked':\n return `Activity \"${diagnosis.activity}\" is visible but not yet executable — unmet requirement(s): ${diagnosis.requirements.join(', ')}. It will not advance until those are satisfied.`\n case 'completed':\n return `Completed at ${diagnosis.at}.`\n case 'aborted':\n return diagnosis.reason !== undefined\n ? `Aborted at ${diagnosis.at}: ${diagnosis.reason}.`\n : `Aborted at ${diagnosis.at}.`\n case 'stuck':\n return stuckSummary(diagnosis.cause)\n }\n}\n\nfunction stuckSummary(cause: StuckCause): string {\n switch (cause.kind) {\n case 'failed-effect':\n return `Stuck: a failed effect \"${cause.effect.name}\" is blocking activity \"${cause.effect.origin.name}\", which can't complete until the effect succeeds.`\n case 'hung-effect':\n return `Stuck: effect \"${cause.effect.name}\" was claimed but never completed — the drainer likely died mid-dispatch, so it won't drain on its own.`\n case 'failed-activity':\n return `Stuck: activity \"${cause.activity}\" is in a terminal failed state, so any exit transition gated on it can never fire.`\n case 'no-transition-fires':\n return `Stuck: every activity is resolved but no exit transition's filter is satisfied — likely a routing state value a filter reads was never written.`\n case 'transition-unevaluable':\n return `Stuck (recoverable): exit transition(s) ${cause.transitions.join(', ')} read an operand that is missing or unreadable (GROQ null). It advances on its own once that data becomes readable — no manual fix needed.`\n }\n}\n","/**\n * Builds the MCP tool surface: binds the static descriptors (`descriptors.ts`)\n * to implementations that close over a single engine, parses tool input, and\n * runs the engine reads that the projection helpers (`projection.ts`) shape.\n */\n\nimport {\n validateDefinition,\n WORKFLOW_INSTANCE_TYPE,\n tagScopeFilter,\n type AuthoringWorkflow,\n type Engine,\n type WorkflowAccessOverride,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\nimport {defineWorkflow} from '@sanity/workflow-engine/define'\n\nimport {AUTHORING_GUIDE} from './authoring-guide.ts'\nimport {LIST_CAP, TOOL_DESCRIPTORS, type ToolDescriptor} from './descriptors.ts'\nimport {\n diagnosisSummary,\n fetchSubjectTitles,\n isInstanceDone,\n projectState,\n projectSummary,\n} from './projection.ts'\nimport type {\n ProjectedDiagnosis,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ValidateDefinitionResult,\n} from './types.ts'\n\n/**\n * A tool implementation is a function from validated input → JSON-able\n * output. The runner is responsible for serialising the output and for\n * surfacing thrown errors as `is_error` tool results.\n */\nexport type ToolImpl = (input: unknown) => Promise<unknown>\n\nexport interface BuiltTools {\n descriptors: ToolDescriptor[]\n impls: Record<string, ToolImpl>\n}\n\n/**\n * Optional knobs for `buildTools`.\n *\n * `access` — override the engine's actor + grants resolution for every\n * call the tools make. In production, leave this unset: the engine\n * resolves the actor from the supplied client's token via `/users/me`.\n * In tests, pass `ALL_ACCESS` (from `@sanity/workflow-engine-test`)\n * because the TestClient has no token and would otherwise throw on\n * the resolution path.\n */\nexport interface BuildToolsOptions {\n access?: WorkflowAccessOverride\n}\n\n/**\n * Build the MCP tool descriptors and bound implementations against a\n * single engine. Pass the engine the tools should operate on; the\n * returned descriptors are static (safe to cache), the implementations\n * close over the engine.\n *\n * In production this is called once at server boot. In tests it's\n * called per-bench so each eval case has an isolated engine.\n */\nexport function buildTools(engine: Engine, options: BuildToolsOptions = {}): BuiltTools {\n const accessOverride = options.access\n\n const impls: Record<string, ToolImpl> = {\n list_workflow_instances: async (rawInput) => {\n const input = parseListInput(rawInput)\n return listInstances(engine, input)\n },\n get_workflow_state: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'get_workflow_state')\n return getInstanceState({engine, instanceId: input.instance_id, accessOverride})\n },\n diagnose_workflow: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'diagnose_workflow')\n return getInstanceDiagnosis({engine, instanceId: input.instance_id, accessOverride})\n },\n fire_action: async (rawInput) => {\n const input = parseFireActionInput(rawInput)\n await engine.fireAction({\n instanceId: input.instance_id,\n activity: input.activity,\n action: input.action,\n ...(input.params !== undefined ? {params: input.params} : {}),\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n // Re-use the same projection so the LLM sees consistent shapes\n // between get_workflow_state and fire_action responses.\n return getInstanceState({engine, instanceId: input.instance_id, accessOverride})\n },\n // Authoring tools — engine-independent: they teach and check the DSL, they\n // don't touch the lake or an instance.\n get_workflow_authoring_guide: async () => AUTHORING_GUIDE,\n validate_workflow_definition: async (rawInput): Promise<ValidateDefinitionResult> => {\n const {definition} = parseValidateInput(rawInput)\n try {\n // `defineWorkflow` is the gate (valibot structural parse + desugar +\n // cross-field invariants); `validateDefinition` adds GROQ syntax checks.\n // The cast only satisfies the compiler — `defineWorkflow` validates the\n // value at runtime and throws an aggregated, path-prefixed error.\n const stored = defineWorkflow(definition as AuthoringWorkflow)\n validateDefinition(stored)\n return {valid: true, definition: stored}\n } catch (err) {\n return {valid: false, error: err instanceof Error ? err.message : String(err)}\n }\n },\n }\n\n return {descriptors: TOOL_DESCRIPTORS, impls}\n}\n\n// Input parsers — lightweight, no zod dependency\n//\n// Reasoning: pulling in zod would inflate the package for this handful of tools.\n// The MCP server and the eval harness already constrain the shape via\n// the JSON schema in the descriptor; these parsers exist for runtime\n// type-narrowing in TypeScript, not for deep validation.\n\ninterface ListInput {\n definition?: string\n status: 'in_flight' | 'done' | 'any'\n}\n\nfunction parseListInput(raw: unknown): ListInput {\n if (raw === null || typeof raw !== 'object') {\n return {status: 'in_flight'}\n }\n const obj = raw as Record<string, unknown>\n const status = obj.status === 'done' || obj.status === 'any' ? obj.status : ('in_flight' as const)\n const definition = typeof obj.definition === 'string' ? obj.definition : undefined\n return definition !== undefined ? {definition, status} : {status}\n}\n\n/** Narrow an unknown tool input to an object envelope, or throw naming `tool`.\n * The shared first guard for every throwing parser below. */\nfunction asObject(raw: unknown, tool: string): Record<string, unknown> {\n if (raw === null || typeof raw !== 'object') {\n throw new Error(`${tool}: input must be an object`)\n }\n return raw as Record<string, unknown>\n}\n\n/** Read a required non-empty string field off a parsed envelope, or throw\n * naming `tool` and `key`. */\nfunction requireString({\n obj,\n key,\n tool,\n}: {\n obj: Record<string, unknown>\n key: string\n tool: string\n}): string {\n const value = obj[key]\n if (typeof value !== 'string' || value === '') {\n throw new Error(`${tool}: ${key} is required`)\n }\n return value\n}\n\n/** Narrow the validate input to `{definition: object}`. The deep validation\n * is `defineWorkflow`'s job — this only guards the envelope. */\nfunction parseValidateInput(raw: unknown): {definition: object} {\n const obj = asObject(raw, 'validate_workflow_definition')\n if (obj.definition === null || typeof obj.definition !== 'object') {\n throw new Error('validate_workflow_definition: `definition` is required and must be an object')\n }\n return {definition: obj.definition}\n}\n\n/** Shared by every single-instance tool — `tool` names the caller so the\n * thrown error points at the right tool. */\nfunction parseInstanceIdInput(raw: unknown, tool: string): {instance_id: string} {\n const obj = asObject(raw, tool)\n return {instance_id: requireString({obj, key: 'instance_id', tool})}\n}\n\nfunction parseFireActionInput(raw: unknown): {\n instance_id: string\n activity: string\n action: string\n params?: Record<string, unknown>\n} {\n const obj = asObject(raw, 'fire_action')\n // `params` is optional and free-form (shapes are per-action — the engine\n // validates the values against the action's declared params). We only guard\n // the envelope here: present means it must be a plain object, not an array.\n let params: Record<string, unknown> | undefined\n if (obj.params !== undefined) {\n if (obj.params === null || typeof obj.params !== 'object' || Array.isArray(obj.params)) {\n throw new Error('fire_action: params must be an object')\n }\n params = obj.params as Record<string, unknown>\n }\n return {\n instance_id: requireString({obj, key: 'instance_id', tool: 'fire_action'}),\n activity: requireString({obj, key: 'activity', tool: 'fire_action'}),\n action: requireString({obj, key: 'action', tool: 'fire_action'}),\n ...(params !== undefined ? {params} : {}),\n }\n}\n\n// Engine reads, projected for an LLM\n\nasync function listInstances(\n engine: Engine,\n input: ListInput,\n): Promise<{instances: ProjectedInstanceSummary[]}> {\n // Tag-scoped via the engine's own query helper — every instance\n // returned is one this engine is allowed to see.\n const groqParts = [`_type == \"${WORKFLOW_INSTANCE_TYPE}\"`]\n if (input.definition !== undefined) {\n groqParts.push('definition == $definition')\n }\n const groq =\n `*[${groqParts.join(' && ')} && ${tagScopeFilter()}] ` +\n `| order(lastChangedAt desc)[0...${LIST_CAP}]`\n\n const params: Record<string, unknown> = {}\n if (input.definition !== undefined) params.definition = input.definition\n\n const docs = await engine.query<WorkflowInstance[]>({groq, params})\n const filtered = docs.filter((doc) => {\n const done = isInstanceDone(doc)\n if (input.status === 'done') return done\n if (input.status === 'in_flight') return !done\n return true // \"any\"\n })\n const subjectTitles = await fetchSubjectTitles(engine, filtered)\n return {\n instances: filtered.map((doc) => projectSummary(doc, subjectTitles)),\n }\n}\n\nasync function getInstanceState({\n engine,\n instanceId,\n accessOverride,\n}: {\n engine: Engine\n instanceId: string\n accessOverride: WorkflowAccessOverride | undefined\n}): Promise<ProjectedInstanceState> {\n const evaluation = await engine.evaluateInstance({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n const instance = await engine.getInstance({instanceId})\n const subjectTitles = await fetchSubjectTitles(engine, [instance])\n return projectState({instance, evaluation, subjectTitles})\n}\n\nasync function getInstanceDiagnosis({\n engine,\n instanceId,\n accessOverride,\n}: {\n engine: Engine\n instanceId: string\n accessOverride: WorkflowAccessOverride | undefined\n}): Promise<ProjectedDiagnosis> {\n const {diagnosis, remediations} = await engine.diagnose({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n return {\n instanceId,\n state: diagnosis.state,\n summary: diagnosisSummary(diagnosis),\n ...(diagnosis.state === 'stuck' ? {cause: diagnosis.cause.kind} : {}),\n remediations,\n }\n}\n"],"names":["extractDocumentId","actionVerdict","isTerminalStage","defineWorkflow","validateDefinition","WORKFLOW_INSTANCE_TYPE","tagScopeFilter"],"mappings":";;;AAqBA,MAAM,iBAAiB;AAAA,EACrB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,cAAc;AAAA,EACd,QAAQ,CAAC,EAAC,MAAM,WAAW,MAAM,WAAW,OAAO,YAAY,cAAc,EAAC,MAAM,QAAA,GAAS;AAAA,EAC7F,QAAQ;AAAA,IACN;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,SAAS,CAAC,EAAC,MAAM,WAAW,OAAO,WAAW,QAAQ,OAAA,CAAO;AAAA,QAAA;AAAA,MAC/D;AAAA,MAEF,aAAa,CAAC,EAAC,MAAM,eAAe,OAAO,sBAAsB,IAAI,WAAA,CAAW;AAAA,IAAA;AAAA,IAElF,EAAC,MAAM,YAAY,OAAO,YAAY,aAAa,sCAAA;AAAA,EAAgC;AAEvF,GAEM,oBAAoB;AAAA,EACxB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,cAAc;AAAA,EACd,QAAQ,CAAC,EAAC,MAAM,WAAW,MAAM,WAAW,OAAO,YAAY,cAAc,EAAC,MAAM,QAAA,GAAS;AAAA,EAC7F,QAAQ;AAAA,IACN;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,SAAS,CAAC,EAAC,MAAM,UAAU,OAAO,qBAAqB,QAAQ,OAAA,CAAO;AAAA,QAAA;AAAA,MACxE;AAAA,MAEF,aAAa,CAAC,EAAC,MAAM,aAAa,OAAO,kBAAkB,IAAI,SAAA,CAAS;AAAA,IAAA;AAAA,IAE1E;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,SAAS;AAAA,YACP,EAAC,MAAM,WAAW,OAAO,WAAW,QAAQ,OAAA;AAAA,YAC5C,EAAC,MAAM,UAAU,OAAO,UAAU,QAAQ,SAAA;AAAA,UAAQ;AAAA,QACpD;AAAA,MACF;AAAA,MAEF,aAAa;AAAA,QACX,EAAC,MAAM,eAAe,OAAO,sBAAsB,IAAI,WAAA;AAAA;AAAA,QAEvD;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,IAAI;AAAA,UACJ,QAAQ;AAAA,QAAA;AAAA,MACV;AAAA,IACF;AAAA,IAEF,EAAC,MAAM,YAAY,OAAO,YAAY,aAAa,sCAAA;AAAA,EAAgC;AAEvF,GASM,cAAc,k5KA8FP,kBAAkB,GAAG,WAAW;AAAA;AAAA;AAAA;AAAA,uBAI3B,eAAe,KAAK;AAAA;AAAA,EAEpC,KAAK,UAAU,gBAAgB,MAAM,CAAC,CAAC;AAAA;AAAA;AAAA,uBAGvB,kBAAkB,KAAK;AAAA;AAAA,EAEvC,KAAK,UAAU,mBAAmB,MAAM,CAAC,CAAC;AAAA;AAAA,GC9I/B,WAAW,IAMX,mBAAqC;AAAA,EAChD;AAAA,IACE,MAAM;AAAA,IACN,aACE,yoBAUA,OAAO,EAAQ,IACf;AAAA,IAGF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,YAAY;AAAA,UACV,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,QAEJ,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM,CAAC,aAAa,QAAQ,KAAK;AAAA,UACjC,aACE;AAAA,QAAA;AAAA,MAEJ;AAAA,MAEF,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAaF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,aAAa;AAAA,UACX,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,MACf;AAAA,MAEF,UAAU,CAAC,aAAa;AAAA,MACxB,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAWF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,aAAa;AAAA,UACX,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,MACf;AAAA,MAEF,UAAU,CAAC,aAAa;AAAA,MACxB,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAWF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,aAAa;AAAA,UACX,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,UAAU;AAAA,UACR,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,QAGJ,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,QAGJ,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,aACE;AAAA,UAMF,sBAAsB;AAAA,QAAA;AAAA,MACxB;AAAA,MAEF,UAAU,CAAC,eAAe,YAAY,QAAQ;AAAA,MAC9C,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAKF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY,CAAA;AAAA,MACZ,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAQF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,YAAY;AAAA,UACV,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,MAEJ;AAAA,MAEF,UAAU,CAAC,YAAY;AAAA,MACvB,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAEJ,GC/MM,qBAAqB;AAwB3B,SAAS,cAAc,UAAgD;AACrE,QAAM,QAAQ,SAAS,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,aAAa,EAAE,SAAS,kBAAkB;AAChG,MAAI,EAAA,UAAU,UAAa,MAAM,UAAU;AAC3C,WAAO,MAAM,OAAO;AACtB;AAYA,eAAsB,mBACpB,QACA,WAC8B;AAI9B,QAAM,iCAAiB,IAAA;AACvB,aAAW,QAAQ,WAAW;AAC5B,UAAM,MAAM,cAAc,IAAI;AAC9B,QAAI,QAAQ;AACZ,UAAI;AACF,mBAAW,IAAIA,eAAAA,kBAAkB,GAAG,GAAG,GAAG;AAAA,MAC5C,QAAQ;AAAA,MAER;AAAA,EACF;AACA,MAAI,WAAW,SAAS,EAAG,4BAAW,IAAA;AAOtC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,OAAO,OAAO;AAAA,MACzB;AAAA,MACA,EAAC,QAAQ,MAAM,KAAK,WAAW,KAAA,CAAM,EAAA;AAAA,IAAC;AAAA,EAE1C,SAAS,KAAK;AAGZ,WAAA,QAAQ,OAAO;AAAA,MACb,6EACE,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA;AAAA,IAAA,uBAES,IAAA;AAAA,EACb;AAEA,QAAM,0BAAU,IAAA;AAChB,aAAW,OAAO,MAAM;AACtB,UAAM,MAAM,WAAW,IAAI,IAAI,GAAG;AAC9B,YAAQ,UAAa,OAAO,IAAI,SAAU,YAAY,IAAI,UAAU,MACtE,IAAI,IAAI,KAAK,IAAI,KAAK;AAAA,EAE1B;AACA,SAAO;AACT;AAQA,SAAS,aACP,UACA,eAC8B;AAC9B,QAAM,MAAM,cAAc,QAAQ;AAClC,MAAI,QAAQ,OAAW;AACvB,QAAM,QAAQ,cAAc,IAAI,GAAG;AACnC,SAAO,UAAU,SAAY,EAAC,KAAK,KAAK,MAAA,IAAS,EAAC,KAAK,IAAA;AACzD;AAEO,SAAS,eACd,KACA,eAC0B;AAC1B,QAAM,aAAa,KAAK,MAAM,IAAI,kBAAkB,GAE9C,aADQ,WAAW,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,YAAY,GAC7C,OACpB,gBAAgB,WAAW,OAC3B,UAAU,aAAa,KAAK,aAAa;AAC/C,SAAO;AAAA,IACL,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,GAAI,kBAAkB,SAAY,EAAC,cAAA,IAAiB,CAAA;AAAA,IACpD,cAAc,IAAI;AAAA,IAClB,GAAI,eAAe,SAAY,EAAC,mBAAmB,WAAA,IAAc,CAAA;AAAA,IACjE,eAAe,IAAI;AAAA,IACnB,MAAM,eAAe,GAAG;AAAA,IACxB,GAAI,YAAY,SAAY,EAAC,YAAW,CAAA;AAAA,EAAC;AAE7C;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAI2B;AAKzB,QAAM,gBADa,KAAK,MAAM,SAAS,kBAAkB,EACxB,OAC3B,aAAa,WAAW,aAAa,MAAM,OAE3C,aAAkC,WAAW,aAAa,WAAW,IAAI,CAAC,OAAO;AAIrF,UAAM,UAAoC,GAAG,QAAQ,IAAI,CAAC,OAAO;AAC/D,YAAM,UAAUC,eAAAA,cAAc,IAAI,EAAE;AACpC,aAAO;AAAA,QACL,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,SAAY,EAAC,OAAO,QAAQ,MAAA,IAAS,CAAA;AAAA,QAC3D,SAAS,QAAQ;AAAA,QACjB,GAAI,QAAQ,YAAY,MAAS,QAAQ,mBAAmB,SACxD,EAAC,gBAAgB,qBAAqB,QAAQ,cAAc,EAAA,IAC5D,CAAA;AAAA,MAAC;AAAA,IAET,CAAC;AAED,WAAO;AAAA,MACL,UAAU,GAAG,SAAS;AAAA,MACtB,GAAI,GAAG,SAAS,UAAU,SAAY,EAAC,OAAO,GAAG,SAAS,MAAA,IAAS,CAAA;AAAA,MACnE,GAAI,GAAG,SAAS,gBAAgB,SAAY,EAAC,aAAa,GAAG,SAAS,YAAA,IAAe,CAAA;AAAA,MACrF,QAAQ,GAAG;AAAA,MACX;AAAA,IAAA;AAAA,EAEJ,CAAC,GAEK,gBAAyC,SAAS,QACrD,MAAM,EAAY,EAClB,WAAA,EACA,IAAI,CAAC,WAAW;AAAA,IACf,IAAI,MAAM;AAAA,IACV,MAAM,MAAM;AAAA,IACZ,SAAS,sBAAsB,KAAK;AAAA,EAAA,EACpC,GAEE,UAAU,aAAa,UAAU,aAAa;AACpD,SAAO;AAAA,IACL,YAAY,SAAS;AAAA,IACrB,YAAY,SAAS;AAAA,IACrB,GAAI,kBAAkB,SAAY,EAAC,cAAA,IAAiB,CAAA;AAAA,IACpD,cAAc,SAAS;AAAA,IACvB,GAAI,eAAe,SAAY,EAAC,mBAAmB,WAAA,IAAc,CAAA;AAAA,IACjE,MAAM,eAAe,QAAQ;AAAA,IAC7B,GAAI,YAAY,SAAY,EAAC,QAAA,IAAW,CAAA;AAAA,IACxC;AAAA,IACA;AAAA,EAAA;AAEJ;AAEO,SAAS,eAAe,UAAqC;AAIlE,MAAI,SAAS,gBAAgB,OAAW,QAAO;AAC/C,MAAI;AAEF,UAAM,QADa,KAAK,MAAM,SAAS,kBAAkB,EAChC,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,YAAY;AAC5E,WAAO,UAAU,UAAaC,eAAAA,gBAAgB,KAAK;AAAA,EACrD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,qBAAqB,QAAgC;AACnE,UAAQ,OAAO,MAAA;AAAA,IACb,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,iCAAiC,OAAO,SAAS,KAAK,IAAI,CAAC;AAAA,IACpE,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EAAA;AAEb;AAEA,SAAS,sBAAsB,OAA0D;AACvF,UAAQ,MAAM,OAAA;AAAA,IACZ,KAAK;AACH,aAAO,kBAAkB,OAAO,MAAM,KAAK,CAAC;AAAA,IAC9C,KAAK;AACH,aAAO,iBAAiB,OAAO,MAAM,KAAK,CAAC,aAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IAC1E,KAAK;AACH,aAAO,aAAa,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC5C,KAAK;AACH,aAAO,aAAa,OAAO,MAAM,QAAQ,CAAC,aAAa,OAAO,MAAM,IAAI,CAAC,WAAM,OAAO,MAAM,EAAE,CAAC;AAAA,IACjG,KAAK;AACH,aAAO,WAAW,OAAO,MAAM,MAAM,CAAC,wBAAwB,OAAO,MAAM,QAAQ,CAAC;AAAA,IACtF,KAAK;AACH,aAAO,sBAAsB,OAAO,MAAM,SAAS,CAAC,aAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACnF,KAAK;AACH,aAAO,WAAW,OAAO,MAAM,MAAM,CAAC;AAAA,IACxC,KAAK;AACH,aAAO,WAAW,OAAO,MAAM,MAAM,CAAC,gBAAgB,OAAO,MAAM,MAAM,CAAC;AAAA,IAC5E,KAAK;AACH,aAAO,yCAAyC,OAAO,MAAM,QAAQ,CAAC;AAAA,IACxE;AACE,aAAO,MAAM;AAAA,EAAA;AAEnB;AAUO,SAAS,iBAAiB,WAA8B;AAC7D,UAAQ,UAAU,OAAA;AAAA,IAChB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,mCAAmC,UAAU,QAAQ,MAAM,UAAU,QAAQ,KAAK,MAAM,CAAC;AAAA,IAClG,KAAK;AACH,aAAO,aAAa,UAAU,QAAQ,oEAA+D,UAAU,aAAa,KAAK,IAAI,CAAC;AAAA,IACxI,KAAK;AACH,aAAO,gBAAgB,UAAU,EAAE;AAAA,IACrC,KAAK;AACH,aAAO,UAAU,WAAW,SACxB,cAAc,UAAU,EAAE,KAAK,UAAU,MAAM,MAC/C,cAAc,UAAU,EAAE;AAAA,IAChC,KAAK;AACH,aAAO,aAAa,UAAU,KAAK;AAAA,EAAA;AAEzC;AAEA,SAAS,aAAa,OAA2B;AAC/C,UAAQ,MAAM,MAAA;AAAA,IACZ,KAAK;AACH,aAAO,2BAA2B,MAAM,OAAO,IAAI,2BAA2B,MAAM,OAAO,OAAO,IAAI;AAAA,IACxG,KAAK;AACH,aAAO,kBAAkB,MAAM,OAAO,IAAI;AAAA,IAC5C,KAAK;AACH,aAAO,oBAAoB,MAAM,QAAQ;AAAA,IAC3C,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,2CAA2C,MAAM,YAAY,KAAK,IAAI,CAAC;AAAA,EAAA;AAEpF;AC/QO,SAAS,WAAW,QAAgB,UAA6B,IAAgB;AACtF,QAAM,iBAAiB,QAAQ;AA+C/B,SAAO,EAAC,aAAa,kBAAkB,OA7CC;AAAA,IACtC,yBAAyB,OAAO,aAAa;AAC3C,YAAM,QAAQ,eAAe,QAAQ;AACrC,aAAO,cAAc,QAAQ,KAAK;AAAA,IACpC;AAAA,IACA,oBAAoB,OAAO,aAAa;AACtC,YAAM,QAAQ,qBAAqB,UAAU,oBAAoB;AACjE,aAAO,iBAAiB,EAAC,QAAQ,YAAY,MAAM,aAAa,gBAAe;AAAA,IACjF;AAAA,IACA,mBAAmB,OAAO,aAAa;AACrC,YAAM,QAAQ,qBAAqB,UAAU,mBAAmB;AAChE,aAAO,qBAAqB,EAAC,QAAQ,YAAY,MAAM,aAAa,gBAAe;AAAA,IACrF;AAAA,IACA,aAAa,OAAO,aAAa;AAC/B,YAAM,QAAQ,qBAAqB,QAAQ;AAC3C,aAAA,MAAM,OAAO,WAAW;AAAA,QACtB,YAAY,MAAM;AAAA,QAClB,UAAU,MAAM;AAAA,QAChB,QAAQ,MAAM;AAAA,QACd,GAAI,MAAM,WAAW,SAAY,EAAC,QAAQ,MAAM,OAAA,IAAU,CAAA;AAAA,QAC1D,GAAI,mBAAmB,SAAY,EAAC,QAAQ,eAAA,IAAkB,CAAA;AAAA,MAAC,CAChE,GAGM,iBAAiB,EAAC,QAAQ,YAAY,MAAM,aAAa,gBAAe;AAAA,IACjF;AAAA;AAAA;AAAA,IAGA,8BAA8B,YAAY;AAAA,IAC1C,8BAA8B,OAAO,aAAgD;AACnF,YAAM,EAAC,WAAA,IAAc,mBAAmB,QAAQ;AAChD,UAAI;AAKF,cAAM,SAASC,OAAAA,eAAe,UAA+B;AAC7D,eAAAC,eAAAA,mBAAmB,MAAM,GAClB,EAAC,OAAO,IAAM,YAAY,OAAA;AAAA,MACnC,SAAS,KAAK;AACZ,eAAO,EAAC,OAAO,IAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAA;AAAA,MAC9E;AAAA,IACF;AAAA,EAAA,EACF;AAGF;AAcA,SAAS,eAAe,KAAyB;AAC/C,MAAI,QAAQ,QAAQ,OAAO,OAAQ;AACjC,WAAO,EAAC,QAAQ,YAAA;AAElB,QAAM,MAAM,KACN,SAAS,IAAI,WAAW,UAAU,IAAI,WAAW,QAAQ,IAAI,SAAU,aACvE,aAAa,OAAO,IAAI,cAAe,WAAW,IAAI,aAAa;AACzE,SAAO,eAAe,SAAY,EAAC,YAAY,OAAA,IAAU,EAAC,OAAA;AAC5D;AAIA,SAAS,SAAS,KAAc,MAAuC;AACrE,MAAI,QAAQ,QAAQ,OAAO,OAAQ;AACjC,UAAM,IAAI,MAAM,GAAG,IAAI,2BAA2B;AAEpD,SAAO;AACT;AAIA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF,GAIW;AACT,QAAM,QAAQ,IAAI,GAAG;AACrB,MAAI,OAAO,SAAU,YAAY,UAAU;AACzC,UAAM,IAAI,MAAM,GAAG,IAAI,KAAK,GAAG,cAAc;AAE/C,SAAO;AACT;AAIA,SAAS,mBAAmB,KAAoC;AAC9D,QAAM,MAAM,SAAS,KAAK,8BAA8B;AACxD,MAAI,IAAI,eAAe,QAAQ,OAAO,IAAI,cAAe;AACvD,UAAM,IAAI,MAAM,8EAA8E;AAEhG,SAAO,EAAC,YAAY,IAAI,WAAA;AAC1B;AAIA,SAAS,qBAAqB,KAAc,MAAqC;AAC/E,QAAM,MAAM,SAAS,KAAK,IAAI;AAC9B,SAAO,EAAC,aAAa,cAAc,EAAC,KAAK,KAAK,eAAe,KAAA,CAAK,EAAA;AACpE;AAEA,SAAS,qBAAqB,KAK5B;AACA,QAAM,MAAM,SAAS,KAAK,aAAa;AAIvC,MAAI;AACJ,MAAI,IAAI,WAAW,QAAW;AAC5B,QAAI,IAAI,WAAW,QAAQ,OAAO,IAAI,UAAW,YAAY,MAAM,QAAQ,IAAI,MAAM;AACnF,YAAM,IAAI,MAAM,uCAAuC;AAEzD,aAAS,IAAI;AAAA,EACf;AACA,SAAO;AAAA,IACL,aAAa,cAAc,EAAC,KAAK,KAAK,eAAe,MAAM,eAAc;AAAA,IACzE,UAAU,cAAc,EAAC,KAAK,KAAK,YAAY,MAAM,eAAc;AAAA,IACnE,QAAQ,cAAc,EAAC,KAAK,KAAK,UAAU,MAAM,eAAc;AAAA,IAC/D,GAAI,WAAW,SAAY,EAAC,WAAU,CAAA;AAAA,EAAC;AAE3C;AAIA,eAAe,cACb,QACA,OACkD;AAGlD,QAAM,YAAY,CAAC,aAAaC,eAAAA,sBAAsB,GAAG;AACrD,QAAM,eAAe,UACvB,UAAU,KAAK,2BAA2B;AAE5C,QAAM,OACJ,KAAK,UAAU,KAAK,MAAM,CAAC,OAAOC,eAAAA,eAAA,CAAgB,qCACf,QAAQ,KAEvC,SAAkC,CAAA;AACpC,QAAM,eAAe,WAAW,OAAO,aAAa,MAAM;AAG9D,QAAM,YADO,MAAM,OAAO,MAA0B,EAAC,MAAM,QAAO,GAC5C,OAAO,CAAC,QAAQ;AACpC,UAAM,OAAO,eAAe,GAAG;AAC/B,WAAI,MAAM,WAAW,SAAe,OAChC,MAAM,WAAW,cAAoB,CAAC,OACnC;AAAA,EACT,CAAC,GACK,gBAAgB,MAAM,mBAAmB,QAAQ,QAAQ;AAC/D,SAAO;AAAA,IACL,WAAW,SAAS,IAAI,CAAC,QAAQ,eAAe,KAAK,aAAa,CAAC;AAAA,EAAA;AAEvE;AAEA,eAAe,iBAAiB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAIoC;AAClC,QAAM,aAAa,MAAM,OAAO,iBAAiB;AAAA,IAC/C;AAAA,IACA,GAAI,mBAAmB,SAAY,EAAC,QAAQ,eAAA,IAAkB,CAAA;AAAA,EAAC,CAChE,GACK,WAAW,MAAM,OAAO,YAAY,EAAC,WAAA,CAAW,GAChD,gBAAgB,MAAM,mBAAmB,QAAQ,CAAC,QAAQ,CAAC;AACjE,SAAO,aAAa,EAAC,UAAU,YAAY,eAAc;AAC3D;AAEA,eAAe,qBAAqB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,GAIgC;AAC9B,QAAM,EAAC,WAAW,aAAA,IAAgB,MAAM,OAAO,SAAS;AAAA,IACtD;AAAA,IACA,GAAI,mBAAmB,SAAY,EAAC,QAAQ,eAAA,IAAkB,CAAA;AAAA,EAAC,CAChE;AACD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,UAAU;AAAA,IACjB,SAAS,iBAAiB,SAAS;AAAA,IACnC,GAAI,UAAU,UAAU,UAAU,EAAC,OAAO,UAAU,MAAM,KAAA,IAAQ,CAAA;AAAA,IAClE;AAAA,EAAA;AAEJ;;"}
package/dist/index.js CHANGED
@@ -149,6 +149,11 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
149
149
  action: {
150
150
  type: "string",
151
151
  description: "The id of the action on that activity. Must be one of the actions listed as allowed=true on the activity."
152
+ },
153
+ params: {
154
+ type: "object",
155
+ description: `Optional. Values for the action's declared params, keyed by param name (e.g. {"note": "Unsupported claim in paragraph 3."}). Required when the action declares a required param \u2014 get_workflow_state lists each action's params and whether they are required. The shape is per-action, so this is a free-form object; the engine validates the supplied values against the action's declared params and rejects the call if a required one is missing.`,
156
+ additionalProperties: !0
152
157
  }
153
158
  },
154
159
  required: ["instance_id", "activity", "action"],
@@ -234,7 +239,11 @@ function projectSummary(doc, subjectTitles) {
234
239
  ...subject !== void 0 ? { subject } : {}
235
240
  };
236
241
  }
237
- function projectState(instance, evaluation, subjectTitles) {
242
+ function projectState({
243
+ instance,
244
+ evaluation,
245
+ subjectTitles
246
+ }) {
238
247
  const workflowTitle = JSON.parse(instance.definitionSnapshot).title, stageTitle = evaluation.currentStage.stage.title, activities = evaluation.currentStage.activities.map((te) => {
239
248
  const actions = te.actions.map((ae) => {
240
249
  const verdict = actionVerdict(te, ae);
@@ -278,15 +287,21 @@ function isInstanceDone(instance) {
278
287
  return !1;
279
288
  }
280
289
  }
281
- const DISABLED_REASON_TEXT = {
282
- "filter-failed": "action's filter condition did not hold for this actor",
283
- "activity-not-active": "activity is not currently active",
284
- "stage-terminal": "stage is terminal \u2014 no further actions possible",
285
- "instance-completed": "instance has already completed",
286
- "requirements-unmet": "activity's declared requirements are not yet satisfied"
287
- };
288
290
  function formatDisabledReason(reason) {
289
- return DISABLED_REASON_TEXT[reason.kind] ?? reason.kind.replace(/-/g, " ");
291
+ switch (reason.kind) {
292
+ case "filter-failed":
293
+ return "action's filter condition did not hold for this actor";
294
+ case "activity-not-active":
295
+ return "activity is not currently active";
296
+ case "stage-terminal":
297
+ return "stage is terminal \u2014 no further actions possible";
298
+ case "mutation-guard-denied":
299
+ return `blocked by mutation guard(s): ${reason.guardIds.join(", ")}`;
300
+ case "instance-completed":
301
+ return "instance has already completed";
302
+ case "requirements-unmet":
303
+ return "activity's declared requirements are not yet satisfied";
304
+ }
290
305
  }
291
306
  function summariseHistoryEntry(entry) {
292
307
  switch (entry._type) {
@@ -351,11 +366,11 @@ function buildTools(engine, options = {}) {
351
366
  },
352
367
  get_workflow_state: async (rawInput) => {
353
368
  const input = parseInstanceIdInput(rawInput, "get_workflow_state");
354
- return getInstanceState(engine, input.instance_id, accessOverride);
369
+ return getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
355
370
  },
356
371
  diagnose_workflow: async (rawInput) => {
357
372
  const input = parseInstanceIdInput(rawInput, "diagnose_workflow");
358
- return getInstanceDiagnosis(engine, input.instance_id, accessOverride);
373
+ return getInstanceDiagnosis({ engine, instanceId: input.instance_id, accessOverride });
359
374
  },
360
375
  fire_action: async (rawInput) => {
361
376
  const input = parseFireActionInput(rawInput);
@@ -363,8 +378,9 @@ function buildTools(engine, options = {}) {
363
378
  instanceId: input.instance_id,
364
379
  activity: input.activity,
365
380
  action: input.action,
381
+ ...input.params !== void 0 ? { params: input.params } : {},
366
382
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
367
- }), getInstanceState(engine, input.instance_id, accessOverride);
383
+ }), getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
368
384
  },
369
385
  // Authoring tools — engine-independent: they teach and check the DSL, they
370
386
  // don't touch the lake or an instance.
@@ -386,33 +402,44 @@ function parseListInput(raw) {
386
402
  const obj = raw, status = obj.status === "done" || obj.status === "any" ? obj.status : "in_flight", definition = typeof obj.definition == "string" ? obj.definition : void 0;
387
403
  return definition !== void 0 ? { definition, status } : { status };
388
404
  }
389
- function parseValidateInput(raw) {
405
+ function asObject(raw, tool) {
390
406
  if (raw === null || typeof raw != "object")
391
- throw new Error("validate_workflow_definition: input must be an object");
392
- const obj = raw;
407
+ throw new Error(`${tool}: input must be an object`);
408
+ return raw;
409
+ }
410
+ function requireString({
411
+ obj,
412
+ key,
413
+ tool
414
+ }) {
415
+ const value = obj[key];
416
+ if (typeof value != "string" || value === "")
417
+ throw new Error(`${tool}: ${key} is required`);
418
+ return value;
419
+ }
420
+ function parseValidateInput(raw) {
421
+ const obj = asObject(raw, "validate_workflow_definition");
393
422
  if (obj.definition === null || typeof obj.definition != "object")
394
423
  throw new Error("validate_workflow_definition: `definition` is required and must be an object");
395
424
  return { definition: obj.definition };
396
425
  }
397
426
  function parseInstanceIdInput(raw, tool) {
398
- if (raw === null || typeof raw != "object")
399
- throw new Error(`${tool}: input must be an object`);
400
- const obj = raw;
401
- if (typeof obj.instance_id != "string" || obj.instance_id === "")
402
- throw new Error(`${tool}: instance_id is required`);
403
- return { instance_id: obj.instance_id };
427
+ const obj = asObject(raw, tool);
428
+ return { instance_id: requireString({ obj, key: "instance_id", tool }) };
404
429
  }
405
430
  function parseFireActionInput(raw) {
406
- if (raw === null || typeof raw != "object")
407
- throw new Error("fire_action: input must be an object");
408
- const obj = raw;
409
- for (const key of ["instance_id", "activity", "action"])
410
- if (typeof obj[key] != "string" || obj[key] === "")
411
- throw new Error(`fire_action: ${key} is required`);
431
+ const obj = asObject(raw, "fire_action");
432
+ let params;
433
+ if (obj.params !== void 0) {
434
+ if (obj.params === null || typeof obj.params != "object" || Array.isArray(obj.params))
435
+ throw new Error("fire_action: params must be an object");
436
+ params = obj.params;
437
+ }
412
438
  return {
413
- instance_id: obj.instance_id,
414
- activity: obj.activity,
415
- action: obj.action
439
+ instance_id: requireString({ obj, key: "instance_id", tool: "fire_action" }),
440
+ activity: requireString({ obj, key: "activity", tool: "fire_action" }),
441
+ action: requireString({ obj, key: "action", tool: "fire_action" }),
442
+ ...params !== void 0 ? { params } : {}
416
443
  };
417
444
  }
418
445
  async function listInstances(engine, input) {
@@ -428,14 +455,22 @@ async function listInstances(engine, input) {
428
455
  instances: filtered.map((doc) => projectSummary(doc, subjectTitles))
429
456
  };
430
457
  }
431
- async function getInstanceState(engine, instanceId, accessOverride) {
458
+ async function getInstanceState({
459
+ engine,
460
+ instanceId,
461
+ accessOverride
462
+ }) {
432
463
  const evaluation = await engine.evaluateInstance({
433
464
  instanceId,
434
465
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
435
466
  }), instance = await engine.getInstance({ instanceId }), subjectTitles = await fetchSubjectTitles(engine, [instance]);
436
- return projectState(instance, evaluation, subjectTitles);
467
+ return projectState({ instance, evaluation, subjectTitles });
437
468
  }
438
- async function getInstanceDiagnosis(engine, instanceId, accessOverride) {
469
+ async function getInstanceDiagnosis({
470
+ engine,
471
+ instanceId,
472
+ accessOverride
473
+ }) {
439
474
  const { diagnosis, remediations } = await engine.diagnose({
440
475
  instanceId,
441
476
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/authoring-guide.ts","../src/descriptors.ts","../src/projection.ts","../src/tools.ts"],"sourcesContent":["/**\n * Content for the `get_workflow_authoring_guide` tool.\n *\n * The agent calling the MCP server generates the workflow definition; this\n * guide is what teaches it the DSL well enough to do that on the first pass.\n * It is the orientation the examples can't convey on their own (the GROQ\n * built-ins, the sugars, the hard rules) plus two worked examples.\n *\n * The examples are kept inline (rather than imported from\n * `@sanity/workflow-examples`) on purpose: that package exports the *desugared*\n * `WorkflowDefinition` (the post-`defineWorkflow` shape), but an agent should emit\n * the *authoring* (sugar) shape — JSON, exactly what `validate_workflow_definition`\n * accepts — so importing the examples would show the wrong shape. Keeping them\n * inline also avoids promoting `@sanity/workflow-examples` to a runtime dependency\n * of a package headed for host embedding. The companion test runs every example\n * through the real validators, so a stale snippet fails CI rather than misleading\n * an agent.\n */\n\nimport type {AuthoringWorkflow} from '@sanity/workflow-engine'\n\nconst minimalExample = {\n name: 'quick-approval',\n title: 'Quick approval',\n description: 'One review stage with a single approve action, then a terminal stage.',\n initialStage: 'review',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'review',\n title: 'Review',\n activities: [\n {\n name: 'decide',\n title: 'Decide',\n activation: 'auto',\n actions: [{name: 'approve', title: 'Approve', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-approved', title: 'Approve and finish', to: 'approved'}],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\nconst reviewLoopExample = {\n name: 'doc-review',\n title: 'Document review',\n description: 'Draft, then editorial review that approves or rejects back to drafting.',\n initialStage: 'drafting',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'drafting',\n title: 'Drafting',\n activities: [\n {\n name: 'write',\n title: 'Write the draft',\n activation: 'auto',\n actions: [{name: 'submit', title: 'Submit for review', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-review', title: 'Send to review', to: 'review'}],\n },\n {\n name: 'review',\n title: 'Editorial review',\n activities: [\n {\n name: 'review',\n title: 'Review the draft',\n activation: 'auto',\n actions: [\n {name: 'approve', title: 'Approve', status: 'done'},\n {name: 'reject', title: 'Reject', status: 'failed'},\n ],\n },\n ],\n transitions: [\n {name: 'to-approved', title: 'Approve and finish', to: 'approved'},\n // Reject flips the activity to `failed`; this routes the instance back.\n {\n name: 'to-drafting',\n title: 'Send back to drafting',\n to: 'drafting',\n filter: '$anyActivityFailed',\n },\n ],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\n/**\n * Worked examples included verbatim in {@link AUTHORING_GUIDE}, and exercised by\n * the drift-guard test. Authoring (sugar) shape — what an agent passes to\n * `validate_workflow_definition`.\n */\nexport const GUIDE_EXAMPLES: AuthoringWorkflow[] = [minimalExample, reviewLoopExample]\n\nconst ORIENTATION = `# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (filters, predicates, guards) is a GROQ *string*. Generate the JSON,\nthen call \\`validate_workflow_definition\\` to check it; fix the reported errors\nand re-validate until it returns \\`valid: true\\`. The validator returns the\n*desugared* definition under \\`definition\\` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: \\`{ name, title, description?, initialStage, fields?, stages[], predicates? }\\`.\n \\`initialStage\\` must be the \\`name\\` of a declared stage. Do NOT include a\n \\`version\\` — definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: \\`{ name, title?, description?, activities?, transitions? }\\`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: \\`{ name, title?, activation?, actions? }\\`. \\`activation\\` is \\`'auto'\\`\n (starts when the stage is entered) or \\`'manual'\\` (default — a human starts it).\n- **Action**: \\`{ name, title?, status?, params?, ops? }\\`.\n \\`status: 'done' | 'skipped' | 'failed'\\` is sugar that flips the *firing activity* to\n that status when the action fires. \\`ops\\` are mutations applied when the action\n fires (e.g. \\`{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}\\`).\n \\`params\\` are values the action collects from the caller (referenced by\n \\`{type:'param', param:'<name>'}\\` sources).\n- **Transition**: \\`{ name, title?, to, filter? }\\`. \\`to\\` must name a declared\n stage. \\`filter\\` is a GROQ condition gating the exit; omit it and it defaults\n to \\`$allActivitiesDone\\`.\n- **Field** (workflow- or stage-scoped persistent state): \\`{ type, name, title?, initialValue? }\\`.\n Scalar \\`type\\`s mirror Sanity: \\`string\\`, \\`text\\` (multiline), \\`number\\`,\n \\`boolean\\`, \\`date\\` (YYYY-MM-DD), \\`datetime\\` (ISO), \\`url\\`. Plus references\n (\\`doc.ref\\`, \\`doc.refs\\`, \\`release.ref\\`), identities (\\`actor\\` — one concrete\n principal; \\`assignee\\` / \\`assignees\\` — one or many user-or-role assignees), and\n the compositional kinds \\`object\\` (\\`{ type:'object', name, fields: [...] }\\`) and\n \\`array\\` (\\`{ type:'array', name, of: [...] }\\`) — \\`fields\\`/\\`of\\` are themselves\n field shapes, so any structure composes. \\`initialValue\\` seeds the slot once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: \\`{type:'input'}\\` (the caller supplies it when the\n instance starts), \\`{type:'query', query:'<groq>'}\\` (computed from the lake),\n \\`{type:'literal', value:<json>}\\`, or \\`{type:'fieldRead', field:'<name>'}\\`.\n By convention a workflow has an \\`input\\`-sourced \\`doc.ref\\` named \\`subject\\` — the\n headline document it is about.\n Two list sugars desugar to \\`array\\`: \\`{type:'todoList', name}\\` (ad-hoc\n status-tracked work — rows \\`{ label, status, assignee?, dueDate? }\\`) and\n \\`{type:'notes', name}\\` (an append-only audit/comment log — rows\n \\`{ body, actor, at }\\`; pairs with the \\`audit\\` op, which stamps \\`actor\\`/\\`at\\`).\n\n## GROQ in conditions\n\nBuilt-in variables available in filters/predicates: \\`$allActivitiesDone\\`,\n\\`$anyActivityFailed\\` (booleans over the current stage's activities), \\`$activities\\` (the activity\nlist), \\`$fields\\` (field values), \\`$now\\`, and the caller-scoped vars \\`$actor\\`\n(the acting user), \\`$assigned\\` (whether the caller is the activity's assignee — the\nidiomatic permission gate, used as \\`filter: '$assigned'\\`), and \\`$can\\`. Define\nreusable named conditions under top-level \\`predicates: { name: '<groq>' }\\` and\nreference them as \\`$name\\` (e.g.\n\\`predicates: { ready: \"count($activities[status != 'done']) == 0\" }\\` → \\`$ready\\`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by \\`_type\\` (e.g. \\`*[_type==\"article\"]\\`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a \\`doc.ref\\` field for it.\n\n## Sugars worth knowing\n\n- Action \\`status: 'done' | 'skipped' | 'failed'\\` — flips the firing activity (shown above).\n- Omitted transition \\`filter\\` — defaults to \\`$allActivitiesDone\\`.\n- Omitted activity \\`activation\\` — defaults to \\`'manual'\\`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. \\`review → drafting\\` gated\n on \\`$anyActivityFailed\\`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment — a workflow should not\n dead-end just because something was declined.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition \\`to\\` and \\`initialStage\\` names a declared stage.\n- Custom \\`predicates\\` must not shadow a built-in (e.g. \\`allActivitiesDone\\`).\n- Every GROQ string must parse and must not be a \\`_type\\` discovery scan.`\n\n/**\n * The full text returned by `get_workflow_authoring_guide`: orientation plus the\n * two worked examples rendered as JSON.\n */\nexport const AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\\`\\`\\`json\n${JSON.stringify(minimalExample, null, 2)}\n\\`\\`\\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\\`\\`\\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\\`\\`\\`\n`\n","/**\n * The MCP tool surface for workflow operations — the static descriptors an\n * agent reads to decide what to call. The implementations live in `tools.ts`;\n * these are pure data (descriptions + JSON schemas) and safe to cache.\n *\n * Two jobs: operate running instances, and author new definitions.\n *\n * Operate an instance:\n *\n * - `list_workflow_instances` is the only way to *discover* what's\n * running without already knowing an id. Filterable, capped.\n * - `get_workflow_state` is the \"tell me everything actionable about\n * one instance\" call. It composes the engine's `evaluate` projection\n * into a flat shape an LLM can reason over without piecing together\n * raw `WorkflowInstance` documents.\n * - `diagnose_workflow` answers \"*why* isn't this moving?\" — it\n * classifies an instance as healthy (waiting/progressing) or genuinely\n * stuck (a failed effect/activity, a dead-end transition) and names the\n * remediations that would unstick it. A pure read; the remediations are\n * advisory (none execute through this server).\n * - `fire_action` is the only write. Mirrors the engine's universal\n * \"something happened\" entry point — everything other than admin\n * override flows through it.\n *\n * Author a definition (both pure, engine-independent, neither deploys):\n *\n * - `get_workflow_authoring_guide` returns the DSL guide an agent reads\n * before writing a definition — shape, GROQ built-ins, sugars, examples.\n * - `validate_workflow_definition` runs the deploy-time checks (structure +\n * invariants + GROQ) and returns the desugared definition or a\n * path-prefixed error list. A human deploys it via the CLI.\n *\n * Surface choices that matter for an LLM consumer:\n *\n * - Descriptions are written for an agent, not a human reader.\n * Each one says (a) what the tool does, (b) when to use it,\n * (c) what *not* to use it for.\n * - Parameter names use snake_case, matching MCP convention.\n * - The write tool's description spells out that this is the way\n * to advance workflow state — not a side door. Without that line,\n * an LLM tends to look for a `complete_activity` or `set_stage` tool.\n * - Read tools cap their output. A 50-page `recent_history` would\n * burn context and is rarely informative; 8 is enough to spot\n * loops or stuck-in-stage patterns.\n */\n\n/**\n * The descriptor shape the Anthropic Messages API + the MCP SDK both\n * accept. Kept here as a small, transport-neutral interface so we can\n * feed the same descriptors to either consumer.\n */\nexport interface ToolDescriptor {\n name: string\n description: string\n /** JSON Schema describing the tool's parameters. */\n input_schema: {\n type: 'object'\n properties: Record<string, unknown>\n required?: string[]\n additionalProperties?: boolean\n }\n}\n\n/** Maximum instances returned by list_workflow_instances. */\nexport const LIST_CAP = 25\n\n/**\n * The static tool descriptors. Independent of any engine — the bound\n * implementations that close over an engine live in {@link buildTools}.\n */\nexport const TOOL_DESCRIPTORS: ToolDescriptor[] = [\n {\n name: 'list_workflow_instances',\n description:\n 'List workflow instances in the configured workspace. Use this when you need ' +\n \"to find a workflow but don't already know its instance id, or to survey \" +\n \"what's in flight. Returns a compact summary — id, workflow type id and \" +\n 'human-readable `workflowTitle`, current stage, whether the instance is done, ' +\n 'and (when the workflow has a conventional subject document) a `subject` ' +\n \"field with the subject doc's ref and title. Use `workflowTitle` when the \" +\n 'user names the workflow by type (e.g. \"article reviews\") and `subject.title` ' +\n \"when they name a specific in-flight instance by what it's about (e.g. \" +\n '\"the article-review about pricing\"). ' +\n 'Capped at ' +\n String(LIST_CAP) +\n ' results. ' +\n 'Do NOT use this to inspect a single known instance — use get_workflow_state ' +\n 'for that, the response will be richer.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'string',\n description:\n \"Optional. Restrict to instances of this workflow definition (e.g. 'article-review').\",\n },\n status: {\n type: 'string',\n enum: ['in_flight', 'done', 'any'],\n description:\n \"Optional. 'in_flight' returns instances not in a terminal stage; \" +\n \"'done' returns completed/aborted; 'any' returns both. Defaults to 'in_flight'.\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_state',\n description:\n 'Get the current state of a single workflow instance, projected for action. ' +\n \"Returns the workflow's id and human-readable `workflowTitle`, the current \" +\n 'stage, the subject document the workflow is about (when the workflow has a ' +\n 'conventional subject — its ref and title), every activity on the current stage ' +\n 'with its available actions (and whether each action is currently allowed), ' +\n 'and the most recent history entries. Use this whenever you need to understand ' +\n \"what's possible on an instance before deciding to act. \" +\n 'This is a pure read — it does not change anything. ' +\n 'If you only need to discover what instances exist, use list_workflow_instances ' +\n 'instead; this tool requires you to know the instance id. ' +\n 'list_workflow_instances also returns the same `workflowTitle` and `subject` ' +\n 'fields, so prefer it for fan-out discovery rather than polling ' +\n 'get_workflow_state per instance.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'diagnose_workflow',\n description:\n \"Explain why a single workflow instance is or isn't progressing. Returns a \" +\n 'verdict (`state`: progressing, waiting, blocked, completed, aborted, or ' +\n 'stuck), a one-line `summary`, and — when stuck — a structured `cause` plus ' +\n 'the `remediations` that would unstick it. Use this when an instance seems ' +\n 'stalled or the user asks \"why isn\\'t this moving?\": it distinguishes a healthy ' +\n 'instance (waiting on a human, or will advance on its own) from a genuinely ' +\n 'stuck one (a failed effect or activity, a dead-end transition). ' +\n 'This is a pure read — it changes nothing, and the remediations it names are ' +\n 'advisory: none can be executed through this server. To actually advance a ' +\n 'healthy waiting instance use fire_action; for per-activity action detail use ' +\n 'get_workflow_state.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'fire_action',\n description:\n 'Advance a workflow instance by firing an action on one of its activities. ' +\n 'This is the only way to advance workflow state from the outside — there is no ' +\n \"separate 'complete activity' or 'transition stage' tool. To find the right \" +\n '(activity, action) pair, call get_workflow_state first and pick from the ' +\n \"allowed actions listed on the current stage's activities. \" +\n 'After firing, the engine cascades any auto-transitions that become eligible ' +\n \"(so an 'approve' action on a review activity may transition the workflow to a \" +\n 'terminal stage in one shot). Returns the resulting state, same shape as ' +\n 'get_workflow_state. ' +\n 'If the action is not currently allowed (e.g. the activity is already done, or a ' +\n 'guard fails), this returns an error describing why.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n activity: {\n type: 'string',\n description:\n 'The id of the activity on the current stage. Must be one of the activities ' +\n 'returned by get_workflow_state.',\n },\n action: {\n type: 'string',\n description:\n 'The id of the action on that activity. Must be one of the actions listed ' +\n 'as allowed=true on the activity.',\n },\n },\n required: ['instance_id', 'activity', 'action'],\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_authoring_guide',\n description:\n 'Get the guide for authoring a workflow definition: the DSL shape, the ' +\n 'GROQ condition built-ins, the sugars, the rules the validator enforces, ' +\n 'and two worked JSON examples. Call this BEFORE writing a definition from ' +\n 'a description, then generate the JSON and check it with ' +\n 'validate_workflow_definition. Pure read; takes no arguments.',\n input_schema: {\n type: 'object',\n properties: {},\n additionalProperties: false,\n },\n },\n {\n name: 'validate_workflow_definition',\n description:\n 'Validate a workflow definition you have authored. Runs the same checks as ' +\n 'deploy — structural shape, cross-field invariants (e.g. every transition ' +\n 'target is a declared stage), and GROQ syntax — without writing anything. ' +\n 'Returns `{valid:true, definition}` where `definition` is the desugared form ' +\n 'that would deploy, or `{valid:false, error}` with every problem listed and ' +\n 'path-prefixed. This does NOT deploy — a human deploys the validated ' +\n 'definition via the CLI. Call get_workflow_authoring_guide first for the ' +\n 'shape; on `valid:false`, fix the reported problems and validate again.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'object',\n description:\n 'The workflow definition to validate, as a JSON object in authoring ' +\n 'shape. See get_workflow_authoring_guide for the shape and examples.',\n },\n },\n required: ['definition'],\n additionalProperties: false,\n },\n },\n]\n","/**\n * Projection helpers — engine output shaped for an LLM consumer.\n *\n * Why a hand-written projection rather than returning the engine's\n * `WorkflowEvaluation` raw: the LLM only needs (a) which activities exist,\n * (b) which actions are allowed on each, (c) why an action is denied\n * when it is. The evaluation includes per-stage field-entry resolution\n * and snapshot bookkeeping that bloats the payload without helping\n * the model pick the right next move.\n *\n * The engine reads that feed these live in `tools.ts`; this module is\n * pure shaping plus the one batched subject-title lookup they all share.\n */\n\nimport {\n actionVerdict,\n extractDocumentId,\n isTerminalStage,\n type Diagnosis,\n type Engine,\n type StuckCause,\n type WorkflowDefinition,\n type WorkflowEvaluation,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport type {\n ProjectedActionVerdict,\n ProjectedHistoryEntry,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ProjectedSubject,\n ProjectedActivity,\n} from './types.ts'\n\n/**\n * The field-entry name we treat as the workflow's \"subject\" — the headline\n * doc the workflow is about. Convention-only (see ProjectedSubject docs);\n * a workflow without an entry with this name won't have `subject` populated\n * on the projection.\n */\nconst SUBJECT_ENTRY_NAME = 'subject'\n\n/** Maximum history entries returned in get_workflow_state. */\nconst HISTORY_CAP = 8\n\n// Subject doc resolution\n//\n// The conventional \"subject\" entry holds a doc.ref to the thing the\n// workflow is about. The entry value is a GDR URI (e.g.\n// `dataset:proj:ds:article-1`) — what `_id` in the store actually\n// uses is the bare `article-1` part. Extract, batch-fetch by id list,\n// build a Map keyed by GDR URI.\n//\n// Known gap: cross-resource subject docs. If the subject lives in a\n// different Sanity resource than the engine's own workflowResource,\n// this lookup misses — the engine's `query` runs against the workflow\n// resource's client. Production-shape fix is to thread the engine's\n// `resourceClients` resolver through here; out of scope for v0.1.\n\n/**\n * Look up the conventional subject entry on a workflow instance's\n * workflow-scope fields. Returns the GDR URI when present, undefined\n * when the workflow has no subject entry or the entry is null.\n */\nfunction getSubjectGdr(instance: WorkflowInstance): string | undefined {\n const entry = instance.fields.find((s) => s._type === 'doc.ref' && s.name === SUBJECT_ENTRY_NAME)\n if (entry === undefined || entry._type !== 'doc.ref') return undefined\n return entry.value?.id\n}\n\n/**\n * Batch-fetch the `title` field for every subject doc referenced by\n * the supplied instances. Returns a `Map<gdrUri, title>` — missing\n * keys mean the doc had no title (or didn't exist / was unreadable).\n * The caller projects `ref` from the GDR either way; this map only\n * controls whether `title` is populated.\n *\n * Single GROQ call regardless of how many instances are supplied. At\n * `LIST_CAP=25` this is cheap; per-instance lookups would be N+1.\n */\nexport async function fetchSubjectTitles(\n engine: Engine,\n instances: WorkflowInstance[],\n): Promise<Map<string, string>> {\n // Build docId → gdrUri so we can attribute results back. Skip any\n // malformed GDRs silently — the projection still surfaces `ref`,\n // the LLM falls back to that as an identifier.\n const docIdToGdr = new Map<string, string>()\n for (const inst of instances) {\n const gdr = getSubjectGdr(inst)\n if (gdr === undefined) continue\n try {\n docIdToGdr.set(extractDocumentId(gdr), gdr)\n } catch {\n // Malformed GDR — leave it out of the title lookup.\n }\n }\n if (docIdToGdr.size === 0) return new Map()\n\n // Subject docs are ordinary content, not workflow docs — they carry no\n // engine tag, so we read them straight off the engine's client rather\n // than through the tag-scoped `engine.query` (which mandates a\n // `$tag` filter). Access is the client credential's concern: a\n // doc the caller can't read simply comes back absent.\n let docs: {_id: string; title?: string}[]\n try {\n docs = await engine.client.fetch<{_id: string; title?: string}[]>(\n '*[_id in $docIds]{_id, title}',\n {docIds: Array.from(docIdToGdr.keys())},\n )\n } catch (err) {\n // Titles are optional enrichment — degrade to bare refs rather than\n // failing the whole tool call when the lookup can't be served.\n process.stderr.write(\n `workflow-mcp: subject-title lookup failed; returning refs without titles: ${\n err instanceof Error ? err.message : String(err)\n }\\n`,\n )\n return new Map()\n }\n\n const out = new Map<string, string>()\n for (const doc of docs) {\n const gdr = docIdToGdr.get(doc._id)\n if (gdr !== undefined && typeof doc.title === 'string' && doc.title !== '') {\n out.set(gdr, doc.title)\n }\n }\n return out\n}\n\n/**\n * Build a `ProjectedSubject` for one instance, given the title map\n * produced by `fetchSubjectTitles`. Returns undefined when the\n * workflow has no subject entry; returns `{ ref }` without `title`\n * when the entry is present but the doc has no title field.\n */\nfunction buildSubject(\n instance: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedSubject | undefined {\n const gdr = getSubjectGdr(instance)\n if (gdr === undefined) return undefined\n const title = subjectTitles.get(gdr)\n return title !== undefined ? {ref: gdr, title} : {ref: gdr}\n}\n\nexport function projectSummary(\n doc: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedInstanceSummary {\n const definition = JSON.parse(doc.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === doc.currentStage)\n const stageTitle = stage?.title\n const workflowTitle = definition.title\n const subject = buildSubject(doc, subjectTitles)\n return {\n instanceId: doc._id,\n definition: doc.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: doc.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n lastChangedAt: doc.lastChangedAt,\n done: isInstanceDone(doc),\n ...(subject !== undefined ? {subject} : {}),\n }\n}\n\nexport function projectState(\n instance: WorkflowInstance,\n evaluation: WorkflowEvaluation,\n subjectTitles: Map<string, string>,\n): ProjectedInstanceState {\n // The evaluation already exposes the current stage's title from the\n // snapshot, but not the workflow's own title — parse the snapshot\n // once here to read it.\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const workflowTitle = definition.title\n const stageTitle = evaluation.currentStage.stage.title\n\n const activities: ProjectedActivity[] = evaluation.currentStage.activities.map((te) => {\n // Each action's verdict comes from the engine's shared `actionVerdict` —\n // the same \"is this firable, and why not\" the CLI reads — so the verdict\n // shape has one source; we keep the LLM-shaped per-activity nesting.\n const actions: ProjectedActionVerdict[] = te.actions.map((ae) => {\n const verdict = actionVerdict(te, ae)\n return {\n action: verdict.action,\n ...(verdict.title !== undefined ? {title: verdict.title} : {}),\n allowed: verdict.allowed,\n ...(verdict.allowed === false && verdict.disabledReason !== undefined\n ? {disabledReason: formatDisabledReason(verdict.disabledReason)}\n : {}),\n }\n })\n\n return {\n activity: te.activity.name,\n ...(te.activity.title !== undefined ? {title: te.activity.title} : {}),\n ...(te.activity.description !== undefined ? {description: te.activity.description} : {}),\n status: te.status,\n actions,\n }\n })\n\n const recentHistory: ProjectedHistoryEntry[] = instance.history\n .slice(-HISTORY_CAP)\n .toReversed()\n .map((entry) => ({\n at: entry.at,\n type: entry._type,\n summary: summariseHistoryEntry(entry),\n }))\n\n const subject = buildSubject(instance, subjectTitles)\n return {\n instanceId: instance._id,\n definition: instance.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: instance.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n done: isInstanceDone(instance),\n ...(subject !== undefined ? {subject} : {}),\n activities,\n recentHistory,\n }\n}\n\nexport function isInstanceDone(instance: WorkflowInstance): boolean {\n // The engine sets `completedAt` when an instance enters a terminal\n // stage; falling back to the structural check covers any edge case\n // where the definition snapshot is the source of truth.\n if (instance.completedAt !== undefined) return true\n try {\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === instance.currentStage)\n return stage !== undefined && isTerminalStage(stage)\n } catch {\n return false\n }\n}\n\n/**\n * Engine `disabledReason` is a structured tagged union (see\n * `DisabledReason` in @sanity/workflow-engine). For an LLM\n * we want a short English phrase rather than the tagged shape — the\n * tagged shape is still available on the raw evaluation if a richer\n * consumer wants it.\n */\nconst DISABLED_REASON_TEXT: Record<string, string> = {\n 'filter-failed': \"action's filter condition did not hold for this actor\",\n 'activity-not-active': 'activity is not currently active',\n 'stage-terminal': 'stage is terminal — no further actions possible',\n 'instance-completed': 'instance has already completed',\n 'requirements-unmet': \"activity's declared requirements are not yet satisfied\",\n}\n\nfunction formatDisabledReason(reason: {kind: string}): string {\n // Fall back to a humanised form of the kind for reasons we don't have\n // bespoke copy for, so a new engine reason never renders as a raw enum.\n return DISABLED_REASON_TEXT[reason.kind] ?? reason.kind.replace(/-/g, ' ')\n}\n\nfunction summariseHistoryEntry(entry: {_type: string} & Record<string, unknown>): string {\n switch (entry._type) {\n case 'stageEntered':\n return `entered stage \"${String(entry.stage)}\"`\n case 'stageExited':\n return `exited stage \"${String(entry.stage)}\" → \"${String(entry.toStage)}\"`\n case 'activityActivated':\n return `activity \"${String(entry.activity)}\" activated`\n case 'activityStatusChanged':\n return `activity \"${String(entry.activity)}\" status: ${String(entry.from)} → ${String(entry.to)}`\n case 'actionFired':\n return `action \"${String(entry.action)}\" fired on activity \"${String(entry.activity)}\"`\n case 'transitionFired':\n return `transition fired: \"${String(entry.fromStage)}\" → \"${String(entry.toStage)}\"`\n case 'effectQueued':\n return `effect \"${String(entry.effect)}\" queued`\n case 'effectCompleted':\n return `effect \"${String(entry.effect)}\" completed (${String(entry.status)})`\n case 'spawned':\n return `spawned child workflow from activity \"${String(entry.activity)}\"`\n default:\n return entry._type\n }\n}\n\n// Diagnosis summary — a plain-English one-liner per verdict, the LLM-facing\n// counterpart to the CLI's terminal-decorated rendering. The engine's\n// `diagnostics` module deliberately does no rendering, so each consumer writes\n// its own; this one folds the key identifiers (the stuck effect/activity, the\n// waiting actions) into one undecorated line.\n\n/** Exported for unit coverage of every verdict branch — see `tools.test.ts`.\n * Used internally by the diagnose_workflow read in `tools.ts`. */\nexport function diagnosisSummary(diagnosis: Diagnosis): string {\n switch (diagnosis.state) {\n case 'progressing':\n return 'This instance will advance on its own.'\n case 'waiting':\n return `Waiting for action on activity \"${diagnosis.activity}\": ${diagnosis.actions.join(' or ')}. This is the normal in-flight state — it advances when someone acts.`\n case 'blocked':\n return `Activity \"${diagnosis.activity}\" is visible but not yet executable — unmet requirement(s): ${diagnosis.requirements.join(', ')}. It will not advance until those are satisfied.`\n case 'completed':\n return `Completed at ${diagnosis.at}.`\n case 'aborted':\n return diagnosis.reason !== undefined\n ? `Aborted at ${diagnosis.at}: ${diagnosis.reason}.`\n : `Aborted at ${diagnosis.at}.`\n case 'stuck':\n return stuckSummary(diagnosis.cause)\n }\n}\n\nfunction stuckSummary(cause: StuckCause): string {\n switch (cause.kind) {\n case 'failed-effect':\n return `Stuck: a failed effect \"${cause.effect.name}\" is blocking activity \"${cause.effect.origin.name}\", which can't complete until the effect succeeds.`\n case 'hung-effect':\n return `Stuck: effect \"${cause.effect.name}\" was claimed but never completed — the drainer likely died mid-dispatch, so it won't drain on its own.`\n case 'failed-activity':\n return `Stuck: activity \"${cause.activity}\" is in a terminal failed state, so any exit transition gated on it can never fire.`\n case 'no-transition-fires':\n return `Stuck: every activity is resolved but no exit transition's filter is satisfied — likely a routing state value a filter reads was never written.`\n case 'transition-unevaluable':\n return `Stuck (recoverable): exit transition(s) ${cause.transitions.join(', ')} read an operand that is missing or unreadable (GROQ null). It advances on its own once that data becomes readable — no manual fix needed.`\n }\n}\n","/**\n * Builds the MCP tool surface: binds the static descriptors (`descriptors.ts`)\n * to implementations that close over a single engine, parses tool input, and\n * runs the engine reads that the projection helpers (`projection.ts`) shape.\n */\n\nimport {\n validateDefinition,\n WORKFLOW_INSTANCE_TYPE,\n tagScopeFilter,\n type AuthoringWorkflow,\n type Engine,\n type WorkflowAccessOverride,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\nimport {defineWorkflow} from '@sanity/workflow-engine/define'\n\nimport {AUTHORING_GUIDE} from './authoring-guide.ts'\nimport {LIST_CAP, TOOL_DESCRIPTORS, type ToolDescriptor} from './descriptors.ts'\nimport {\n diagnosisSummary,\n fetchSubjectTitles,\n isInstanceDone,\n projectState,\n projectSummary,\n} from './projection.ts'\nimport type {\n ProjectedDiagnosis,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ValidateDefinitionResult,\n} from './types.ts'\n\n/**\n * A tool implementation is a function from validated input → JSON-able\n * output. The runner is responsible for serialising the output and for\n * surfacing thrown errors as `is_error` tool results.\n */\nexport type ToolImpl = (input: unknown) => Promise<unknown>\n\nexport interface BuiltTools {\n descriptors: ToolDescriptor[]\n impls: Record<string, ToolImpl>\n}\n\n/**\n * Optional knobs for `buildTools`.\n *\n * `access` — override the engine's actor + grants resolution for every\n * call the tools make. In production, leave this unset: the engine\n * resolves the actor from the supplied client's token via `/users/me`.\n * In tests, pass `ALL_ACCESS` (from `@sanity/workflow-engine-test`)\n * because the TestClient has no token and would otherwise throw on\n * the resolution path.\n */\nexport interface BuildToolsOptions {\n access?: WorkflowAccessOverride\n}\n\n/**\n * Build the MCP tool descriptors and bound implementations against a\n * single engine. Pass the engine the tools should operate on; the\n * returned descriptors are static (safe to cache), the implementations\n * close over the engine.\n *\n * In production this is called once at server boot. In tests it's\n * called per-bench so each eval case has an isolated engine.\n */\nexport function buildTools(engine: Engine, options: BuildToolsOptions = {}): BuiltTools {\n const accessOverride = options.access\n\n const impls: Record<string, ToolImpl> = {\n list_workflow_instances: async (rawInput) => {\n const input = parseListInput(rawInput)\n return listInstances(engine, input)\n },\n get_workflow_state: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'get_workflow_state')\n return getInstanceState(engine, input.instance_id, accessOverride)\n },\n diagnose_workflow: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'diagnose_workflow')\n return getInstanceDiagnosis(engine, input.instance_id, accessOverride)\n },\n fire_action: async (rawInput) => {\n const input = parseFireActionInput(rawInput)\n await engine.fireAction({\n instanceId: input.instance_id,\n activity: input.activity,\n action: input.action,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n // Re-use the same projection so the LLM sees consistent shapes\n // between get_workflow_state and fire_action responses.\n return getInstanceState(engine, input.instance_id, accessOverride)\n },\n // Authoring tools — engine-independent: they teach and check the DSL, they\n // don't touch the lake or an instance.\n get_workflow_authoring_guide: async () => AUTHORING_GUIDE,\n validate_workflow_definition: async (rawInput): Promise<ValidateDefinitionResult> => {\n const {definition} = parseValidateInput(rawInput)\n try {\n // `defineWorkflow` is the gate (valibot structural parse + desugar +\n // cross-field invariants); `validateDefinition` adds GROQ syntax checks.\n // The cast only satisfies the compiler — `defineWorkflow` validates the\n // value at runtime and throws an aggregated, path-prefixed error.\n const stored = defineWorkflow(definition as AuthoringWorkflow)\n validateDefinition(stored)\n return {valid: true, definition: stored}\n } catch (err) {\n return {valid: false, error: err instanceof Error ? err.message : String(err)}\n }\n },\n }\n\n return {descriptors: TOOL_DESCRIPTORS, impls}\n}\n\n// Input parsers — lightweight, no zod dependency\n//\n// Reasoning: pulling in zod would inflate the package for this handful of tools.\n// The MCP server and the eval harness already constrain the shape via\n// the JSON schema in the descriptor; these parsers exist for runtime\n// type-narrowing in TypeScript, not for deep validation.\n\ninterface ListInput {\n definition?: string\n status: 'in_flight' | 'done' | 'any'\n}\n\nfunction parseListInput(raw: unknown): ListInput {\n if (raw === null || typeof raw !== 'object') {\n return {status: 'in_flight'}\n }\n const obj = raw as Record<string, unknown>\n const status = obj.status === 'done' || obj.status === 'any' ? obj.status : ('in_flight' as const)\n const definition = typeof obj.definition === 'string' ? obj.definition : undefined\n return definition !== undefined ? {definition, status} : {status}\n}\n\n/** Narrow the validate input to `{definition: object}`. The deep validation\n * is `defineWorkflow`'s job — this only guards the envelope. */\nfunction parseValidateInput(raw: unknown): {definition: object} {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('validate_workflow_definition: input must be an object')\n }\n const obj = raw as Record<string, unknown>\n if (obj.definition === null || typeof obj.definition !== 'object') {\n throw new Error('validate_workflow_definition: `definition` is required and must be an object')\n }\n return {definition: obj.definition}\n}\n\n/** Shared by every single-instance tool — `tool` names the caller so the\n * thrown error points at the right tool. */\nfunction parseInstanceIdInput(raw: unknown, tool: string): {instance_id: string} {\n if (raw === null || typeof raw !== 'object') {\n throw new Error(`${tool}: input must be an object`)\n }\n const obj = raw as Record<string, unknown>\n if (typeof obj.instance_id !== 'string' || obj.instance_id === '') {\n throw new Error(`${tool}: instance_id is required`)\n }\n return {instance_id: obj.instance_id}\n}\n\nfunction parseFireActionInput(raw: unknown): {\n instance_id: string\n activity: string\n action: string\n} {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('fire_action: input must be an object')\n }\n const obj = raw as Record<string, unknown>\n for (const key of ['instance_id', 'activity', 'action'] as const) {\n if (typeof obj[key] !== 'string' || obj[key] === '') {\n throw new Error(`fire_action: ${key} is required`)\n }\n }\n return {\n instance_id: obj.instance_id as string,\n activity: obj.activity as string,\n action: obj.action as string,\n }\n}\n\n// Engine reads, projected for an LLM\n\nasync function listInstances(\n engine: Engine,\n input: ListInput,\n): Promise<{instances: ProjectedInstanceSummary[]}> {\n // Tag-scoped via the engine's own query helper — every instance\n // returned is one this engine is allowed to see.\n const groqParts = [`_type == \"${WORKFLOW_INSTANCE_TYPE}\"`]\n if (input.definition !== undefined) {\n groqParts.push('definition == $definition')\n }\n const groq =\n `*[${groqParts.join(' && ')} && ${tagScopeFilter()}] ` +\n `| order(lastChangedAt desc)[0...${LIST_CAP}]`\n\n const params: Record<string, unknown> = {}\n if (input.definition !== undefined) params.definition = input.definition\n\n const docs = await engine.query<WorkflowInstance[]>({groq, params})\n const filtered = docs.filter((doc) => {\n const done = isInstanceDone(doc)\n if (input.status === 'done') return done\n if (input.status === 'in_flight') return !done\n return true // \"any\"\n })\n const subjectTitles = await fetchSubjectTitles(engine, filtered)\n return {\n instances: filtered.map((doc) => projectSummary(doc, subjectTitles)),\n }\n}\n\nasync function getInstanceState(\n engine: Engine,\n instanceId: string,\n accessOverride: WorkflowAccessOverride | undefined,\n): Promise<ProjectedInstanceState> {\n const evaluation = await engine.evaluateInstance({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n const instance = await engine.getInstance({instanceId})\n const subjectTitles = await fetchSubjectTitles(engine, [instance])\n return projectState(instance, evaluation, subjectTitles)\n}\n\nasync function getInstanceDiagnosis(\n engine: Engine,\n instanceId: string,\n accessOverride: WorkflowAccessOverride | undefined,\n): Promise<ProjectedDiagnosis> {\n const {diagnosis, remediations} = await engine.diagnose({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n return {\n instanceId,\n state: diagnosis.state,\n summary: diagnosisSummary(diagnosis),\n ...(diagnosis.state === 'stuck' ? {cause: diagnosis.cause.kind} : {}),\n remediations,\n }\n}\n"],"names":[],"mappings":";;AAqBA,MAAM,iBAAiB;AAAA,EACrB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,cAAc;AAAA,EACd,QAAQ,CAAC,EAAC,MAAM,WAAW,MAAM,WAAW,OAAO,YAAY,cAAc,EAAC,MAAM,QAAA,GAAS;AAAA,EAC7F,QAAQ;AAAA,IACN;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,SAAS,CAAC,EAAC,MAAM,WAAW,OAAO,WAAW,QAAQ,OAAA,CAAO;AAAA,QAAA;AAAA,MAC/D;AAAA,MAEF,aAAa,CAAC,EAAC,MAAM,eAAe,OAAO,sBAAsB,IAAI,WAAA,CAAW;AAAA,IAAA;AAAA,IAElF,EAAC,MAAM,YAAY,OAAO,YAAY,aAAa,sCAAA;AAAA,EAAgC;AAEvF,GAEM,oBAAoB;AAAA,EACxB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,cAAc;AAAA,EACd,QAAQ,CAAC,EAAC,MAAM,WAAW,MAAM,WAAW,OAAO,YAAY,cAAc,EAAC,MAAM,QAAA,GAAS;AAAA,EAC7F,QAAQ;AAAA,IACN;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,SAAS,CAAC,EAAC,MAAM,UAAU,OAAO,qBAAqB,QAAQ,OAAA,CAAO;AAAA,QAAA;AAAA,MACxE;AAAA,MAEF,aAAa,CAAC,EAAC,MAAM,aAAa,OAAO,kBAAkB,IAAI,SAAA,CAAS;AAAA,IAAA;AAAA,IAE1E;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,YAAY;AAAA,UACZ,SAAS;AAAA,YACP,EAAC,MAAM,WAAW,OAAO,WAAW,QAAQ,OAAA;AAAA,YAC5C,EAAC,MAAM,UAAU,OAAO,UAAU,QAAQ,SAAA;AAAA,UAAQ;AAAA,QACpD;AAAA,MACF;AAAA,MAEF,aAAa;AAAA,QACX,EAAC,MAAM,eAAe,OAAO,sBAAsB,IAAI,WAAA;AAAA;AAAA,QAEvD;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,IAAI;AAAA,UACJ,QAAQ;AAAA,QAAA;AAAA,MACV;AAAA,IACF;AAAA,IAEF,EAAC,MAAM,YAAY,OAAO,YAAY,aAAa,sCAAA;AAAA,EAAgC;AAEvF,GASM,cAAc,k5KA8FP,kBAAkB,GAAG,WAAW;AAAA;AAAA;AAAA;AAAA,uBAI3B,eAAe,KAAK;AAAA;AAAA,EAEpC,KAAK,UAAU,gBAAgB,MAAM,CAAC,CAAC;AAAA;AAAA;AAAA,uBAGvB,kBAAkB,KAAK;AAAA;AAAA,EAEvC,KAAK,UAAU,mBAAmB,MAAM,CAAC,CAAC;AAAA;AAAA,GC9I/B,WAAW,IAMX,mBAAqC;AAAA,EAChD;AAAA,IACE,MAAM;AAAA,IACN,aACE,yoBAUA,OAAO,EAAQ,IACf;AAAA,IAGF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,YAAY;AAAA,UACV,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,QAEJ,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,MAAM,CAAC,aAAa,QAAQ,KAAK;AAAA,UACjC,aACE;AAAA,QAAA;AAAA,MAEJ;AAAA,MAEF,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAaF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,aAAa;AAAA,UACX,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,MACf;AAAA,MAEF,UAAU,CAAC,aAAa;AAAA,MACxB,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAWF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,aAAa;AAAA,UACX,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,MACf;AAAA,MAEF,UAAU,CAAC,aAAa;AAAA,MACxB,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAWF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,aAAa;AAAA,UACX,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,UAAU;AAAA,UACR,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,QAGJ,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,MAEJ;AAAA,MAEF,UAAU,CAAC,eAAe,YAAY,QAAQ;AAAA,MAC9C,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAKF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY,CAAA;AAAA,MACZ,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,aACE;AAAA,IAQF,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,YAAY;AAAA,QACV,YAAY;AAAA,UACV,MAAM;AAAA,UACN,aACE;AAAA,QAAA;AAAA,MAEJ;AAAA,MAEF,UAAU,CAAC,YAAY;AAAA,MACvB,sBAAsB;AAAA,IAAA;AAAA,EACxB;AAEJ,GCrMM,qBAAqB;AAwB3B,SAAS,cAAc,UAAgD;AACrE,QAAM,QAAQ,SAAS,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,aAAa,EAAE,SAAS,kBAAkB;AAChG,MAAI,EAAA,UAAU,UAAa,MAAM,UAAU;AAC3C,WAAO,MAAM,OAAO;AACtB;AAYA,eAAsB,mBACpB,QACA,WAC8B;AAI9B,QAAM,iCAAiB,IAAA;AACvB,aAAW,QAAQ,WAAW;AAC5B,UAAM,MAAM,cAAc,IAAI;AAC9B,QAAI,QAAQ;AACZ,UAAI;AACF,mBAAW,IAAI,kBAAkB,GAAG,GAAG,GAAG;AAAA,MAC5C,QAAQ;AAAA,MAER;AAAA,EACF;AACA,MAAI,WAAW,SAAS,EAAG,4BAAW,IAAA;AAOtC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,OAAO,OAAO;AAAA,MACzB;AAAA,MACA,EAAC,QAAQ,MAAM,KAAK,WAAW,KAAA,CAAM,EAAA;AAAA,IAAC;AAAA,EAE1C,SAAS,KAAK;AAGZ,WAAA,QAAQ,OAAO;AAAA,MACb,6EACE,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA;AAAA,IAAA,uBAES,IAAA;AAAA,EACb;AAEA,QAAM,0BAAU,IAAA;AAChB,aAAW,OAAO,MAAM;AACtB,UAAM,MAAM,WAAW,IAAI,IAAI,GAAG;AAC9B,YAAQ,UAAa,OAAO,IAAI,SAAU,YAAY,IAAI,UAAU,MACtE,IAAI,IAAI,KAAK,IAAI,KAAK;AAAA,EAE1B;AACA,SAAO;AACT;AAQA,SAAS,aACP,UACA,eAC8B;AAC9B,QAAM,MAAM,cAAc,QAAQ;AAClC,MAAI,QAAQ,OAAW;AACvB,QAAM,QAAQ,cAAc,IAAI,GAAG;AACnC,SAAO,UAAU,SAAY,EAAC,KAAK,KAAK,MAAA,IAAS,EAAC,KAAK,IAAA;AACzD;AAEO,SAAS,eACd,KACA,eAC0B;AAC1B,QAAM,aAAa,KAAK,MAAM,IAAI,kBAAkB,GAE9C,aADQ,WAAW,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,YAAY,GAC7C,OACpB,gBAAgB,WAAW,OAC3B,UAAU,aAAa,KAAK,aAAa;AAC/C,SAAO;AAAA,IACL,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,GAAI,kBAAkB,SAAY,EAAC,cAAA,IAAiB,CAAA;AAAA,IACpD,cAAc,IAAI;AAAA,IAClB,GAAI,eAAe,SAAY,EAAC,mBAAmB,WAAA,IAAc,CAAA;AAAA,IACjE,eAAe,IAAI;AAAA,IACnB,MAAM,eAAe,GAAG;AAAA,IACxB,GAAI,YAAY,SAAY,EAAC,YAAW,CAAA;AAAA,EAAC;AAE7C;AAEO,SAAS,aACd,UACA,YACA,eACwB;AAKxB,QAAM,gBADa,KAAK,MAAM,SAAS,kBAAkB,EACxB,OAC3B,aAAa,WAAW,aAAa,MAAM,OAE3C,aAAkC,WAAW,aAAa,WAAW,IAAI,CAAC,OAAO;AAIrF,UAAM,UAAoC,GAAG,QAAQ,IAAI,CAAC,OAAO;AAC/D,YAAM,UAAU,cAAc,IAAI,EAAE;AACpC,aAAO;AAAA,QACL,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,SAAY,EAAC,OAAO,QAAQ,MAAA,IAAS,CAAA;AAAA,QAC3D,SAAS,QAAQ;AAAA,QACjB,GAAI,QAAQ,YAAY,MAAS,QAAQ,mBAAmB,SACxD,EAAC,gBAAgB,qBAAqB,QAAQ,cAAc,EAAA,IAC5D,CAAA;AAAA,MAAC;AAAA,IAET,CAAC;AAED,WAAO;AAAA,MACL,UAAU,GAAG,SAAS;AAAA,MACtB,GAAI,GAAG,SAAS,UAAU,SAAY,EAAC,OAAO,GAAG,SAAS,MAAA,IAAS,CAAA;AAAA,MACnE,GAAI,GAAG,SAAS,gBAAgB,SAAY,EAAC,aAAa,GAAG,SAAS,YAAA,IAAe,CAAA;AAAA,MACrF,QAAQ,GAAG;AAAA,MACX;AAAA,IAAA;AAAA,EAEJ,CAAC,GAEK,gBAAyC,SAAS,QACrD,MAAM,EAAY,EAClB,WAAA,EACA,IAAI,CAAC,WAAW;AAAA,IACf,IAAI,MAAM;AAAA,IACV,MAAM,MAAM;AAAA,IACZ,SAAS,sBAAsB,KAAK;AAAA,EAAA,EACpC,GAEE,UAAU,aAAa,UAAU,aAAa;AACpD,SAAO;AAAA,IACL,YAAY,SAAS;AAAA,IACrB,YAAY,SAAS;AAAA,IACrB,GAAI,kBAAkB,SAAY,EAAC,cAAA,IAAiB,CAAA;AAAA,IACpD,cAAc,SAAS;AAAA,IACvB,GAAI,eAAe,SAAY,EAAC,mBAAmB,WAAA,IAAc,CAAA;AAAA,IACjE,MAAM,eAAe,QAAQ;AAAA,IAC7B,GAAI,YAAY,SAAY,EAAC,QAAA,IAAW,CAAA;AAAA,IACxC;AAAA,IACA;AAAA,EAAA;AAEJ;AAEO,SAAS,eAAe,UAAqC;AAIlE,MAAI,SAAS,gBAAgB,OAAW,QAAO;AAC/C,MAAI;AAEF,UAAM,QADa,KAAK,MAAM,SAAS,kBAAkB,EAChC,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,YAAY;AAC5E,WAAO,UAAU,UAAa,gBAAgB,KAAK;AAAA,EACrD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,MAAM,uBAA+C;AAAA,EACnD,iBAAiB;AAAA,EACjB,uBAAuB;AAAA,EACvB,kBAAkB;AAAA,EAClB,sBAAsB;AAAA,EACtB,sBAAsB;AACxB;AAEA,SAAS,qBAAqB,QAAgC;AAG5D,SAAO,qBAAqB,OAAO,IAAI,KAAK,OAAO,KAAK,QAAQ,MAAM,GAAG;AAC3E;AAEA,SAAS,sBAAsB,OAA0D;AACvF,UAAQ,MAAM,OAAA;AAAA,IACZ,KAAK;AACH,aAAO,kBAAkB,OAAO,MAAM,KAAK,CAAC;AAAA,IAC9C,KAAK;AACH,aAAO,iBAAiB,OAAO,MAAM,KAAK,CAAC,aAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IAC1E,KAAK;AACH,aAAO,aAAa,OAAO,MAAM,QAAQ,CAAC;AAAA,IAC5C,KAAK;AACH,aAAO,aAAa,OAAO,MAAM,QAAQ,CAAC,aAAa,OAAO,MAAM,IAAI,CAAC,WAAM,OAAO,MAAM,EAAE,CAAC;AAAA,IACjG,KAAK;AACH,aAAO,WAAW,OAAO,MAAM,MAAM,CAAC,wBAAwB,OAAO,MAAM,QAAQ,CAAC;AAAA,IACtF,KAAK;AACH,aAAO,sBAAsB,OAAO,MAAM,SAAS,CAAC,aAAQ,OAAO,MAAM,OAAO,CAAC;AAAA,IACnF,KAAK;AACH,aAAO,WAAW,OAAO,MAAM,MAAM,CAAC;AAAA,IACxC,KAAK;AACH,aAAO,WAAW,OAAO,MAAM,MAAM,CAAC,gBAAgB,OAAO,MAAM,MAAM,CAAC;AAAA,IAC5E,KAAK;AACH,aAAO,yCAAyC,OAAO,MAAM,QAAQ,CAAC;AAAA,IACxE;AACE,aAAO,MAAM;AAAA,EAAA;AAEnB;AAUO,SAAS,iBAAiB,WAA8B;AAC7D,UAAQ,UAAU,OAAA;AAAA,IAChB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,mCAAmC,UAAU,QAAQ,MAAM,UAAU,QAAQ,KAAK,MAAM,CAAC;AAAA,IAClG,KAAK;AACH,aAAO,aAAa,UAAU,QAAQ,oEAA+D,UAAU,aAAa,KAAK,IAAI,CAAC;AAAA,IACxI,KAAK;AACH,aAAO,gBAAgB,UAAU,EAAE;AAAA,IACrC,KAAK;AACH,aAAO,UAAU,WAAW,SACxB,cAAc,UAAU,EAAE,KAAK,UAAU,MAAM,MAC/C,cAAc,UAAU,EAAE;AAAA,IAChC,KAAK;AACH,aAAO,aAAa,UAAU,KAAK;AAAA,EAAA;AAEzC;AAEA,SAAS,aAAa,OAA2B;AAC/C,UAAQ,MAAM,MAAA;AAAA,IACZ,KAAK;AACH,aAAO,2BAA2B,MAAM,OAAO,IAAI,2BAA2B,MAAM,OAAO,OAAO,IAAI;AAAA,IACxG,KAAK;AACH,aAAO,kBAAkB,MAAM,OAAO,IAAI;AAAA,IAC5C,KAAK;AACH,aAAO,oBAAoB,MAAM,QAAQ;AAAA,IAC3C,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,2CAA2C,MAAM,YAAY,KAAK,IAAI,CAAC;AAAA,EAAA;AAEpF;ACrQO,SAAS,WAAW,QAAgB,UAA6B,IAAgB;AACtF,QAAM,iBAAiB,QAAQ;AA8C/B,SAAO,EAAC,aAAa,kBAAkB,OA5CC;AAAA,IACtC,yBAAyB,OAAO,aAAa;AAC3C,YAAM,QAAQ,eAAe,QAAQ;AACrC,aAAO,cAAc,QAAQ,KAAK;AAAA,IACpC;AAAA,IACA,oBAAoB,OAAO,aAAa;AACtC,YAAM,QAAQ,qBAAqB,UAAU,oBAAoB;AACjE,aAAO,iBAAiB,QAAQ,MAAM,aAAa,cAAc;AAAA,IACnE;AAAA,IACA,mBAAmB,OAAO,aAAa;AACrC,YAAM,QAAQ,qBAAqB,UAAU,mBAAmB;AAChE,aAAO,qBAAqB,QAAQ,MAAM,aAAa,cAAc;AAAA,IACvE;AAAA,IACA,aAAa,OAAO,aAAa;AAC/B,YAAM,QAAQ,qBAAqB,QAAQ;AAC3C,aAAA,MAAM,OAAO,WAAW;AAAA,QACtB,YAAY,MAAM;AAAA,QAClB,UAAU,MAAM;AAAA,QAChB,QAAQ,MAAM;AAAA,QACd,GAAI,mBAAmB,SAAY,EAAC,QAAQ,eAAA,IAAkB,CAAA;AAAA,MAAC,CAChE,GAGM,iBAAiB,QAAQ,MAAM,aAAa,cAAc;AAAA,IACnE;AAAA;AAAA;AAAA,IAGA,8BAA8B,YAAY;AAAA,IAC1C,8BAA8B,OAAO,aAAgD;AACnF,YAAM,EAAC,WAAA,IAAc,mBAAmB,QAAQ;AAChD,UAAI;AAKF,cAAM,SAAS,eAAe,UAA+B;AAC7D,eAAA,mBAAmB,MAAM,GAClB,EAAC,OAAO,IAAM,YAAY,OAAA;AAAA,MACnC,SAAS,KAAK;AACZ,eAAO,EAAC,OAAO,IAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAA;AAAA,MAC9E;AAAA,IACF;AAAA,EAAA,EACF;AAGF;AAcA,SAAS,eAAe,KAAyB;AAC/C,MAAI,QAAQ,QAAQ,OAAO,OAAQ;AACjC,WAAO,EAAC,QAAQ,YAAA;AAElB,QAAM,MAAM,KACN,SAAS,IAAI,WAAW,UAAU,IAAI,WAAW,QAAQ,IAAI,SAAU,aACvE,aAAa,OAAO,IAAI,cAAe,WAAW,IAAI,aAAa;AACzE,SAAO,eAAe,SAAY,EAAC,YAAY,OAAA,IAAU,EAAC,OAAA;AAC5D;AAIA,SAAS,mBAAmB,KAAoC;AAC9D,MAAI,QAAQ,QAAQ,OAAO,OAAQ;AACjC,UAAM,IAAI,MAAM,uDAAuD;AAEzE,QAAM,MAAM;AACZ,MAAI,IAAI,eAAe,QAAQ,OAAO,IAAI,cAAe;AACvD,UAAM,IAAI,MAAM,8EAA8E;AAEhG,SAAO,EAAC,YAAY,IAAI,WAAA;AAC1B;AAIA,SAAS,qBAAqB,KAAc,MAAqC;AAC/E,MAAI,QAAQ,QAAQ,OAAO,OAAQ;AACjC,UAAM,IAAI,MAAM,GAAG,IAAI,2BAA2B;AAEpD,QAAM,MAAM;AACZ,MAAI,OAAO,IAAI,eAAgB,YAAY,IAAI,gBAAgB;AAC7D,UAAM,IAAI,MAAM,GAAG,IAAI,2BAA2B;AAEpD,SAAO,EAAC,aAAa,IAAI,YAAA;AAC3B;AAEA,SAAS,qBAAqB,KAI5B;AACA,MAAI,QAAQ,QAAQ,OAAO,OAAQ;AACjC,UAAM,IAAI,MAAM,sCAAsC;AAExD,QAAM,MAAM;AACZ,aAAW,OAAO,CAAC,eAAe,YAAY,QAAQ;AACpD,QAAI,OAAO,IAAI,GAAG,KAAM,YAAY,IAAI,GAAG,MAAM;AAC/C,YAAM,IAAI,MAAM,gBAAgB,GAAG,cAAc;AAGrD,SAAO;AAAA,IACL,aAAa,IAAI;AAAA,IACjB,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,EAAA;AAEhB;AAIA,eAAe,cACb,QACA,OACkD;AAGlD,QAAM,YAAY,CAAC,aAAa,sBAAsB,GAAG;AACrD,QAAM,eAAe,UACvB,UAAU,KAAK,2BAA2B;AAE5C,QAAM,OACJ,KAAK,UAAU,KAAK,MAAM,CAAC,OAAO,eAAA,CAAgB,qCACf,QAAQ,KAEvC,SAAkC,CAAA;AACpC,QAAM,eAAe,WAAW,OAAO,aAAa,MAAM;AAG9D,QAAM,YADO,MAAM,OAAO,MAA0B,EAAC,MAAM,QAAO,GAC5C,OAAO,CAAC,QAAQ;AACpC,UAAM,OAAO,eAAe,GAAG;AAC/B,WAAI,MAAM,WAAW,SAAe,OAChC,MAAM,WAAW,cAAoB,CAAC,OACnC;AAAA,EACT,CAAC,GACK,gBAAgB,MAAM,mBAAmB,QAAQ,QAAQ;AAC/D,SAAO;AAAA,IACL,WAAW,SAAS,IAAI,CAAC,QAAQ,eAAe,KAAK,aAAa,CAAC;AAAA,EAAA;AAEvE;AAEA,eAAe,iBACb,QACA,YACA,gBACiC;AACjC,QAAM,aAAa,MAAM,OAAO,iBAAiB;AAAA,IAC/C;AAAA,IACA,GAAI,mBAAmB,SAAY,EAAC,QAAQ,eAAA,IAAkB,CAAA;AAAA,EAAC,CAChE,GACK,WAAW,MAAM,OAAO,YAAY,EAAC,WAAA,CAAW,GAChD,gBAAgB,MAAM,mBAAmB,QAAQ,CAAC,QAAQ,CAAC;AACjE,SAAO,aAAa,UAAU,YAAY,aAAa;AACzD;AAEA,eAAe,qBACb,QACA,YACA,gBAC6B;AAC7B,QAAM,EAAC,WAAW,aAAA,IAAgB,MAAM,OAAO,SAAS;AAAA,IACtD;AAAA,IACA,GAAI,mBAAmB,SAAY,EAAC,QAAQ,eAAA,IAAkB,CAAA;AAAA,EAAC,CAChE;AACD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,UAAU;AAAA,IACjB,SAAS,iBAAiB,SAAS;AAAA,IACnC,GAAI,UAAU,UAAU,UAAU,EAAC,OAAO,UAAU,MAAM,KAAA,IAAQ,CAAA;AAAA,IAClE;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"index.js","sources":["../src/authoring-guide.ts","../src/descriptors.ts","../src/projection.ts","../src/tools.ts"],"sourcesContent":["/**\n * Content for the `get_workflow_authoring_guide` tool.\n *\n * The agent calling the MCP server generates the workflow definition; this\n * guide is what teaches it the DSL well enough to do that on the first pass.\n * It is the orientation the examples can't convey on their own (the GROQ\n * built-ins, the sugars, the hard rules) plus two worked examples.\n *\n * The examples are kept inline (rather than imported from\n * `@sanity/workflow-examples`) on purpose: that package exports the *desugared*\n * `WorkflowDefinition` (the post-`defineWorkflow` shape), but an agent should emit\n * the *authoring* (sugar) shape — JSON, exactly what `validate_workflow_definition`\n * accepts — so importing the examples would show the wrong shape. Keeping them\n * inline also avoids promoting `@sanity/workflow-examples` to a runtime dependency\n * of a package headed for host embedding. The companion test runs every example\n * through the real validators, so a stale snippet fails CI rather than misleading\n * an agent.\n */\n\nimport type {AuthoringWorkflow} from '@sanity/workflow-engine'\n\nconst minimalExample = {\n name: 'quick-approval',\n title: 'Quick approval',\n description: 'One review stage with a single approve action, then a terminal stage.',\n initialStage: 'review',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'review',\n title: 'Review',\n activities: [\n {\n name: 'decide',\n title: 'Decide',\n activation: 'auto',\n actions: [{name: 'approve', title: 'Approve', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-approved', title: 'Approve and finish', to: 'approved'}],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\nconst reviewLoopExample = {\n name: 'doc-review',\n title: 'Document review',\n description: 'Draft, then editorial review that approves or rejects back to drafting.',\n initialStage: 'drafting',\n fields: [{type: 'doc.ref', name: 'subject', title: 'Document', initialValue: {type: 'input'}}],\n stages: [\n {\n name: 'drafting',\n title: 'Drafting',\n activities: [\n {\n name: 'write',\n title: 'Write the draft',\n activation: 'auto',\n actions: [{name: 'submit', title: 'Submit for review', status: 'done'}],\n },\n ],\n transitions: [{name: 'to-review', title: 'Send to review', to: 'review'}],\n },\n {\n name: 'review',\n title: 'Editorial review',\n activities: [\n {\n name: 'review',\n title: 'Review the draft',\n activation: 'auto',\n actions: [\n {name: 'approve', title: 'Approve', status: 'done'},\n {name: 'reject', title: 'Reject', status: 'failed'},\n ],\n },\n ],\n transitions: [\n {name: 'to-approved', title: 'Approve and finish', to: 'approved'},\n // Reject flips the activity to `failed`; this routes the instance back.\n {\n name: 'to-drafting',\n title: 'Send back to drafting',\n to: 'drafting',\n filter: '$anyActivityFailed',\n },\n ],\n },\n {name: 'approved', title: 'Approved', description: 'Terminal — no transitions out.'},\n ],\n} satisfies AuthoringWorkflow\n\n/**\n * Worked examples included verbatim in {@link AUTHORING_GUIDE}, and exercised by\n * the drift-guard test. Authoring (sugar) shape — what an agent passes to\n * `validate_workflow_definition`.\n */\nexport const GUIDE_EXAMPLES: AuthoringWorkflow[] = [minimalExample, reviewLoopExample]\n\nconst ORIENTATION = `# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it — every\ncondition (filters, predicates, guards) is a GROQ *string*. Generate the JSON,\nthen call \\`validate_workflow_definition\\` to check it; fix the reported errors\nand re-validate until it returns \\`valid: true\\`. The validator returns the\n*desugared* definition under \\`definition\\` — that is exactly what would deploy.\n\n## Shape\n\n- **Workflow**: \\`{ name, title, description?, initialStage, fields?, stages[], predicates? }\\`.\n \\`initialStage\\` must be the \\`name\\` of a declared stage. Do NOT include a\n \\`version\\` — definitions are immutable and content-addressed; deploy assigns\n the version from the content, the author never writes one.\n- **Stage**: \\`{ name, title?, description?, activities?, transitions? }\\`. A stage with\n no transitions is terminal. Reaching a terminal stage ends the workflow.\n- **Activity**: \\`{ name, title?, activation?, actions? }\\`. \\`activation\\` is \\`'auto'\\`\n (starts when the stage is entered) or \\`'manual'\\` (default — a human starts it).\n- **Action**: \\`{ name, title?, status?, params?, ops? }\\`.\n \\`status: 'done' | 'skipped' | 'failed'\\` is sugar that flips the *firing activity* to\n that status when the action fires. \\`ops\\` are mutations applied when the action\n fires (e.g. \\`{type:'field.set', target:{field:'x'}, value:{type:'param', param:'p'}}\\`).\n \\`params\\` are values the action collects from the caller (referenced by\n \\`{type:'param', param:'<name>'}\\` sources).\n- **Transition**: \\`{ name, title?, to, filter? }\\`. \\`to\\` must name a declared\n stage. \\`filter\\` is a GROQ condition gating the exit; omit it and it defaults\n to \\`$allActivitiesDone\\`.\n- **Field** (workflow- or stage-scoped persistent state): \\`{ type, name, title?, initialValue? }\\`.\n Scalar \\`type\\`s mirror Sanity: \\`string\\`, \\`text\\` (multiline), \\`number\\`,\n \\`boolean\\`, \\`date\\` (YYYY-MM-DD), \\`datetime\\` (ISO), \\`url\\`. Plus references\n (\\`doc.ref\\`, \\`doc.refs\\`, \\`release.ref\\`), identities (\\`actor\\` — one concrete\n principal; \\`assignee\\` / \\`assignees\\` — one or many user-or-role assignees), and\n the compositional kinds \\`object\\` (\\`{ type:'object', name, fields: [...] }\\`) and\n \\`array\\` (\\`{ type:'array', name, of: [...] }\\`) — \\`fields\\`/\\`of\\` are themselves\n field shapes, so any structure composes. \\`initialValue\\` seeds the slot once at\n materialisation and is **optional** — omit it for op-filled working memory\n (the common default). Arms: \\`{type:'input'}\\` (the caller supplies it when the\n instance starts), \\`{type:'query', query:'<groq>'}\\` (computed from the lake),\n \\`{type:'literal', value:<json>}\\`, or \\`{type:'fieldRead', field:'<name>'}\\`.\n By convention a workflow has an \\`input\\`-sourced \\`doc.ref\\` named \\`subject\\` — the\n headline document it is about.\n Two list sugars desugar to \\`array\\`: \\`{type:'todoList', name}\\` (ad-hoc\n status-tracked work — rows \\`{ label, status, assignee?, dueDate? }\\`) and\n \\`{type:'notes', name}\\` (an append-only audit/comment log — rows\n \\`{ body, actor, at }\\`; pairs with the \\`audit\\` op, which stamps \\`actor\\`/\\`at\\`).\n\n## GROQ in conditions\n\nBuilt-in variables available in filters/predicates: \\`$allActivitiesDone\\`,\n\\`$anyActivityFailed\\` (booleans over the current stage's activities), \\`$activities\\` (the activity\nlist), \\`$fields\\` (field values), \\`$now\\`, and the caller-scoped vars \\`$actor\\`\n(the acting user), \\`$assigned\\` (whether the caller is the activity's assignee — the\nidiomatic permission gate, used as \\`filter: '$assigned'\\`), and \\`$can\\`. Define\nreusable named conditions under top-level \\`predicates: { name: '<groq>' }\\` and\nreference them as \\`$name\\` (e.g.\n\\`predicates: { ready: \"count($activities[status != 'done']) == 0\" }\\` → \\`$ready\\`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) — **never** scan by \\`_type\\` (e.g. \\`*[_type==\"article\"]\\`);\nthat is a discovery query and the validator rejects it. To bring a document into\nscope, declare a \\`doc.ref\\` field for it.\n\n## Sugars worth knowing\n\n- Action \\`status: 'done' | 'skipped' | 'failed'\\` — flips the firing activity (shown above).\n- Omitted transition \\`filter\\` — defaults to \\`$allActivitiesDone\\`.\n- Omitted activity \\`activation\\` — defaults to \\`'manual'\\`.\n\n## Modeling defaults\n\nValid is not the same as good. Prefer these unless the request says otherwise:\n\n- **A decline/reject loops back.** Route a rejected / changes-requested\n transition to an *earlier* stage for revision (e.g. \\`review → drafting\\` gated\n on \\`$anyActivityFailed\\`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment — a workflow should not\n dead-end just because something was declined.\n- **Prefer draft → review.** Model an author working in a drafting stage who\n submits, then a review stage that gates. Don't add more review stages unless\n the request asks for multiple approvers or rounds.\n- **When the shape is ambiguous, pick the conventional one and confirm** with the\n user rather than inventing extra stages.\n\n## Rules the validator enforces\n\n- Stage names, activity names (per stage) and transition names are unique.\n- Every transition \\`to\\` and \\`initialStage\\` names a declared stage.\n- Custom \\`predicates\\` must not shadow a built-in (e.g. \\`allActivitiesDone\\`).\n- Every GROQ string must parse and must not be a \\`_type\\` discovery scan.`\n\n/**\n * The full text returned by `get_workflow_authoring_guide`: orientation plus the\n * two worked examples rendered as JSON.\n */\nexport const AUTHORING_GUIDE = `${ORIENTATION}\n\n## Examples\n\n### Example 1 — ${minimalExample.title} (minimal: one stage, one action)\n\\`\\`\\`json\n${JSON.stringify(minimalExample, null, 2)}\n\\`\\`\\`\n\n### Example 2 — ${reviewLoopExample.title} (review loop: reject routes back)\n\\`\\`\\`json\n${JSON.stringify(reviewLoopExample, null, 2)}\n\\`\\`\\`\n`\n","/**\n * The MCP tool surface for workflow operations — the static descriptors an\n * agent reads to decide what to call. The implementations live in `tools.ts`;\n * these are pure data (descriptions + JSON schemas) and safe to cache.\n *\n * Two jobs: operate running instances, and author new definitions.\n *\n * Operate an instance:\n *\n * - `list_workflow_instances` is the only way to *discover* what's\n * running without already knowing an id. Filterable, capped.\n * - `get_workflow_state` is the \"tell me everything actionable about\n * one instance\" call. It composes the engine's `evaluate` projection\n * into a flat shape an LLM can reason over without piecing together\n * raw `WorkflowInstance` documents.\n * - `diagnose_workflow` answers \"*why* isn't this moving?\" — it\n * classifies an instance as healthy (waiting/progressing) or genuinely\n * stuck (a failed effect/activity, a dead-end transition) and names the\n * remediations that would unstick it. A pure read; the remediations are\n * advisory (none execute through this server).\n * - `fire_action` is the only write. Mirrors the engine's universal\n * \"something happened\" entry point — everything other than admin\n * override flows through it.\n *\n * Author a definition (both pure, engine-independent, neither deploys):\n *\n * - `get_workflow_authoring_guide` returns the DSL guide an agent reads\n * before writing a definition — shape, GROQ built-ins, sugars, examples.\n * - `validate_workflow_definition` runs the deploy-time checks (structure +\n * invariants + GROQ) and returns the desugared definition or a\n * path-prefixed error list. A human deploys it via the CLI.\n *\n * Surface choices that matter for an LLM consumer:\n *\n * - Descriptions are written for an agent, not a human reader.\n * Each one says (a) what the tool does, (b) when to use it,\n * (c) what *not* to use it for.\n * - Parameter names use snake_case, matching MCP convention.\n * - The write tool's description spells out that this is the way\n * to advance workflow state — not a side door. Without that line,\n * an LLM tends to look for a `complete_activity` or `set_stage` tool.\n * - Read tools cap their output. A 50-page `recent_history` would\n * burn context and is rarely informative; 8 is enough to spot\n * loops or stuck-in-stage patterns.\n */\n\n/**\n * The descriptor shape the Anthropic Messages API + the MCP SDK both\n * accept. Kept here as a small, transport-neutral interface so we can\n * feed the same descriptors to either consumer.\n */\nexport interface ToolDescriptor {\n name: string\n description: string\n /** JSON Schema describing the tool's parameters. */\n input_schema: {\n type: 'object'\n properties: Record<string, unknown>\n required?: string[]\n additionalProperties?: boolean\n }\n}\n\n/** Maximum instances returned by list_workflow_instances. */\nexport const LIST_CAP = 25\n\n/**\n * The static tool descriptors. Independent of any engine — the bound\n * implementations that close over an engine live in {@link buildTools}.\n */\nexport const TOOL_DESCRIPTORS: ToolDescriptor[] = [\n {\n name: 'list_workflow_instances',\n description:\n 'List workflow instances in the configured workspace. Use this when you need ' +\n \"to find a workflow but don't already know its instance id, or to survey \" +\n \"what's in flight. Returns a compact summary — id, workflow type id and \" +\n 'human-readable `workflowTitle`, current stage, whether the instance is done, ' +\n 'and (when the workflow has a conventional subject document) a `subject` ' +\n \"field with the subject doc's ref and title. Use `workflowTitle` when the \" +\n 'user names the workflow by type (e.g. \"article reviews\") and `subject.title` ' +\n \"when they name a specific in-flight instance by what it's about (e.g. \" +\n '\"the article-review about pricing\"). ' +\n 'Capped at ' +\n String(LIST_CAP) +\n ' results. ' +\n 'Do NOT use this to inspect a single known instance — use get_workflow_state ' +\n 'for that, the response will be richer.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'string',\n description:\n \"Optional. Restrict to instances of this workflow definition (e.g. 'article-review').\",\n },\n status: {\n type: 'string',\n enum: ['in_flight', 'done', 'any'],\n description:\n \"Optional. 'in_flight' returns instances not in a terminal stage; \" +\n \"'done' returns completed/aborted; 'any' returns both. Defaults to 'in_flight'.\",\n },\n },\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_state',\n description:\n 'Get the current state of a single workflow instance, projected for action. ' +\n \"Returns the workflow's id and human-readable `workflowTitle`, the current \" +\n 'stage, the subject document the workflow is about (when the workflow has a ' +\n 'conventional subject — its ref and title), every activity on the current stage ' +\n 'with its available actions (and whether each action is currently allowed), ' +\n 'and the most recent history entries. Use this whenever you need to understand ' +\n \"what's possible on an instance before deciding to act. \" +\n 'This is a pure read — it does not change anything. ' +\n 'If you only need to discover what instances exist, use list_workflow_instances ' +\n 'instead; this tool requires you to know the instance id. ' +\n 'list_workflow_instances also returns the same `workflowTitle` and `subject` ' +\n 'fields, so prefer it for fan-out discovery rather than polling ' +\n 'get_workflow_state per instance.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'diagnose_workflow',\n description:\n \"Explain why a single workflow instance is or isn't progressing. Returns a \" +\n 'verdict (`state`: progressing, waiting, blocked, completed, aborted, or ' +\n 'stuck), a one-line `summary`, and — when stuck — a structured `cause` plus ' +\n 'the `remediations` that would unstick it. Use this when an instance seems ' +\n 'stalled or the user asks \"why isn\\'t this moving?\": it distinguishes a healthy ' +\n 'instance (waiting on a human, or will advance on its own) from a genuinely ' +\n 'stuck one (a failed effect or activity, a dead-end transition). ' +\n 'This is a pure read — it changes nothing, and the remediations it names are ' +\n 'advisory: none can be executed through this server. To actually advance a ' +\n 'healthy waiting instance use fire_action; for per-activity action detail use ' +\n 'get_workflow_state.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n },\n required: ['instance_id'],\n additionalProperties: false,\n },\n },\n {\n name: 'fire_action',\n description:\n 'Advance a workflow instance by firing an action on one of its activities. ' +\n 'This is the only way to advance workflow state from the outside — there is no ' +\n \"separate 'complete activity' or 'transition stage' tool. To find the right \" +\n '(activity, action) pair, call get_workflow_state first and pick from the ' +\n \"allowed actions listed on the current stage's activities. \" +\n 'After firing, the engine cascades any auto-transitions that become eligible ' +\n \"(so an 'approve' action on a review activity may transition the workflow to a \" +\n 'terminal stage in one shot). Returns the resulting state, same shape as ' +\n 'get_workflow_state. ' +\n 'If the action is not currently allowed (e.g. the activity is already done, or a ' +\n 'guard fails), this returns an error describing why.',\n input_schema: {\n type: 'object',\n properties: {\n instance_id: {\n type: 'string',\n description: 'The workflow instance id.',\n },\n activity: {\n type: 'string',\n description:\n 'The id of the activity on the current stage. Must be one of the activities ' +\n 'returned by get_workflow_state.',\n },\n action: {\n type: 'string',\n description:\n 'The id of the action on that activity. Must be one of the actions listed ' +\n 'as allowed=true on the activity.',\n },\n params: {\n type: 'object',\n description:\n \"Optional. Values for the action's declared params, keyed by param name \" +\n '(e.g. {\"note\": \"Unsupported claim in paragraph 3.\"}). Required when the ' +\n \"action declares a required param — get_workflow_state lists each action's \" +\n 'params and whether they are required. The shape is per-action, so this is a ' +\n 'free-form object; the engine validates the supplied values against the ' +\n \"action's declared params and rejects the call if a required one is missing.\",\n additionalProperties: true,\n },\n },\n required: ['instance_id', 'activity', 'action'],\n additionalProperties: false,\n },\n },\n {\n name: 'get_workflow_authoring_guide',\n description:\n 'Get the guide for authoring a workflow definition: the DSL shape, the ' +\n 'GROQ condition built-ins, the sugars, the rules the validator enforces, ' +\n 'and two worked JSON examples. Call this BEFORE writing a definition from ' +\n 'a description, then generate the JSON and check it with ' +\n 'validate_workflow_definition. Pure read; takes no arguments.',\n input_schema: {\n type: 'object',\n properties: {},\n additionalProperties: false,\n },\n },\n {\n name: 'validate_workflow_definition',\n description:\n 'Validate a workflow definition you have authored. Runs the same checks as ' +\n 'deploy — structural shape, cross-field invariants (e.g. every transition ' +\n 'target is a declared stage), and GROQ syntax — without writing anything. ' +\n 'Returns `{valid:true, definition}` where `definition` is the desugared form ' +\n 'that would deploy, or `{valid:false, error}` with every problem listed and ' +\n 'path-prefixed. This does NOT deploy — a human deploys the validated ' +\n 'definition via the CLI. Call get_workflow_authoring_guide first for the ' +\n 'shape; on `valid:false`, fix the reported problems and validate again.',\n input_schema: {\n type: 'object',\n properties: {\n definition: {\n type: 'object',\n description:\n 'The workflow definition to validate, as a JSON object in authoring ' +\n 'shape. See get_workflow_authoring_guide for the shape and examples.',\n },\n },\n required: ['definition'],\n additionalProperties: false,\n },\n },\n]\n","/**\n * Projection helpers — engine output shaped for an LLM consumer.\n *\n * Why a hand-written projection rather than returning the engine's\n * `WorkflowEvaluation` raw: the LLM only needs (a) which activities exist,\n * (b) which actions are allowed on each, (c) why an action is denied\n * when it is. The evaluation includes per-stage field-entry resolution\n * and snapshot bookkeeping that bloats the payload without helping\n * the model pick the right next move.\n *\n * The engine reads that feed these live in `tools.ts`; this module is\n * pure shaping plus the one batched subject-title lookup they all share.\n */\n\nimport {\n actionVerdict,\n extractDocumentId,\n isTerminalStage,\n type Diagnosis,\n type DisabledReason,\n type Engine,\n type StuckCause,\n type WorkflowDefinition,\n type WorkflowEvaluation,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\n\nimport type {\n ProjectedActionVerdict,\n ProjectedHistoryEntry,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ProjectedSubject,\n ProjectedActivity,\n} from './types.ts'\n\n/**\n * The field-entry name we treat as the workflow's \"subject\" — the headline\n * doc the workflow is about. Convention-only (see ProjectedSubject docs);\n * a workflow without an entry with this name won't have `subject` populated\n * on the projection.\n */\nconst SUBJECT_ENTRY_NAME = 'subject'\n\n/** Maximum history entries returned in get_workflow_state. */\nconst HISTORY_CAP = 8\n\n// Subject doc resolution\n//\n// The conventional \"subject\" entry holds a doc.ref to the thing the\n// workflow is about. The entry value is a GDR URI (e.g.\n// `dataset:proj:ds:article-1`) — what `_id` in the store actually\n// uses is the bare `article-1` part. Extract, batch-fetch by id list,\n// build a Map keyed by GDR URI.\n//\n// Known gap: cross-resource subject docs. If the subject lives in a\n// different Sanity resource than the engine's own workflowResource,\n// this lookup misses — the engine's `query` runs against the workflow\n// resource's client. Production-shape fix is to thread the engine's\n// `resourceClients` resolver through here; out of scope for v0.1.\n\n/**\n * Look up the conventional subject entry on a workflow instance's\n * workflow-scope fields. Returns the GDR URI when present, undefined\n * when the workflow has no subject entry or the entry is null.\n */\nfunction getSubjectGdr(instance: WorkflowInstance): string | undefined {\n const entry = instance.fields.find((s) => s._type === 'doc.ref' && s.name === SUBJECT_ENTRY_NAME)\n if (entry === undefined || entry._type !== 'doc.ref') return undefined\n return entry.value?.id\n}\n\n/**\n * Batch-fetch the `title` field for every subject doc referenced by\n * the supplied instances. Returns a `Map<gdrUri, title>` — missing\n * keys mean the doc had no title (or didn't exist / was unreadable).\n * The caller projects `ref` from the GDR either way; this map only\n * controls whether `title` is populated.\n *\n * Single GROQ call regardless of how many instances are supplied. At\n * `LIST_CAP=25` this is cheap; per-instance lookups would be N+1.\n */\nexport async function fetchSubjectTitles(\n engine: Engine,\n instances: WorkflowInstance[],\n): Promise<Map<string, string>> {\n // Build docId → gdrUri so we can attribute results back. Skip any\n // malformed GDRs silently — the projection still surfaces `ref`,\n // the LLM falls back to that as an identifier.\n const docIdToGdr = new Map<string, string>()\n for (const inst of instances) {\n const gdr = getSubjectGdr(inst)\n if (gdr === undefined) continue\n try {\n docIdToGdr.set(extractDocumentId(gdr), gdr)\n } catch {\n // Malformed GDR — leave it out of the title lookup.\n }\n }\n if (docIdToGdr.size === 0) return new Map()\n\n // Subject docs are ordinary content, not workflow docs — they carry no\n // engine tag, so we read them straight off the engine's client rather\n // than through the tag-scoped `engine.query` (which mandates a\n // `$tag` filter). Access is the client credential's concern: a\n // doc the caller can't read simply comes back absent.\n let docs: {_id: string; title?: string}[]\n try {\n docs = await engine.client.fetch<{_id: string; title?: string}[]>(\n '*[_id in $docIds]{_id, title}',\n {docIds: Array.from(docIdToGdr.keys())},\n )\n } catch (err) {\n // Titles are optional enrichment — degrade to bare refs rather than\n // failing the whole tool call when the lookup can't be served.\n process.stderr.write(\n `workflow-mcp: subject-title lookup failed; returning refs without titles: ${\n err instanceof Error ? err.message : String(err)\n }\\n`,\n )\n return new Map()\n }\n\n const out = new Map<string, string>()\n for (const doc of docs) {\n const gdr = docIdToGdr.get(doc._id)\n if (gdr !== undefined && typeof doc.title === 'string' && doc.title !== '') {\n out.set(gdr, doc.title)\n }\n }\n return out\n}\n\n/**\n * Build a `ProjectedSubject` for one instance, given the title map\n * produced by `fetchSubjectTitles`. Returns undefined when the\n * workflow has no subject entry; returns `{ ref }` without `title`\n * when the entry is present but the doc has no title field.\n */\nfunction buildSubject(\n instance: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedSubject | undefined {\n const gdr = getSubjectGdr(instance)\n if (gdr === undefined) return undefined\n const title = subjectTitles.get(gdr)\n return title !== undefined ? {ref: gdr, title} : {ref: gdr}\n}\n\nexport function projectSummary(\n doc: WorkflowInstance,\n subjectTitles: Map<string, string>,\n): ProjectedInstanceSummary {\n const definition = JSON.parse(doc.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === doc.currentStage)\n const stageTitle = stage?.title\n const workflowTitle = definition.title\n const subject = buildSubject(doc, subjectTitles)\n return {\n instanceId: doc._id,\n definition: doc.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: doc.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n lastChangedAt: doc.lastChangedAt,\n done: isInstanceDone(doc),\n ...(subject !== undefined ? {subject} : {}),\n }\n}\n\nexport function projectState({\n instance,\n evaluation,\n subjectTitles,\n}: {\n instance: WorkflowInstance\n evaluation: WorkflowEvaluation\n subjectTitles: Map<string, string>\n}): ProjectedInstanceState {\n // The evaluation already exposes the current stage's title from the\n // snapshot, but not the workflow's own title — parse the snapshot\n // once here to read it.\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const workflowTitle = definition.title\n const stageTitle = evaluation.currentStage.stage.title\n\n const activities: ProjectedActivity[] = evaluation.currentStage.activities.map((te) => {\n // Each action's verdict comes from the engine's shared `actionVerdict` —\n // the same \"is this firable, and why not\" the CLI reads — so the verdict\n // shape has one source; we keep the LLM-shaped per-activity nesting.\n const actions: ProjectedActionVerdict[] = te.actions.map((ae) => {\n const verdict = actionVerdict(te, ae)\n return {\n action: verdict.action,\n ...(verdict.title !== undefined ? {title: verdict.title} : {}),\n allowed: verdict.allowed,\n ...(verdict.allowed === false && verdict.disabledReason !== undefined\n ? {disabledReason: formatDisabledReason(verdict.disabledReason)}\n : {}),\n }\n })\n\n return {\n activity: te.activity.name,\n ...(te.activity.title !== undefined ? {title: te.activity.title} : {}),\n ...(te.activity.description !== undefined ? {description: te.activity.description} : {}),\n status: te.status,\n actions,\n }\n })\n\n const recentHistory: ProjectedHistoryEntry[] = instance.history\n .slice(-HISTORY_CAP)\n .toReversed()\n .map((entry) => ({\n at: entry.at,\n type: entry._type,\n summary: summariseHistoryEntry(entry),\n }))\n\n const subject = buildSubject(instance, subjectTitles)\n return {\n instanceId: instance._id,\n definition: instance.definition,\n ...(workflowTitle !== undefined ? {workflowTitle} : {}),\n currentStage: instance.currentStage,\n ...(stageTitle !== undefined ? {currentStageTitle: stageTitle} : {}),\n done: isInstanceDone(instance),\n ...(subject !== undefined ? {subject} : {}),\n activities,\n recentHistory,\n }\n}\n\nexport function isInstanceDone(instance: WorkflowInstance): boolean {\n // The engine sets `completedAt` when an instance enters a terminal\n // stage; falling back to the structural check covers any edge case\n // where the definition snapshot is the source of truth.\n if (instance.completedAt !== undefined) return true\n try {\n const definition = JSON.parse(instance.definitionSnapshot) as WorkflowDefinition\n const stage = definition.stages.find((s) => s.name === instance.currentStage)\n return stage !== undefined && isTerminalStage(stage)\n } catch {\n return false\n }\n}\n\n/**\n * Render the engine's structured {@link DisabledReason} tagged union as a short\n * English phrase for an LLM (the tagged shape stays on the raw evaluation for a\n * richer consumer). Exhaustive by switch: adding a `DisabledReason` kind fails\n * the build here rather than silently degrading to the raw enum string — and the\n * `mutation-guard-denied` arm surfaces its `guardIds` instead of dropping them.\n * Deliberately distinct, softer phrasing from the engine's own detail renderer\n * (a different audience).\n */\nexport function formatDisabledReason(reason: DisabledReason): string {\n switch (reason.kind) {\n case 'filter-failed':\n return \"action's filter condition did not hold for this actor\"\n case 'activity-not-active':\n return 'activity is not currently active'\n case 'stage-terminal':\n return 'stage is terminal — no further actions possible'\n case 'mutation-guard-denied':\n return `blocked by mutation guard(s): ${reason.guardIds.join(', ')}`\n case 'instance-completed':\n return 'instance has already completed'\n case 'requirements-unmet':\n return \"activity's declared requirements are not yet satisfied\"\n }\n}\n\nfunction summariseHistoryEntry(entry: {_type: string} & Record<string, unknown>): string {\n switch (entry._type) {\n case 'stageEntered':\n return `entered stage \"${String(entry.stage)}\"`\n case 'stageExited':\n return `exited stage \"${String(entry.stage)}\" → \"${String(entry.toStage)}\"`\n case 'activityActivated':\n return `activity \"${String(entry.activity)}\" activated`\n case 'activityStatusChanged':\n return `activity \"${String(entry.activity)}\" status: ${String(entry.from)} → ${String(entry.to)}`\n case 'actionFired':\n return `action \"${String(entry.action)}\" fired on activity \"${String(entry.activity)}\"`\n case 'transitionFired':\n return `transition fired: \"${String(entry.fromStage)}\" → \"${String(entry.toStage)}\"`\n case 'effectQueued':\n return `effect \"${String(entry.effect)}\" queued`\n case 'effectCompleted':\n return `effect \"${String(entry.effect)}\" completed (${String(entry.status)})`\n case 'spawned':\n return `spawned child workflow from activity \"${String(entry.activity)}\"`\n default:\n return entry._type\n }\n}\n\n// Diagnosis summary — a plain-English one-liner per verdict, the LLM-facing\n// counterpart to the CLI's terminal-decorated rendering. The engine's\n// `diagnostics` module deliberately does no rendering, so each consumer writes\n// its own; this one folds the key identifiers (the stuck effect/activity, the\n// waiting actions) into one undecorated line.\n\n/** Exported for unit coverage of every verdict branch — see `tools.test.ts`.\n * Used internally by the diagnose_workflow read in `tools.ts`. */\nexport function diagnosisSummary(diagnosis: Diagnosis): string {\n switch (diagnosis.state) {\n case 'progressing':\n return 'This instance will advance on its own.'\n case 'waiting':\n return `Waiting for action on activity \"${diagnosis.activity}\": ${diagnosis.actions.join(' or ')}. This is the normal in-flight state — it advances when someone acts.`\n case 'blocked':\n return `Activity \"${diagnosis.activity}\" is visible but not yet executable — unmet requirement(s): ${diagnosis.requirements.join(', ')}. It will not advance until those are satisfied.`\n case 'completed':\n return `Completed at ${diagnosis.at}.`\n case 'aborted':\n return diagnosis.reason !== undefined\n ? `Aborted at ${diagnosis.at}: ${diagnosis.reason}.`\n : `Aborted at ${diagnosis.at}.`\n case 'stuck':\n return stuckSummary(diagnosis.cause)\n }\n}\n\nfunction stuckSummary(cause: StuckCause): string {\n switch (cause.kind) {\n case 'failed-effect':\n return `Stuck: a failed effect \"${cause.effect.name}\" is blocking activity \"${cause.effect.origin.name}\", which can't complete until the effect succeeds.`\n case 'hung-effect':\n return `Stuck: effect \"${cause.effect.name}\" was claimed but never completed — the drainer likely died mid-dispatch, so it won't drain on its own.`\n case 'failed-activity':\n return `Stuck: activity \"${cause.activity}\" is in a terminal failed state, so any exit transition gated on it can never fire.`\n case 'no-transition-fires':\n return `Stuck: every activity is resolved but no exit transition's filter is satisfied — likely a routing state value a filter reads was never written.`\n case 'transition-unevaluable':\n return `Stuck (recoverable): exit transition(s) ${cause.transitions.join(', ')} read an operand that is missing or unreadable (GROQ null). It advances on its own once that data becomes readable — no manual fix needed.`\n }\n}\n","/**\n * Builds the MCP tool surface: binds the static descriptors (`descriptors.ts`)\n * to implementations that close over a single engine, parses tool input, and\n * runs the engine reads that the projection helpers (`projection.ts`) shape.\n */\n\nimport {\n validateDefinition,\n WORKFLOW_INSTANCE_TYPE,\n tagScopeFilter,\n type AuthoringWorkflow,\n type Engine,\n type WorkflowAccessOverride,\n type WorkflowInstance,\n} from '@sanity/workflow-engine'\nimport {defineWorkflow} from '@sanity/workflow-engine/define'\n\nimport {AUTHORING_GUIDE} from './authoring-guide.ts'\nimport {LIST_CAP, TOOL_DESCRIPTORS, type ToolDescriptor} from './descriptors.ts'\nimport {\n diagnosisSummary,\n fetchSubjectTitles,\n isInstanceDone,\n projectState,\n projectSummary,\n} from './projection.ts'\nimport type {\n ProjectedDiagnosis,\n ProjectedInstanceState,\n ProjectedInstanceSummary,\n ValidateDefinitionResult,\n} from './types.ts'\n\n/**\n * A tool implementation is a function from validated input → JSON-able\n * output. The runner is responsible for serialising the output and for\n * surfacing thrown errors as `is_error` tool results.\n */\nexport type ToolImpl = (input: unknown) => Promise<unknown>\n\nexport interface BuiltTools {\n descriptors: ToolDescriptor[]\n impls: Record<string, ToolImpl>\n}\n\n/**\n * Optional knobs for `buildTools`.\n *\n * `access` — override the engine's actor + grants resolution for every\n * call the tools make. In production, leave this unset: the engine\n * resolves the actor from the supplied client's token via `/users/me`.\n * In tests, pass `ALL_ACCESS` (from `@sanity/workflow-engine-test`)\n * because the TestClient has no token and would otherwise throw on\n * the resolution path.\n */\nexport interface BuildToolsOptions {\n access?: WorkflowAccessOverride\n}\n\n/**\n * Build the MCP tool descriptors and bound implementations against a\n * single engine. Pass the engine the tools should operate on; the\n * returned descriptors are static (safe to cache), the implementations\n * close over the engine.\n *\n * In production this is called once at server boot. In tests it's\n * called per-bench so each eval case has an isolated engine.\n */\nexport function buildTools(engine: Engine, options: BuildToolsOptions = {}): BuiltTools {\n const accessOverride = options.access\n\n const impls: Record<string, ToolImpl> = {\n list_workflow_instances: async (rawInput) => {\n const input = parseListInput(rawInput)\n return listInstances(engine, input)\n },\n get_workflow_state: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'get_workflow_state')\n return getInstanceState({engine, instanceId: input.instance_id, accessOverride})\n },\n diagnose_workflow: async (rawInput) => {\n const input = parseInstanceIdInput(rawInput, 'diagnose_workflow')\n return getInstanceDiagnosis({engine, instanceId: input.instance_id, accessOverride})\n },\n fire_action: async (rawInput) => {\n const input = parseFireActionInput(rawInput)\n await engine.fireAction({\n instanceId: input.instance_id,\n activity: input.activity,\n action: input.action,\n ...(input.params !== undefined ? {params: input.params} : {}),\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n // Re-use the same projection so the LLM sees consistent shapes\n // between get_workflow_state and fire_action responses.\n return getInstanceState({engine, instanceId: input.instance_id, accessOverride})\n },\n // Authoring tools — engine-independent: they teach and check the DSL, they\n // don't touch the lake or an instance.\n get_workflow_authoring_guide: async () => AUTHORING_GUIDE,\n validate_workflow_definition: async (rawInput): Promise<ValidateDefinitionResult> => {\n const {definition} = parseValidateInput(rawInput)\n try {\n // `defineWorkflow` is the gate (valibot structural parse + desugar +\n // cross-field invariants); `validateDefinition` adds GROQ syntax checks.\n // The cast only satisfies the compiler — `defineWorkflow` validates the\n // value at runtime and throws an aggregated, path-prefixed error.\n const stored = defineWorkflow(definition as AuthoringWorkflow)\n validateDefinition(stored)\n return {valid: true, definition: stored}\n } catch (err) {\n return {valid: false, error: err instanceof Error ? err.message : String(err)}\n }\n },\n }\n\n return {descriptors: TOOL_DESCRIPTORS, impls}\n}\n\n// Input parsers — lightweight, no zod dependency\n//\n// Reasoning: pulling in zod would inflate the package for this handful of tools.\n// The MCP server and the eval harness already constrain the shape via\n// the JSON schema in the descriptor; these parsers exist for runtime\n// type-narrowing in TypeScript, not for deep validation.\n\ninterface ListInput {\n definition?: string\n status: 'in_flight' | 'done' | 'any'\n}\n\nfunction parseListInput(raw: unknown): ListInput {\n if (raw === null || typeof raw !== 'object') {\n return {status: 'in_flight'}\n }\n const obj = raw as Record<string, unknown>\n const status = obj.status === 'done' || obj.status === 'any' ? obj.status : ('in_flight' as const)\n const definition = typeof obj.definition === 'string' ? obj.definition : undefined\n return definition !== undefined ? {definition, status} : {status}\n}\n\n/** Narrow an unknown tool input to an object envelope, or throw naming `tool`.\n * The shared first guard for every throwing parser below. */\nfunction asObject(raw: unknown, tool: string): Record<string, unknown> {\n if (raw === null || typeof raw !== 'object') {\n throw new Error(`${tool}: input must be an object`)\n }\n return raw as Record<string, unknown>\n}\n\n/** Read a required non-empty string field off a parsed envelope, or throw\n * naming `tool` and `key`. */\nfunction requireString({\n obj,\n key,\n tool,\n}: {\n obj: Record<string, unknown>\n key: string\n tool: string\n}): string {\n const value = obj[key]\n if (typeof value !== 'string' || value === '') {\n throw new Error(`${tool}: ${key} is required`)\n }\n return value\n}\n\n/** Narrow the validate input to `{definition: object}`. The deep validation\n * is `defineWorkflow`'s job — this only guards the envelope. */\nfunction parseValidateInput(raw: unknown): {definition: object} {\n const obj = asObject(raw, 'validate_workflow_definition')\n if (obj.definition === null || typeof obj.definition !== 'object') {\n throw new Error('validate_workflow_definition: `definition` is required and must be an object')\n }\n return {definition: obj.definition}\n}\n\n/** Shared by every single-instance tool — `tool` names the caller so the\n * thrown error points at the right tool. */\nfunction parseInstanceIdInput(raw: unknown, tool: string): {instance_id: string} {\n const obj = asObject(raw, tool)\n return {instance_id: requireString({obj, key: 'instance_id', tool})}\n}\n\nfunction parseFireActionInput(raw: unknown): {\n instance_id: string\n activity: string\n action: string\n params?: Record<string, unknown>\n} {\n const obj = asObject(raw, 'fire_action')\n // `params` is optional and free-form (shapes are per-action — the engine\n // validates the values against the action's declared params). We only guard\n // the envelope here: present means it must be a plain object, not an array.\n let params: Record<string, unknown> | undefined\n if (obj.params !== undefined) {\n if (obj.params === null || typeof obj.params !== 'object' || Array.isArray(obj.params)) {\n throw new Error('fire_action: params must be an object')\n }\n params = obj.params as Record<string, unknown>\n }\n return {\n instance_id: requireString({obj, key: 'instance_id', tool: 'fire_action'}),\n activity: requireString({obj, key: 'activity', tool: 'fire_action'}),\n action: requireString({obj, key: 'action', tool: 'fire_action'}),\n ...(params !== undefined ? {params} : {}),\n }\n}\n\n// Engine reads, projected for an LLM\n\nasync function listInstances(\n engine: Engine,\n input: ListInput,\n): Promise<{instances: ProjectedInstanceSummary[]}> {\n // Tag-scoped via the engine's own query helper — every instance\n // returned is one this engine is allowed to see.\n const groqParts = [`_type == \"${WORKFLOW_INSTANCE_TYPE}\"`]\n if (input.definition !== undefined) {\n groqParts.push('definition == $definition')\n }\n const groq =\n `*[${groqParts.join(' && ')} && ${tagScopeFilter()}] ` +\n `| order(lastChangedAt desc)[0...${LIST_CAP}]`\n\n const params: Record<string, unknown> = {}\n if (input.definition !== undefined) params.definition = input.definition\n\n const docs = await engine.query<WorkflowInstance[]>({groq, params})\n const filtered = docs.filter((doc) => {\n const done = isInstanceDone(doc)\n if (input.status === 'done') return done\n if (input.status === 'in_flight') return !done\n return true // \"any\"\n })\n const subjectTitles = await fetchSubjectTitles(engine, filtered)\n return {\n instances: filtered.map((doc) => projectSummary(doc, subjectTitles)),\n }\n}\n\nasync function getInstanceState({\n engine,\n instanceId,\n accessOverride,\n}: {\n engine: Engine\n instanceId: string\n accessOverride: WorkflowAccessOverride | undefined\n}): Promise<ProjectedInstanceState> {\n const evaluation = await engine.evaluateInstance({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n const instance = await engine.getInstance({instanceId})\n const subjectTitles = await fetchSubjectTitles(engine, [instance])\n return projectState({instance, evaluation, subjectTitles})\n}\n\nasync function getInstanceDiagnosis({\n engine,\n instanceId,\n accessOverride,\n}: {\n engine: Engine\n instanceId: string\n accessOverride: WorkflowAccessOverride | undefined\n}): Promise<ProjectedDiagnosis> {\n const {diagnosis, remediations} = await engine.diagnose({\n instanceId,\n ...(accessOverride !== undefined ? {access: accessOverride} : {}),\n })\n return {\n instanceId,\n state: diagnosis.state,\n summary: diagnosisSummary(diagnosis),\n ...(diagnosis.state === 'stuck' ? {cause: diagnosis.cause.kind} : {}),\n remediations,\n }\n}\n"],"names":[],"mappings":"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"}
package/dist/stdio.js CHANGED
@@ -17,7 +17,11 @@ function parseResourceType(raw) {
17
17
  return value;
18
18
  throw new Error(`Invalid WORKFLOW_RESOURCE_TYPE: ${value}`);
19
19
  }
20
- async function handleToolCall(impls, name, args) {
20
+ async function handleToolCall({
21
+ impls,
22
+ name,
23
+ args
24
+ }) {
21
25
  const impl = impls[name];
22
26
  if (impl === void 0)
23
27
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: !0 };
@@ -59,7 +63,7 @@ async function runStdioServer() {
59
63
  }))
60
64
  })), server.setRequestHandler(
61
65
  CallToolRequestSchema,
62
- async (request) => handleToolCall(impls, request.params.name, request.params.arguments)
66
+ async (request) => handleToolCall({ impls, name: request.params.name, args: request.params.arguments })
63
67
  );
64
68
  const transport = new StdioServerTransport();
65
69
  await server.connect(transport);
package/dist/stdio.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.js","sources":["../src/server.ts","../src/stdio.ts"],"sourcesContent":["/**\n * Server-side helpers for the stdio MCP entry point. Kept separate from\n * the boot path so the env parsing and tool-call dispatch are\n * unit-testable; `src/stdio.ts` wires these to the real\n * `@modelcontextprotocol/sdk` transport.\n */\n\nimport type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'\n\nimport type {ToolImpl} from './tools.ts'\n\nexport type ResourceType = 'dataset' | 'canvas' | 'media-library' | 'dashboard'\n\nconst RESOURCE_TYPES: readonly ResourceType[] = ['dataset', 'canvas', 'media-library', 'dashboard']\n\n/** Read a required env var, throwing a clear error when it's absent/empty. */\nexport function requireEnv(env: NodeJS.ProcessEnv, name: string): string {\n const value = env[name]\n if (typeof value !== 'string' || value === '') {\n throw new Error(`Missing required environment variable: ${name}`)\n }\n return value\n}\n\n/** Parse `WORKFLOW_RESOURCE_TYPE`, defaulting to `dataset`. */\nexport function parseResourceType(raw: string | undefined): ResourceType {\n const value = raw ?? 'dataset'\n if ((RESOURCE_TYPES as readonly string[]).includes(value)) {\n return value as ResourceType\n }\n throw new Error(`Invalid WORKFLOW_RESOURCE_TYPE: ${value}`)\n}\n\n/**\n * Dispatch one tool call against the bound impls. Unknown tools and\n * thrown impls both become `isError` text results so the MCP client\n * sees a recoverable error rather than a transport-level failure.\n */\nexport async function handleToolCall(\n impls: Record<string, ToolImpl>,\n name: string,\n args: unknown,\n): Promise<CallToolResult> {\n const impl = impls[name]\n if (impl === undefined) {\n return {content: [{type: 'text', text: `Unknown tool: ${name}`}], isError: true}\n }\n try {\n const result = await impl(args ?? {})\n return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]}\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return {content: [{type: 'text', text: message}], isError: true}\n }\n}\n","/**\n * stdio MCP host — boots a {@link Server} from `@modelcontextprotocol/sdk`,\n * binds the workflow tools to an engine built from environment variables,\n * and listens on stdio.\n *\n * This is one of two thin hosts over the shared {@link buildTools} core:\n * the stdio host (here) is the early-access self-host (one env-built\n * engine, BYO scoped token, system-actor attribution); the production\n * host embeds {@link buildTools} with per-request identity elsewhere.\n *\n * Kept as its own package entry (not inline in `bin/`) so it can be\n * compiled to `dist/stdio.js` by pkg-utils and re-used by both the\n * published bin (`bin/run.js`) and the dev entry (`bin/workflow-mcp.ts`)\n * — one boot path, no duplication. Keeping it out of the `.` entry also\n * keeps the MCP transport deps out of the library import graph.\n */\n\nimport {Server} from '@modelcontextprotocol/sdk/server/index.js'\nimport {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {CallToolRequestSchema, ListToolsRequestSchema} from '@modelcontextprotocol/sdk/types.js'\nimport {createClient} from '@sanity/client'\nimport {createEngine} from '@sanity/workflow-engine'\n\nimport {handleToolCall, parseResourceType, requireEnv} from './server.ts'\nimport {buildTools} from './tools.ts'\n\n/**\n * Read the environment, build the engine, register the tools, and serve\n * on stdio. Resolves when the transport is connected (the process then\n * stays alive on the open stdio streams).\n */\nexport async function runStdioServer(): Promise<void> {\n const projectId = requireEnv(process.env, 'SANITY_PROJECT_ID')\n const dataset = requireEnv(process.env, 'SANITY_DATASET')\n const token = requireEnv(process.env, 'SANITY_AUTH_TOKEN')\n // The environment partition. Required and never defaulted — the engine\n // enforces nothing, so the tag is the only thing stopping a misconfigured\n // server from reading and writing prod. Fail loudly rather than guess.\n const tag = requireEnv(process.env, 'WORKFLOW_TAG')\n const apiHost = process.env.SANITY_API_HOST ?? 'https://api.sanity.io'\n\n const client = createClient({\n projectId,\n dataset,\n token,\n apiHost,\n apiVersion: '2026-04-29',\n useCdn: false,\n })\n\n // Stderr (not stdout — stdout is the MCP JSON-RPC channel). Surfaces in\n // Claude Desktop's developer log; lets you spot wrong-environment\n // mistakes without spelunking through tool errors.\n process.stderr.write(\n `workflow-mcp MCP starting against ${apiHost} (project=${projectId}, dataset=${dataset}, tag=${tag})\\n`,\n )\n\n const engine = createEngine({\n client,\n workflowResource: {\n type: parseResourceType(process.env.WORKFLOW_RESOURCE_TYPE),\n id: process.env.WORKFLOW_RESOURCE_ID ?? `${projectId}.${dataset}`,\n },\n tag,\n })\n\n // The engine's default actor resolution hits `/users/me`, which a\n // project-scoped token can't reach. Mirror the CLI's \"pin a system\n // actor for writes\" pattern — every call from this MCP is attributed\n // to `workflow-mcp` rather than a real user. Production-shaped auth\n // (where the LLM operates on behalf of a known user) needs a different\n // shape; this is the early-access compromise the CLI also makes.\n const {descriptors, impls} = buildTools(engine, {\n access: {actor: {kind: 'system', id: 'workflow-mcp'}},\n })\n\n const server = new Server({name: 'workflow-mcp', version: '0.0.0'}, {capabilities: {tools: {}}})\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: descriptors.map((d) => ({\n name: d.name,\n description: d.description,\n inputSchema: d.input_schema,\n })),\n }))\n\n server.setRequestHandler(CallToolRequestSchema, async (request) =>\n handleToolCall(impls, request.params.name, request.params.arguments),\n )\n\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n"],"names":[],"mappings":";;;;;;AAaA,MAAM,iBAA0C,CAAC,WAAW,UAAU,iBAAiB,WAAW;AAG3F,SAAS,WAAW,KAAwB,MAAsB;AACvE,QAAM,QAAQ,IAAI,IAAI;AACtB,MAAI,OAAO,SAAU,YAAY,UAAU;AACzC,UAAM,IAAI,MAAM,0CAA0C,IAAI,EAAE;AAElE,SAAO;AACT;AAGO,SAAS,kBAAkB,KAAuC;AACvE,QAAM,QAAQ,OAAO;AACrB,MAAK,eAAqC,SAAS,KAAK;AACtD,WAAO;AAET,QAAM,IAAI,MAAM,mCAAmC,KAAK,EAAE;AAC5D;AAOA,eAAsB,eACpB,OACA,MACA,MACyB;AACzB,QAAM,OAAO,MAAM,IAAI;AACvB,MAAI,SAAS;AACX,WAAO,EAAC,SAAS,CAAC,EAAC,MAAM,QAAQ,MAAM,iBAAiB,IAAI,GAAA,CAAG,GAAG,SAAS,GAAA;AAE7E,MAAI;AACF,UAAM,SAAS,MAAM,KAAK,QAAQ,CAAA,CAAE;AACpC,WAAO,EAAC,SAAS,CAAC,EAAC,MAAM,QAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAA,CAAE,EAAA;AAAA,EACzE,SAAS,KAAK;AAEZ,WAAO,EAAC,SAAS,CAAC,EAAC,MAAM,QAAQ,MADjB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAChB,GAAG,SAAS,GAAA;AAAA,EAC7D;AACF;ACvBA,eAAsB,iBAAgC;AACpD,QAAM,YAAY,WAAW,QAAQ,KAAK,mBAAmB,GACvD,UAAU,WAAW,QAAQ,KAAK,gBAAgB,GAClD,QAAQ,WAAW,QAAQ,KAAK,mBAAmB,GAInD,MAAM,WAAW,QAAQ,KAAK,cAAc,GAC5C,UAAU,QAAQ,IAAI,mBAAmB,yBAEzC,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA,CACT;AAKD,UAAQ,OAAO;AAAA,IACb,qCAAqC,OAAO,aAAa,SAAS,aAAa,OAAO,SAAS,GAAG;AAAA;AAAA,EAAA;AAGpG,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA,kBAAkB;AAAA,MAChB,MAAM,kBAAkB,QAAQ,IAAI,sBAAsB;AAAA,MAC1D,IAAI,QAAQ,IAAI,wBAAwB,GAAG,SAAS,IAAI,OAAO;AAAA,IAAA;AAAA,IAEjE;AAAA,EAAA,CACD,GAQK,EAAC,aAAa,MAAA,IAAS,WAAW,QAAQ;AAAA,IAC9C,QAAQ,EAAC,OAAO,EAAC,MAAM,UAAU,IAAI,iBAAc;AAAA,EAAC,CACrD,GAEK,SAAS,IAAI,OAAO,EAAC,MAAM,gBAAgB,SAAS,QAAA,GAAU,EAAC,cAAc,EAAC,OAAO,CAAA,EAAC,GAAG;AAE/F,SAAO,kBAAkB,wBAAwB,aAAa;AAAA,IAC5D,OAAO,YAAY,IAAI,CAAC,OAAO;AAAA,MAC7B,MAAM,EAAE;AAAA,MACR,aAAa,EAAE;AAAA,MACf,aAAa,EAAE;AAAA,IAAA,EACf;AAAA,EAAA,EACF,GAEF,OAAO;AAAA,IAAkB;AAAA,IAAuB,OAAO,YACrD,eAAe,OAAO,QAAQ,OAAO,MAAM,QAAQ,OAAO,SAAS;AAAA,EAAA;AAGrE,QAAM,YAAY,IAAI,qBAAA;AACtB,QAAM,OAAO,QAAQ,SAAS;AAChC;"}
1
+ {"version":3,"file":"stdio.js","sources":["../src/server.ts","../src/stdio.ts"],"sourcesContent":["/**\n * Server-side helpers for the stdio MCP entry point. Kept separate from\n * the boot path so the env parsing and tool-call dispatch are\n * unit-testable; `src/stdio.ts` wires these to the real\n * `@modelcontextprotocol/sdk` transport.\n */\n\nimport type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'\n\nimport type {ToolImpl} from './tools.ts'\n\nexport type ResourceType = 'dataset' | 'canvas' | 'media-library' | 'dashboard'\n\nconst RESOURCE_TYPES: readonly ResourceType[] = ['dataset', 'canvas', 'media-library', 'dashboard']\n\n/** Read a required env var, throwing a clear error when it's absent/empty. */\nexport function requireEnv(env: NodeJS.ProcessEnv, name: string): string {\n const value = env[name]\n if (typeof value !== 'string' || value === '') {\n throw new Error(`Missing required environment variable: ${name}`)\n }\n return value\n}\n\n/** Parse `WORKFLOW_RESOURCE_TYPE`, defaulting to `dataset`. */\nexport function parseResourceType(raw: string | undefined): ResourceType {\n const value = raw ?? 'dataset'\n if ((RESOURCE_TYPES as readonly string[]).includes(value)) {\n return value as ResourceType\n }\n throw new Error(`Invalid WORKFLOW_RESOURCE_TYPE: ${value}`)\n}\n\n/**\n * Dispatch one tool call against the bound impls. Unknown tools and\n * thrown impls both become `isError` text results so the MCP client\n * sees a recoverable error rather than a transport-level failure.\n */\nexport async function handleToolCall({\n impls,\n name,\n args,\n}: {\n impls: Record<string, ToolImpl>\n name: string\n args: unknown\n}): Promise<CallToolResult> {\n const impl = impls[name]\n if (impl === undefined) {\n return {content: [{type: 'text', text: `Unknown tool: ${name}`}], isError: true}\n }\n try {\n const result = await impl(args ?? {})\n return {content: [{type: 'text', text: JSON.stringify(result, null, 2)}]}\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return {content: [{type: 'text', text: message}], isError: true}\n }\n}\n","/**\n * stdio MCP host — boots a {@link Server} from `@modelcontextprotocol/sdk`,\n * binds the workflow tools to an engine built from environment variables,\n * and listens on stdio.\n *\n * This is one of two thin hosts over the shared {@link buildTools} core:\n * the stdio host (here) is the early-access self-host (one env-built\n * engine, BYO scoped token, system-actor attribution); the production\n * host embeds {@link buildTools} with per-request identity elsewhere.\n *\n * Kept as its own package entry (not inline in `bin/`) so it can be\n * compiled to `dist/stdio.js` by pkg-utils and re-used by both the\n * published bin (`bin/run.js`) and the dev entry (`bin/workflow-mcp.ts`)\n * — one boot path, no duplication. Keeping it out of the `.` entry also\n * keeps the MCP transport deps out of the library import graph.\n */\n\nimport {Server} from '@modelcontextprotocol/sdk/server/index.js'\nimport {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {CallToolRequestSchema, ListToolsRequestSchema} from '@modelcontextprotocol/sdk/types.js'\nimport {createClient} from '@sanity/client'\nimport {createEngine} from '@sanity/workflow-engine'\n\nimport {handleToolCall, parseResourceType, requireEnv} from './server.ts'\nimport {buildTools} from './tools.ts'\n\n/**\n * Read the environment, build the engine, register the tools, and serve\n * on stdio. Resolves when the transport is connected (the process then\n * stays alive on the open stdio streams).\n */\nexport async function runStdioServer(): Promise<void> {\n const projectId = requireEnv(process.env, 'SANITY_PROJECT_ID')\n const dataset = requireEnv(process.env, 'SANITY_DATASET')\n const token = requireEnv(process.env, 'SANITY_AUTH_TOKEN')\n // The environment partition. Required and never defaulted — the engine\n // enforces nothing, so the tag is the only thing stopping a misconfigured\n // server from reading and writing prod. Fail loudly rather than guess.\n const tag = requireEnv(process.env, 'WORKFLOW_TAG')\n const apiHost = process.env.SANITY_API_HOST ?? 'https://api.sanity.io'\n\n const client = createClient({\n projectId,\n dataset,\n token,\n apiHost,\n apiVersion: '2026-04-29',\n useCdn: false,\n })\n\n // Stderr (not stdout — stdout is the MCP JSON-RPC channel). Surfaces in\n // Claude Desktop's developer log; lets you spot wrong-environment\n // mistakes without spelunking through tool errors.\n process.stderr.write(\n `workflow-mcp MCP starting against ${apiHost} (project=${projectId}, dataset=${dataset}, tag=${tag})\\n`,\n )\n\n const engine = createEngine({\n client,\n workflowResource: {\n type: parseResourceType(process.env.WORKFLOW_RESOURCE_TYPE),\n id: process.env.WORKFLOW_RESOURCE_ID ?? `${projectId}.${dataset}`,\n },\n tag,\n })\n\n // The engine's default actor resolution hits `/users/me`, which a\n // project-scoped token can't reach. Mirror the CLI's \"pin a system\n // actor for writes\" pattern — every call from this MCP is attributed\n // to `workflow-mcp` rather than a real user. Production-shaped auth\n // (where the LLM operates on behalf of a known user) needs a different\n // shape; this is the early-access compromise the CLI also makes.\n const {descriptors, impls} = buildTools(engine, {\n access: {actor: {kind: 'system', id: 'workflow-mcp'}},\n })\n\n const server = new Server({name: 'workflow-mcp', version: '0.0.0'}, {capabilities: {tools: {}}})\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: descriptors.map((d) => ({\n name: d.name,\n description: d.description,\n inputSchema: d.input_schema,\n })),\n }))\n\n server.setRequestHandler(CallToolRequestSchema, async (request) =>\n handleToolCall({impls, name: request.params.name, args: request.params.arguments}),\n )\n\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n"],"names":[],"mappings":";;;;;;AAaA,MAAM,iBAA0C,CAAC,WAAW,UAAU,iBAAiB,WAAW;AAG3F,SAAS,WAAW,KAAwB,MAAsB;AACvE,QAAM,QAAQ,IAAI,IAAI;AACtB,MAAI,OAAO,SAAU,YAAY,UAAU;AACzC,UAAM,IAAI,MAAM,0CAA0C,IAAI,EAAE;AAElE,SAAO;AACT;AAGO,SAAS,kBAAkB,KAAuC;AACvE,QAAM,QAAQ,OAAO;AACrB,MAAK,eAAqC,SAAS,KAAK;AACtD,WAAO;AAET,QAAM,IAAI,MAAM,mCAAmC,KAAK,EAAE;AAC5D;AAOA,eAAsB,eAAe;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAI4B;AAC1B,QAAM,OAAO,MAAM,IAAI;AACvB,MAAI,SAAS;AACX,WAAO,EAAC,SAAS,CAAC,EAAC,MAAM,QAAQ,MAAM,iBAAiB,IAAI,GAAA,CAAG,GAAG,SAAS,GAAA;AAE7E,MAAI;AACF,UAAM,SAAS,MAAM,KAAK,QAAQ,CAAA,CAAE;AACpC,WAAO,EAAC,SAAS,CAAC,EAAC,MAAM,QAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAA,CAAE,EAAA;AAAA,EACzE,SAAS,KAAK;AAEZ,WAAO,EAAC,SAAS,CAAC,EAAC,MAAM,QAAQ,MADjB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAChB,GAAG,SAAS,GAAA;AAAA,EAC7D;AACF;AC3BA,eAAsB,iBAAgC;AACpD,QAAM,YAAY,WAAW,QAAQ,KAAK,mBAAmB,GACvD,UAAU,WAAW,QAAQ,KAAK,gBAAgB,GAClD,QAAQ,WAAW,QAAQ,KAAK,mBAAmB,GAInD,MAAM,WAAW,QAAQ,KAAK,cAAc,GAC5C,UAAU,QAAQ,IAAI,mBAAmB,yBAEzC,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA,CACT;AAKD,UAAQ,OAAO;AAAA,IACb,qCAAqC,OAAO,aAAa,SAAS,aAAa,OAAO,SAAS,GAAG;AAAA;AAAA,EAAA;AAGpG,QAAM,SAAS,aAAa;AAAA,IAC1B;AAAA,IACA,kBAAkB;AAAA,MAChB,MAAM,kBAAkB,QAAQ,IAAI,sBAAsB;AAAA,MAC1D,IAAI,QAAQ,IAAI,wBAAwB,GAAG,SAAS,IAAI,OAAO;AAAA,IAAA;AAAA,IAEjE;AAAA,EAAA,CACD,GAQK,EAAC,aAAa,MAAA,IAAS,WAAW,QAAQ;AAAA,IAC9C,QAAQ,EAAC,OAAO,EAAC,MAAM,UAAU,IAAI,iBAAc;AAAA,EAAC,CACrD,GAEK,SAAS,IAAI,OAAO,EAAC,MAAM,gBAAgB,SAAS,QAAA,GAAU,EAAC,cAAc,EAAC,OAAO,CAAA,EAAC,GAAG;AAE/F,SAAO,kBAAkB,wBAAwB,aAAa;AAAA,IAC5D,OAAO,YAAY,IAAI,CAAC,OAAO;AAAA,MAC7B,MAAM,EAAE;AAAA,MACR,aAAa,EAAE;AAAA,MACf,aAAa,EAAE;AAAA,IAAA,EACf;AAAA,EAAA,EACF,GAEF,OAAO;AAAA,IAAkB;AAAA,IAAuB,OAAO,YACrD,eAAe,EAAC,OAAO,MAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,OAAO,WAAU;AAAA,EAAA;AAGnF,QAAM,YAAY,IAAI,qBAAA;AACtB,QAAM,OAAO,QAAQ,SAAS;AAChC;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server exposing Sanity workflow tools to agents — operate running workflow instances and author new definitions.",
5
5
  "keywords": [
6
6
  "agent",
@@ -53,14 +53,14 @@
53
53
  "dependencies": {
54
54
  "@modelcontextprotocol/sdk": "^1.29.0",
55
55
  "@sanity/client": "^7.22.1",
56
- "@sanity/workflow-engine": "0.12.0"
56
+ "@sanity/workflow-engine": "0.13.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@sanity/pkg-utils": "^10.5.2",
60
60
  "@types/node": "^24.12.4",
61
61
  "vitest": "^4.1.8",
62
- "@sanity/workflow-engine-test": "0.7.0",
63
- "@sanity/workflow-examples": "0.2.0"
62
+ "@sanity/workflow-engine-test": "0.8.0",
63
+ "@sanity/workflow-examples": "0.3.0"
64
64
  },
65
65
  "engines": {
66
66
  "node": ">=20"