@sanity/workflow-mcp 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,10 +3,9 @@
3
3
  MCP tool surface for the workflows engine. Two jobs, no fluff: operate running
4
4
  instances, and author new definitions.
5
5
 
6
- > [!WARNING]
7
- > POC. The eval harness in [`@sanity/workflow-mcp-evals`](../workflow-mcp-evals)
8
- > is the load-bearing artifact for now this package exposes the tools
9
- > the evals drive, plus a stdio MCP entry point for real MCP clients.
6
+ The tool surface is developed against the eval harness in
7
+ [`@sanity/workflow-mcp-evals`](../workflow-mcp-evals) — this package exposes
8
+ the tools the evals drive, plus a stdio MCP entry point for real MCP clients.
10
9
 
11
10
  ## What the tools do
12
11
 
@@ -57,11 +56,13 @@ it's `bin/run.js`, and once the package is published this is what
57
56
  node packages/workflow-mcp/bin/run.js
58
57
  ```
59
58
 
60
- Both share the one boot path in `src/stdio.ts` and read the same
61
- `SANITY_*` and `WORKFLOW_*` env vars as
62
- [`@sanity/workflow-cli`](../workflow-cli) (which loads them
63
- from the root `.env`). Copy `.env.example` to `.env` at the repo root
64
- to get started note the MCP _requires_ `SANITY_DATASET`.
59
+ Both share the one boot path in `src/stdio.ts`, which reads its
60
+ configuration from the environment: `SANITY_PROJECT_ID`, `SANITY_DATASET`,
61
+ `SANITY_AUTH_TOKEN`, and `WORKFLOW_TAG` are required; `SANITY_API_HOST`,
62
+ `WORKFLOW_RESOURCE_TYPE`, and `WORKFLOW_RESOURCE_ID` are optional. The `dev`
63
+ script loads them from the repo-root `.env`, so copy `.env.example` to `.env`
64
+ to get started. (The CLI has since moved to a `sanity.workflow.ts` config; the
65
+ MCP still takes plain env vars.)
65
66
 
66
67
  ## API
67
68
 
@@ -69,7 +70,7 @@ to get started — note the MCP _requires_ `SANITY_DATASET`.
69
70
  import {createEngine} from '@sanity/workflow-engine'
70
71
  import {buildTools} from '@sanity/workflow-mcp'
71
72
 
72
- const engine = createEngine({client, workflowResource, tags})
73
+ const engine = createEngine({client, workflowResource, tag})
73
74
  const {descriptors, impls} = buildTools(engine)
74
75
 
75
76
  // descriptors → feed to Anthropic API's `tools:` param, or to an MCP
@@ -84,8 +85,9 @@ const {descriptors, impls} = buildTools(engine)
84
85
  the LLM surface until we have an eval case that needs it (and a
85
86
  story for the actor identity that would authorise the override).
86
87
  - **No `start_instance` tool.** Starting an instance is an operator
87
- action that needs context the LLM doesn't have (subject doc id,
88
- initial state). Out of scope for v0.1.
88
+ action that needs context the LLM doesn't have (subject doc ref,
89
+ initial field values); operators start instances with the workflow
90
+ CLI's `start` command.
89
91
  - **No effect completion.** Same logic — effects are queued by the
90
92
  engine and drained by the runtime, not by a human or LLM.
91
93
  - **No subscription / streaming.** MCP supports it; we haven't needed
package/dist/index.cjs CHANGED
@@ -46,31 +46,62 @@ const minimalExample = {
46
46
  {
47
47
  name: "review",
48
48
  title: "Editorial review",
49
+ // The decision is data: each action writes it, the transition filters
50
+ // read it. Stage-scoped so a later review round starts with a clean slate.
51
+ fields: [{ type: "string", name: "decision", title: "Review decision" }],
49
52
  activities: [
50
53
  {
51
54
  name: "review",
52
55
  title: "Review the draft",
53
56
  activation: "auto",
54
57
  actions: [
55
- { name: "approve", title: "Approve", status: "done" },
56
- { name: "reject", title: "Reject", status: "failed" }
58
+ {
59
+ name: "approve",
60
+ title: "Approve",
61
+ status: "done",
62
+ ops: [
63
+ {
64
+ type: "field.set",
65
+ target: { field: "decision" },
66
+ value: { type: "literal", value: "approve" }
67
+ }
68
+ ]
69
+ },
70
+ // Rejecting is the reviewer DOING their job — a decision, not a
71
+ // failure — so it resolves `done` and routes via the field.
72
+ {
73
+ name: "reject",
74
+ title: "Reject",
75
+ status: "done",
76
+ ops: [
77
+ {
78
+ type: "field.set",
79
+ target: { field: "decision" },
80
+ value: { type: "literal", value: "reject" }
81
+ }
82
+ ]
83
+ }
57
84
  ]
58
85
  }
59
86
  ],
60
87
  transitions: [
61
- { name: "to-approved", title: "Approve and finish", to: "approved" },
62
- // Reject flips the activity to `failed`; this routes the instance back.
88
+ {
89
+ name: "to-approved",
90
+ title: "Approve and finish",
91
+ to: "approved",
92
+ filter: "$allActivitiesDone && $fields.decision == 'approve'"
93
+ },
63
94
  {
64
95
  name: "to-drafting",
65
96
  title: "Send back to drafting",
66
97
  to: "drafting",
67
- filter: "$anyActivityFailed"
98
+ filter: "$allActivitiesDone && $fields.decision == 'reject'"
68
99
  }
69
100
  ]
70
101
  },
71
102
  { name: "approved", title: "Approved", description: "Terminal \u2014 no transitions out." }
72
103
  ]
73
- }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it \u2014 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` \u2014 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` \u2014 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 \u2014 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` \u2014 one concrete\n principal; `assignee` / `assignees` \u2014 one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) \u2014 `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the slot once at\n materialisation and is **optional** \u2014 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` \u2014 the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work \u2014 rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log \u2014 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 \u2014 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\" }` \u2192 `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) \u2014 **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'` \u2014 flips the firing activity (shown above).\n- Omitted transition `filter` \u2014 defaults to `$allActivitiesDone`.\n- Omitted activity `activation` \u2014 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 \u2192 drafting` gated\n on `$anyActivityFailed`), not to a terminal stage. Reserve terminal stages for\n completion and for explicit cancellation/abandonment \u2014 a workflow should not\n dead-end just because something was declined.\n- **Prefer draft \u2192 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.", AUTHORING_GUIDE = `${ORIENTATION}
104
+ }, ORIENTATION = "# Authoring a workflow definition\n\nA workflow definition is a plain JSON object. There is no code in it \u2014 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` \u2014 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` \u2014 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 \u2014 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. Status is a health axis, not a decision:\n a routine decision (reject, send back, decline, hold) resolves `'done'` and\n writes the decision into a field the transition filters read \u2014 reserve\n `'failed'` for work that genuinely could not complete (e.g. a missed\n deadline via `failWhen`). `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` \u2014 one concrete\n principal; `assignee` / `assignees` \u2014 one or many user-or-role assignees), and\n the compositional kinds `object` (`{ type:'object', name, fields: [...] }`) and\n `array` (`{ type:'array', name, of: [...] }`) \u2014 `fields`/`of` are themselves\n field shapes, so any structure composes. `initialValue` seeds the field once at\n materialisation and is **optional** \u2014 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` \u2014 the\n headline document it is about.\n Two list sugars desugar to `array`: `{type:'todoList', name}` (ad-hoc\n status-tracked work \u2014 rows `{ label, status, assignee?, dueDate? }`) and\n `{type:'notes', name}` (an append-only audit/comment log \u2014 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 (the engine's exported\n`CONDITION_VARS` inventory is the source of truth): `$allActivitiesDone`,\n`$anyActivityFailed` (booleans over the current stage's activities), `$activities` (the activity\nlist), `$fields` (field values), `$now`, `$self`/`$stage`/`$parent`/`$ancestors`\n(instance identity + position), `$effectStatus` (effect name \u2192 `'done'`/`'failed'`\nof the run queued during the CURRENT stage entry \u2014 the re-entry-safe way to gate\non an effect having drained, e.g.\n`completeWhen: \"defined($effectStatus['my.effect'])\"`), and the caller-scoped\nvars `$actor` (the acting user), `$assigned` (whether the caller is the\nactivity's assignee \u2014 the idiomatic permission gate, used as\n`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\" }` \u2192 `$ready`).\n\nConditions evaluate against an in-memory snapshot (the instance + its subject +\nfield-declared docs) \u2014 **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'` \u2014 flips the firing activity (shown above).\n- Omitted transition `filter` \u2014 defaults to `$allActivitiesDone`.\n- Omitted activity `activation` \u2014 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 \u2192 drafting` gated\n on a `decision` field the reject action wrote), not to a terminal stage.\n Reserve terminal stages for completion and for explicit\n cancellation/abandonment \u2014 a workflow should not dead-end just because\n something was declined.\n- **Decisions are fields, not failures.** When a stage branches on a human\n decision, declare a stage-scoped `string` field (stage scope resets on\n re-entry, so loop-backs start clean), have each deciding action `field.set`\n it, and gate every outbound transition on its value (Example 2). Do not\n encode a decision as `status: 'failed'` + `$anyActivityFailed` \u2014 reporting\n would count healthy loops as failures.\n- **Prefer draft \u2192 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.", AUTHORING_GUIDE = `${ORIENTATION}
74
105
 
75
106
  ## Examples
76
107
 
@@ -86,13 +117,13 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
86
117
  `, LIST_CAP = 25, TOOL_DESCRIPTORS = [
87
118
  {
88
119
  name: "list_workflow_instances",
89
- description: "List workflow instances in the configured workspace. Use this when you need to find a workflow but don't already know its instance id, or to survey what's in flight. Returns a compact summary \u2014 id, workflow type id and human-readable `workflowTitle`, current stage, whether the instance is done, and (when the workflow has a conventional subject document) a `subject` field with the subject doc's ref and title. Use `workflowTitle` when the user names the workflow by type (e.g. \"article reviews\") and `subject.title` when they name a specific in-flight instance by what it's about (e.g. \"the article-review about pricing\"). Capped at " + String(25) + " results. Do NOT use this to inspect a single known instance \u2014 use get_workflow_state for that, the response will be richer.",
120
+ description: "List workflow instances in the configured workspace. Use this when you need to find a workflow but don't already know its instance id, or to survey what's in flight. Returns a compact summary \u2014 id, `definition` (the workflow definition's `name`) and human-readable `workflowTitle`, current stage, whether the instance is done, and (when the workflow has a conventional subject document) a `subject` field with the subject doc's ref and title. Use `workflowTitle` when the user names the workflow by type (e.g. \"article reviews\") and `subject.title` when they name a specific in-flight instance by what it's about (e.g. \"the article-review about pricing\"). Capped at " + String(25) + " results. Do NOT use this to inspect a single known instance \u2014 use get_workflow_state for that, the response will be richer.",
90
121
  input_schema: {
91
122
  type: "object",
92
123
  properties: {
93
124
  definition: {
94
125
  type: "string",
95
- description: "Optional. Restrict to instances of this workflow definition (e.g. 'article-review')."
126
+ description: "Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review')."
96
127
  },
97
128
  status: {
98
129
  type: "string",
@@ -150,6 +181,11 @@ ${JSON.stringify(reviewLoopExample, null, 2)}
150
181
  action: {
151
182
  type: "string",
152
183
  description: "The id of the action on that activity. Must be one of the actions listed as allowed=true on the activity."
184
+ },
185
+ params: {
186
+ type: "object",
187
+ 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.`,
188
+ additionalProperties: !0
153
189
  }
154
190
  },
155
191
  required: ["instance_id", "activity", "action"],
@@ -235,7 +271,11 @@ function projectSummary(doc, subjectTitles) {
235
271
  ...subject !== void 0 ? { subject } : {}
236
272
  };
237
273
  }
238
- function projectState(instance, evaluation, subjectTitles) {
274
+ function projectState({
275
+ instance,
276
+ evaluation,
277
+ subjectTitles
278
+ }) {
239
279
  const workflowTitle = JSON.parse(instance.definitionSnapshot).title, stageTitle = evaluation.currentStage.stage.title, activities = evaluation.currentStage.activities.map((te) => {
240
280
  const actions = te.actions.map((ae) => {
241
281
  const verdict = workflowEngine.actionVerdict(te, ae);
@@ -279,15 +319,21 @@ function isInstanceDone(instance) {
279
319
  return !1;
280
320
  }
281
321
  }
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
322
  function formatDisabledReason(reason) {
290
- return DISABLED_REASON_TEXT[reason.kind] ?? reason.kind.replace(/-/g, " ");
323
+ switch (reason.kind) {
324
+ case "filter-failed":
325
+ return "action's filter condition did not hold for this actor";
326
+ case "activity-not-active":
327
+ return "activity is not currently active";
328
+ case "stage-terminal":
329
+ return "stage is terminal \u2014 no further actions possible";
330
+ case "mutation-guard-denied":
331
+ return `blocked by mutation guard(s): ${workflowEngine.deniedGuardLabels(reason.denied).join(", ")}`;
332
+ case "instance-completed":
333
+ return "instance has already completed";
334
+ case "requirements-unmet":
335
+ return "activity's declared requirements are not yet satisfied";
336
+ }
291
337
  }
292
338
  function summariseHistoryEntry(entry) {
293
339
  switch (entry._type) {
@@ -352,11 +398,11 @@ function buildTools(engine, options = {}) {
352
398
  },
353
399
  get_workflow_state: async (rawInput) => {
354
400
  const input = parseInstanceIdInput(rawInput, "get_workflow_state");
355
- return getInstanceState(engine, input.instance_id, accessOverride);
401
+ return getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
356
402
  },
357
403
  diagnose_workflow: async (rawInput) => {
358
404
  const input = parseInstanceIdInput(rawInput, "diagnose_workflow");
359
- return getInstanceDiagnosis(engine, input.instance_id, accessOverride);
405
+ return getInstanceDiagnosis({ engine, instanceId: input.instance_id, accessOverride });
360
406
  },
361
407
  fire_action: async (rawInput) => {
362
408
  const input = parseFireActionInput(rawInput);
@@ -364,8 +410,9 @@ function buildTools(engine, options = {}) {
364
410
  instanceId: input.instance_id,
365
411
  activity: input.activity,
366
412
  action: input.action,
413
+ ...input.params !== void 0 ? { params: input.params } : {},
367
414
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
368
- }), getInstanceState(engine, input.instance_id, accessOverride);
415
+ }), getInstanceState({ engine, instanceId: input.instance_id, accessOverride });
369
416
  },
370
417
  // Authoring tools — engine-independent: they teach and check the DSL, they
371
418
  // don't touch the lake or an instance.
@@ -387,33 +434,44 @@ function parseListInput(raw) {
387
434
  const obj = raw, status = obj.status === "done" || obj.status === "any" ? obj.status : "in_flight", definition = typeof obj.definition == "string" ? obj.definition : void 0;
388
435
  return definition !== void 0 ? { definition, status } : { status };
389
436
  }
390
- function parseValidateInput(raw) {
437
+ function asObject(raw, tool) {
391
438
  if (raw === null || typeof raw != "object")
392
- throw new Error("validate_workflow_definition: input must be an object");
393
- const obj = raw;
439
+ throw new Error(`${tool}: input must be an object`);
440
+ return raw;
441
+ }
442
+ function requireString({
443
+ obj,
444
+ key,
445
+ tool
446
+ }) {
447
+ const value = obj[key];
448
+ if (typeof value != "string" || value === "")
449
+ throw new Error(`${tool}: ${key} is required`);
450
+ return value;
451
+ }
452
+ function parseValidateInput(raw) {
453
+ const obj = asObject(raw, "validate_workflow_definition");
394
454
  if (obj.definition === null || typeof obj.definition != "object")
395
455
  throw new Error("validate_workflow_definition: `definition` is required and must be an object");
396
456
  return { definition: obj.definition };
397
457
  }
398
458
  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 };
459
+ const obj = asObject(raw, tool);
460
+ return { instance_id: requireString({ obj, key: "instance_id", tool }) };
405
461
  }
406
462
  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`);
463
+ const obj = asObject(raw, "fire_action");
464
+ let params;
465
+ if (obj.params !== void 0) {
466
+ if (obj.params === null || typeof obj.params != "object" || Array.isArray(obj.params))
467
+ throw new Error("fire_action: params must be an object");
468
+ params = obj.params;
469
+ }
413
470
  return {
414
- instance_id: obj.instance_id,
415
- activity: obj.activity,
416
- action: obj.action
471
+ instance_id: requireString({ obj, key: "instance_id", tool: "fire_action" }),
472
+ activity: requireString({ obj, key: "activity", tool: "fire_action" }),
473
+ action: requireString({ obj, key: "action", tool: "fire_action" }),
474
+ ...params !== void 0 ? { params } : {}
417
475
  };
418
476
  }
419
477
  async function listInstances(engine, input) {
@@ -429,14 +487,22 @@ async function listInstances(engine, input) {
429
487
  instances: filtered.map((doc) => projectSummary(doc, subjectTitles))
430
488
  };
431
489
  }
432
- async function getInstanceState(engine, instanceId, accessOverride) {
433
- const evaluation = await engine.evaluateInstance({
490
+ async function getInstanceState({
491
+ engine,
492
+ instanceId,
493
+ accessOverride
494
+ }) {
495
+ const evaluation = await engine.evaluate({
434
496
  instanceId,
435
497
  ...accessOverride !== void 0 ? { access: accessOverride } : {}
436
498
  }), instance = await engine.getInstance({ instanceId }), subjectTitles = await fetchSubjectTitles(engine, [instance]);
437
- return projectState(instance, evaluation, subjectTitles);
499
+ return projectState({ instance, evaluation, subjectTitles });
438
500
  }
439
- async function getInstanceDiagnosis(engine, instanceId, accessOverride) {
501
+ async function getInstanceDiagnosis({
502
+ engine,
503
+ instanceId,
504
+ accessOverride
505
+ }) {
440
506
  const { diagnosis, remediations } = await engine.diagnose({
441
507
  instanceId,
442
508
  ...accessOverride !== void 0 ? { access: accessOverride } : {}