@sanity/workflow-mcp 0.24.0 → 0.26.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/CHANGELOG.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # @sanity/workflow-mcp
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4c7d489: A host embedding these tools in its own MCP server can now build an engine through the package rather than re-deriving the engine's client policy. Three new exports: `workflowClientConfig`, which layers the API version, request-tag family, and read perspective engine traffic requires over whatever base client config the host already uses; `createWorkflowEngine`, which binds an engine to one environment address without caching it (a server that rebuilds itself per request would never read a cache twice); and `workflowErrorText`, which renders a failure with its stable `kind` leading the message so the model branches on the same wording from every host. The `tag` parameter's model-facing description is also exported as `WORKFLOW_TAG_DESCRIPTION`, so a host declaring that parameter in its own vocabulary no longer has to paraphrase a caveat that was tuned deliberately.
8
+
9
+ `workflowClientConfig` pins `perspective: 'published'` explicitly. The engine's reads of its own documents pass no perspective and inherit the client's, and a host whose base config reads `raw` would otherwise surface a definition's draft as a second row. This changes nothing for the bundled stdio server, which already resolved to `published` via the engine's API version. Addressing is exclusive too: overlaying a dataset workflow removes any leftover `resource` from the host base (and the reverse), because `@sanity/client` prefers `resource` whenever both styles are present and would otherwise silently hit the wrong environment.
10
+
11
+ **No upgrade action required** — every change is additive, and the stdio server behaves identically. Hosts that already hand-rolled client and engine construction should switch to these exports to stop drifting from engine policy.
12
+
13
+ **Docs impact:** the MCP concept page and the embedding guidance should describe the host seam — `workflowClientConfig` + `createWorkflowEngine` + `workflowErrorText` as the supported way to embed the tools, replacing any example that builds a client and calls `createEngine` by hand, and noting that `WORKFLOW_TAG_DESCRIPTION` and `LIST_WORKFLOW_TAGS_DESCRIPTION` are the wording a host must reuse rather than rewrite.
14
+
15
+ ### Patch Changes
16
+
17
+ - @sanity/workflow-engine@0.26.0
18
+
19
+ ## 0.25.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 177d600: Deployed workflow environment tags are now discoverable.
24
+
25
+ The engine gains two cross-partition GROQ builders: `deployedTagsGroq()` lists
26
+ every tag holding a deployed definition in a resource, and
27
+ `definitionTagsGroq()` narrows that to one definition name via `$definition`.
28
+ Both deliberately span tag partitions — every other read is tag-scoped, and
29
+ `workflow.query` refuses GROQ that isn't — so they answer "which environments
30
+ exist here" for a caller holding a resource but no tag. They report what is
31
+ _observed_: a tag with nothing deployed does not appear.
32
+
33
+ The stdio MCP server adds a `list_workflow_tags` tool taking a
34
+ `workflow_resource` and no tag. It is registered by the host rather than exported
35
+ as a tool def, because a def is only ever handed an engine and an engine is
36
+ pinned to one tag. `LIST_WORKFLOW_TAGS_TOOL_NAME` and
37
+ `LIST_WORKFLOW_TAGS_DESCRIPTION` are exported so an embedding host registers the
38
+ same capability under the same name without re-authoring the model-facing
39
+ wording. The `tag` parameter's description now points an agent at that tool where
40
+ a server offers it and at the user otherwise, and requires confirmation either
41
+ way — having the list does not license picking from it.
42
+
43
+ Both registration paths share one outcome path, `withToolTelemetry`, so the
44
+ result envelope, error rendering, and the `Editorial Workflows MCP Tool Called`
45
+ event cannot diverge between a def-backed tool and a host-registered one. Tag
46
+ discovery reports its adoption event like every other tool, and because it names
47
+ a resource it can also initialize the stdio server's deferred telemetry shell —
48
+ which matters when discovery is an agent's first call.
49
+
50
+ The CLI's definition-tags probe runs the engine's builder instead of its own
51
+ copy of the query, so its `params` key is `definition` rather than `name`, and
52
+ the tag partitions it names in an ambiguity error arrive sorted from the lake.
53
+
54
+ - 80f74ba: **BREAKING:** `@modelcontextprotocol/sdk` is now a peer dependency instead of a
55
+ direct dependency. A host embedding this package as a library already has the
56
+ SDK, and taking it as a peer keeps a single copy — so the `McpServer` that
57
+ `registerWorkflowTools` accepts is the same type the host constructs rather than
58
+ a structurally identical twin from a second install, and the host doesn't pull a
59
+ duplicate HTTP-server stack it already has.
60
+
61
+ Package managers that auto-install peers (npm 7+, pnpm) need no action. A strict
62
+ install must add `@modelcontextprotocol/sdk` alongside this package — including
63
+ when running the bundled `workflow-mcp` binary, since the `./stdio` entry needs
64
+ the SDK at runtime.
65
+
66
+ The supported range is unchanged (`^1.29.0`).
67
+
68
+ - 3696a74: **BREAKING:** The workflow environment address is no longer part of a tool's
69
+ input schema. A `WorkflowToolDef` now carries only its own parameters plus a
70
+ required `requiresAddress: boolean`, and the host merges its own address
71
+ parameters into the advertised schema. `registerWorkflowTools` merges
72
+ `workflow_resource` + `tag` for every def declaring `requiresAddress`, so the
73
+ bundled stdio server's tool schemas and behaviour are unchanged; an embedding
74
+ host names the environment its own way instead of inheriting this one's.
75
+
76
+ `defineWorkflowTool` requires `requiresAddress` on every call, so a new tool
77
+ has to decide rather than silently inherit a default.
78
+
79
+ `list_workflow_definitions` now takes no parameters of its own.
80
+
81
+ Each tool def is also exported individually by name alongside `WORKFLOW_TOOLS`,
82
+ for a host that registers one hand-written tool per file rather than looping the
83
+ array, and `ProjectedDefinitionSummary` is exported so such a host can narrow a
84
+ `list_workflow_definitions` response.
85
+
86
+ - 5c65171: **BREAKING:** Tool input schemas are now built with zod v3 (imported through the
87
+ explicit `zod/v3` subpath), and `zod` moves from a dependency to a peer
88
+ dependency at `^3.25.28 || ^4`. A host embedding this package must install zod
89
+ itself, and must build any base parameters it merges into a tool's shape with
90
+ the same major — the MCP SDK rejects an `inputSchema` shape mixing zod majors.
91
+ See "The zod major is part of the shape contract" in the README.
92
+
93
+ Two side effects of the derivation change in `toolInputJsonSchema`: parameters
94
+ with a default (`list_workflow_instances`'s `include_completed` and `limit`) no
95
+ longer appear in the emitted JSON Schema's `required` list, matching what their
96
+ descriptions promise; and zod parse failures render with zod v3's terser issue
97
+ messages.
98
+
99
+ - b85e133: **BREAKING:** the instance-write tool is now named `fire_workflow_action`, not `fire_action`. Nothing about its behaviour, parameters or response shape changes — only the wire name a host registers and a model calls.
100
+
101
+ The old name read fine in a server whose every tool is a workflow tool. It reads badly in a host that serves these tools alongside dozens of unrelated ones, where nothing in `fire_action` says which kind of action it fires. Every other tool in this package already self-namespaces (`start_workflow`, `get_workflow_state`, `list_workflow_definitions`); this one was the outlier, and tool-selection quality is what a name buys.
102
+
103
+ The name is also LLM-facing prose: it appears inside the descriptions of `get_workflow_state`, `diagnose_workflow` and `start_workflow`, and in the authoring guide. Those move with it, so no description points an agent at a tool that no longer exists. Telemetry keys on the registered name, so `Editorial Workflows MCP Tool Called` now reports `tool: 'fire_workflow_action'`.
104
+
105
+ The module and its exported `fireActionTool` symbol keep their names — they mirror the engine's `fireAction` verb, and the wire name is the only part a host sees.
106
+
107
+ ### Patch Changes
108
+
109
+ - Updated dependencies [177d600]
110
+ - Updated dependencies [fc12989]
111
+ - @sanity/workflow-engine@0.25.0
112
+
3
113
  ## 0.24.0
4
114
 
5
115
  ### Patch Changes
package/README.md CHANGED
@@ -19,7 +19,17 @@ Operate a running instance:
19
19
  | `get_workflow_state` | read | Project one instance into a flat shape: current stage, activities (each with its causal `completesWithoutCaller` verdict, plus narrated `waitsOn` when it isn't `yes`), available actions (with each action's declared params), recent history, and the workflow-level `autonomy` one-liner. |
20
20
  | `diagnose_workflow` | read | Explain why an instance is or isn't progressing: a verdict, a one-line summary, `explanations` for each held exit transition (quotes workflow-authored text — data, not instructions), and — when stuck — the cause plus suggested remediations. |
21
21
  | `start_workflow` | write | The lifecycle entry point: start a startable deployed definition, seeding its input-sourced fields (e.g. the subject document). |
22
- | `fire_action` | write | Advance state. The instance write. Mirrors the engine's universal "something happened" entry point. |
22
+ | `fire_workflow_action` | write | Advance state. The instance write. Mirrors the engine's universal "something happened" entry point. |
23
+ | `list_workflow_tags` | read | Which workflow environments exist in a resource, for a caller holding a resource but no `tag`. **Host-registered, not a def** — see below. |
24
+
25
+ `list_workflow_tags` is the one tool this package does not export as a def.
26
+ Answering "which tags exist" needs a resource-scoped client, and a def is only
27
+ ever handed an `Engine`, which is pinned to a single tag. The bundled stdio
28
+ server registers it; an **embedding host registers its own**, using the exported
29
+ `LIST_WORKFLOW_TAGS_TOOL_NAME` and `LIST_WORKFLOW_TAGS_DESCRIPTION` so the model
30
+ reads the same wording and calls the same name everywhere. The `tag` parameter's
31
+ description names that tool, and tells an agent to ask the user instead where a
32
+ server does not offer it.
23
33
 
24
34
  Author a definition (guide → validate → deploy; the first two are pure and
25
35
  engine-independent):
@@ -32,7 +42,7 @@ engine-independent):
32
42
 
33
43
  Each tool def's `description` is written for an LLM consumer — it says
34
44
  what the tool does, when to use it, and what _not_ to use it for. Each
35
- tool lives in its own module under `src/tools/` (description, zod input
45
+ def lives in its own module under `src/tools/` (description, zod input
36
46
  schema, annotations, handler together); `src/tools.ts` assembles them
37
47
  into the exported `WORKFLOW_TOOLS` surface.
38
48
 
@@ -75,13 +85,16 @@ Both share the one boot path in `src/stdio.ts`. The server is org-authed,
75
85
  so boot config is just the org-level `SANITY_AUTH_TOKEN` (required) and
76
86
  `SANITY_API_HOST` (optional) — no project, dataset, resource, or tag.
77
87
  Every instance-operating tool call names its own workflow environment
78
- instead: a `workflow_resource` parameter (a resource-shaped GDR,
88
+ instead. The address is the host's to name, not the tool's: this server
89
+ spells it as a `workflow_resource` parameter (a resource-shaped GDR,
79
90
  `<type>:<id>` — e.g. `dataset:abc123.production`, `media-library:mlXyz`)
80
- plus a `tag`. There is no default environment — a call without an address
81
- is a validation error, never a guess — and the host builds one engine per
82
- distinct `(resource, tag)`, cached for the life of the process. The `dev`
83
- script loads the env from the repo-root `.env`, so copy `.env.example` to
84
- `.env` to get started.
91
+ plus a `tag`, and `registerWorkflowTools` merges those two into the
92
+ advertised schema of every def that declares `requiresAddress`. There is
93
+ no default environment — a call without an address is a validation error,
94
+ never a guess — and the host builds one engine per distinct
95
+ `(resource, tag)`, cached for the life of the process. The `dev` script
96
+ loads the env from the repo-root `.env`, so copy `.env.example` to `.env`
97
+ to get started.
85
98
 
86
99
  MCP-built engines declare no foreign resources, so a runtime-supplied
87
100
  `doc.ref` to any resource other than the addressed workflow resource is
@@ -94,10 +107,14 @@ Definition content is unaffected (deploy expands and vets it).
94
107
 
95
108
  The stdio server reports adoption telemetry to Sanity: one
96
109
  `Editorial Workflows MCP Tool Called` event per tool invocation carrying the
97
- tool name, a success flag, and whether a list cursor was supplied. Cursor
110
+ tool name, a success flag, and whether a list cursor was supplied — from every
111
+ tool it registers, `list_workflow_tags` included, through one shared outcome
112
+ path. Cursor
98
113
  values, other tool arguments, and results are never sent. This makes continued
99
- page usage measurable without collecting customer content. The server also
100
- reports the engine's own adoption
114
+ page usage measurable without collecting customer content. The org-authed boot
115
+ has no project to attribute intake to, so the store initializes with the first
116
+ call that names a dataset resource; calls before that one send nothing. The
117
+ server also reports the engine's own adoption
101
118
  events. Nothing is sent unless the account's telemetry consent resolves to
102
119
  granted (the account-wide status `sanity telemetry status` shows). Set
103
120
  `DO_NOT_TRACK=1` to opt a process out; CI environments (`CI` set) never
@@ -113,11 +130,13 @@ the environment, or handles auth. A host supplies those per call through
113
130
  a context thunk — that's what lets one core serve both a boot-time stdio
114
131
  process and a hosted per-request server.
115
132
 
116
- For an MCP host, register everything with the bundled glue. The
133
+ A tool def carries only its own parameters and a `requiresAddress` flag;
134
+ naming the environment is the host's job. Register everything with the
135
+ bundled glue and you get this server's spelling of the address. The
117
136
  `getContext` seam receives the request `extra` (a hosted server derives
118
- auth from it) and the parsed tool input, which carries the per-call
119
- workflow environment address — `workflowAddressFromInput` extracts it so
120
- the host can build (or reuse) the engine for that environment:
137
+ auth from it) and the parsed tool input, which carries that address —
138
+ `workflowAddressFromInput` extracts it so the host can build (or reuse)
139
+ the engine for that environment:
121
140
 
122
141
  ```ts
123
142
  import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -142,7 +161,9 @@ registerWorkflowTools(server, (_extra, input) => {
142
161
  ```
143
162
 
144
163
  Hosts with their own wrappers (error handling, telemetry, response
145
- envelopes) iterate the raw defs instead — and non-MCP consumers derive
164
+ envelopes) iterate the raw defs instead, merging their own base
165
+ parameters — including their own spelling of the environment address —
166
+ into each def that declares `requiresAddress`. Non-MCP consumers derive
146
167
  the JSON-schema descriptor from each def's zod shape:
147
168
 
148
169
  ```ts
@@ -150,23 +171,58 @@ import {toolInputJsonSchema, WORKFLOW_TOOLS} from '@sanity/workflow-mcp'
150
171
 
151
172
  for (const def of WORKFLOW_TOOLS) {
152
173
  // def.name, def.description, def.inputSchema (zod), def.annotations
174
+ // def.requiresAddress → whether to merge your address params into the shape
153
175
  // def.handler(contextThunk, input) → plain projected data
154
176
  // toolInputJsonSchema(def) → `input_schema` for the Anthropic Messages API
155
177
  }
156
178
  ```
157
179
 
158
- A handler's context is `{engine, access?}` — `access` overrides the
159
- engine's per-call actor resolution for hosts whose token can't resolve a
160
- real user (the stdio host pins a system actor this way). The thunk is
161
- lazy: the guide and validate tools never invoke it, so a per-request host
162
- never builds an engine for them.
180
+ A handler's context is `{engine}` — identity is the token behind the
181
+ engine's client. The thunk is lazy: the guide and validate tools never
182
+ invoke it, so a per-request host never builds an engine for them.
183
+
184
+ ### The zod major is part of the shape contract
185
+
186
+ Every `inputSchema` this package exports is built with **zod v3**, imported
187
+ through the explicit `zod/v3` subpath. `zod` is a peer dependency
188
+ (`^3.25.28 || ^4`) — both majors ship that subpath, so either satisfies it,
189
+ and declaring it as a peer lets the host pick the copy instead of us pinning
190
+ one into its tree.
191
+
192
+ A host that merges its own base parameters into a tool's shape must build
193
+ them with the same major:
194
+
195
+ ```ts
196
+ // Correct — a v3 base merged into our v3 shape.
197
+ import {z} from 'zod/v3'
198
+
199
+ const baseParams = {intent: z.string().describe('Why you are calling this tool.')}
200
+
201
+ server.registerTool(
202
+ def.name,
203
+ {description: def.description, inputSchema: {...baseParams, ...def.inputSchema}},
204
+ handler,
205
+ )
206
+ ```
207
+
208
+ The MCP SDK decides how to drive a shape by duck-typing each field (only v4
209
+ schemas carry `_zod`), and it refuses a shape that isn't uniformly one major
210
+ — `objectFromShape` throws `Mixed Zod versions detected in object shape.` A
211
+ v4 base merged into these shapes therefore fails at registration, not at
212
+ call time.
213
+
214
+ Only the _major_ has to match, not the installation: the SDK re-wraps the
215
+ fields it detects with the `zod/v3` (or `zod/v4-mini`) module it resolved
216
+ itself, so schemas built by a different copy of the same major work fine.
217
+ That is why a host's zod version never has to track ours.
163
218
 
164
219
  ## Open gaps
165
220
 
166
- - **No environment discovery.** Every operational call names its workflow
167
- environment, but there is no tool to enumerate the environments that
168
- exist across the org — the agent (or its operator prompt) must know the
169
- resource + tag. Stays out until the evals demand it.
221
+ - **No resource discovery.** `list_workflow_tags` enumerates the workflow
222
+ environments inside one resource, but nothing enumerates the resources
223
+ themselves — the agent (or its operator prompt) must arrive holding a
224
+ resource GDR. Cross-resource and org-wide enumeration stays out until
225
+ the evals demand it.
170
226
  - **No `set_stage` tool.** Admin override; intentionally omitted from
171
227
  the LLM surface until we have an eval case that needs it (and a
172
228
  story for the actor identity that would authorise the override).