@sanity/workflow-mcp 0.5.1 → 0.7.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/README.md CHANGED
@@ -11,23 +11,30 @@ the tools the evals drive, plus a stdio MCP entry point for real MCP clients.
11
11
 
12
12
  Operate a running instance:
13
13
 
14
- | Tool | Read or write | What it's for |
15
- | ------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
16
- | `list_workflow_instances` | read | Discover what's running. Filterable, capped. |
17
- | `get_workflow_state` | read | Project one instance into a flat shape: current stage, activities, available actions, recent history. |
18
- | `diagnose_workflow` | read | Explain why an instance is or isn't progressing: a verdict, a one-line summary, and when stuck the cause plus suggested remediations. |
19
- | `fire_action` | write | Advance state. The only write. Mirrors the engine's universal "something happened" entry point. |
20
-
21
- Author a definition (both pure, engine-independent, neither deploys):
22
-
23
- | Tool | Read or write | What it's for |
24
- | ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
25
- | `get_workflow_authoring_guide` | read | Return the DSL guide an agent reads before authoring — shape, GROQ built-ins, sugars, modeling defaults, and two worked JSON examples. |
26
- | `validate_workflow_definition` | read | Check an authored definition the way deploy does (structure + invariants + GROQ). Returns the desugared definition or a path-prefixed error list. |
27
-
28
- Each descriptor's `description` field is written for an LLM consumer
29
- it says what the tool does, when to use it, and what _not_ to use
30
- it for. See `src/tools.ts`.
14
+ | Tool | Read or write | What it's for |
15
+ | --------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
16
+ | `list_workflow_definitions` | read | The catalogue: which workflow types are deployed (latest version each), and whether they're startable. |
17
+ | `get_workflow_definition` | read | Read one deployed definition's content, envelope-stripped redeployable as-is, so an agent can iterate on a deployed workflow (deploys are create-only). |
18
+ | `list_workflow_instances` | read | Discover what's running. Filterable (`definition`, `document`, `include_completed`in-flight by default), capped. |
19
+ | `get_workflow_state` | read | Project one instance into a flat shape: current stage, activities, available actions (with each action's declared params), recent history. |
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
+ | `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. |
23
+
24
+ Author a definition (guide validate → deploy; the first two are pure and
25
+ engine-independent):
26
+
27
+ | Tool | Read or write | What it's for |
28
+ | ------------------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `get_workflow_authoring_guide` | read | Return the DSL guide an agent reads before authoring — shape, GROQ built-ins, sugars, modeling defaults, and two worked JSON examples. |
30
+ | `validate_workflow_definition` | read | Check authored definitions (a `definitions` batch) the way deploy does (structure + invariants + GROQ). Returns `{valid, results}` — per definition, the desugared form or a path-prefixed error list. |
31
+ | `deploy_workflow_definition` | write | Publish validated definitions into an addressed environment — one `definitions` batch per call (children deploy before the parents that spawn them). Create-only and content-addressed: identical content is an `unchanged` no-op, any change mints the next version. |
32
+
33
+ Each tool def's `description` is written for an LLM consumer — it says
34
+ 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
36
+ schema, annotations, handler together); `src/tools.ts` assembles them
37
+ into the exported `WORKFLOW_TOOLS` surface.
31
38
 
32
39
  ## Why this surface, why no more
33
40
 
@@ -36,9 +43,17 @@ the verbs. An MCP is consumed by an LLM that figures out which tool
36
43
  to use from prose alone. Adding tools beyond the minimum increases
37
44
  the search space the LLM has to navigate. We stay narrow until the
38
45
  evals tell us the LLM is reaching for something we don't expose. The
39
- authoring pair earns its place because authoring-from-a-description is
40
- eval-backed; the agent generates the definition and self-corrects against
41
- `validate_workflow_definition` (deploy stays a deliberate human step).
46
+ authoring trio earns its place because authoring-from-a-description is
47
+ eval-backed; the agent generates the definitions, self-corrects against
48
+ `validate_workflow_definition`, then publishes with
49
+ `deploy_workflow_definition` — safe to hand an agent because deploy is
50
+ create-only: it can never patch or clobber what's already deployed, and
51
+ running instances keep the definition version they started under.
52
+ `get_workflow_definition` closes the loop that create-only would
53
+ otherwise leave open — iterating on a deployed workflow means reading
54
+ it back, modifying, and deploying the next version — and
55
+ `start_workflow` is the lifecycle entry point the catalogue's
56
+ `startable` flag advertises.
42
57
 
43
58
  ## Run the stdio server
44
59
 
@@ -56,44 +71,98 @@ it's `bin/run.js`, and once the package is published this is what
56
71
  node packages/workflow-mcp/bin/run.js
57
72
  ```
58
73
 
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.)
74
+ Both share the one boot path in `src/stdio.ts`. The server is org-authed,
75
+ so boot config is just the org-level `SANITY_AUTH_TOKEN` (required) and
76
+ `SANITY_API_HOST` (optional) no project, dataset, resource, or tag.
77
+ Every instance-operating tool call names its own workflow environment
78
+ instead: a `workflow_resource` parameter (a resource-shaped GDR,
79
+ `<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.
85
+
86
+ ### Telemetry
87
+
88
+ The stdio server reports adoption telemetry to Sanity: one
89
+ `Editorial Workflows MCP Tool Called` event per tool invocation carrying the
90
+ tool name and a success
91
+ flag — never tool arguments or results — plus the engine's own adoption
92
+ events. Nothing is sent unless the account's telemetry consent resolves to
93
+ granted (the account-wide status `sanity telemetry status` shows). Set
94
+ `DO_NOT_TRACK=1` to opt a process out; CI environments (`CI` set) never
95
+ send. Separately from consent, every Content Lake request the server issues
96
+ carries a request tag under the `sanity.workflows-mcp` prefix (composing as
97
+ `sanity.workflows-mcp.<op>`) for request-log attribution — deliberately outside
98
+ the zero-rated `sanity.workflows.*` family, so agent-driven traffic bills.
66
99
 
67
100
  ## API
68
101
 
102
+ The package is a host-neutral library: it never builds an engine, reads
103
+ the environment, or handles auth. A host supplies those per call through
104
+ a context thunk — that's what lets one core serve both a boot-time stdio
105
+ process and a hosted per-request server.
106
+
107
+ For an MCP host, register everything with the bundled glue. The
108
+ `getContext` seam receives the request `extra` (a hosted server derives
109
+ auth from it) and the parsed tool input, which carries the per-call
110
+ workflow environment address — `workflowAddressFromInput` extracts it so
111
+ the host can build (or reuse) the engine for that environment:
112
+
69
113
  ```ts
70
- import {createEngine} from '@sanity/workflow-engine'
71
- import {buildTools} from '@sanity/workflow-mcp'
114
+ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
115
+ import {createEngine, resourceGdr, type Engine} from '@sanity/workflow-engine'
116
+ import {registerWorkflowTools, workflowAddressFromInput} from '@sanity/workflow-mcp'
117
+
118
+ const server = new McpServer({name: 'my-host', version: '1.0.0'})
119
+
120
+ // One engine per addressed environment, reused across calls.
121
+ // clientFor(resource): your host's @sanity/client for that resource —
122
+ // the bundled stdio host's version lives in `src/engine-cache.ts`.
123
+ const engines = new Map<string, Engine>()
124
+
125
+ registerWorkflowTools(server, (_extra, input) => {
126
+ const {workflowResource, tag} = workflowAddressFromInput(input)
127
+ const key = `${resourceGdr(workflowResource)} ${tag}`
128
+ const engine =
129
+ engines.get(key) ?? createEngine({client: clientFor(workflowResource), workflowResource, tag})
130
+ engines.set(key, engine)
131
+ return {engine}
132
+ })
133
+ ```
134
+
135
+ Hosts with their own wrappers (error handling, telemetry, response
136
+ envelopes) iterate the raw defs instead — and non-MCP consumers derive
137
+ the JSON-schema descriptor from each def's zod shape:
72
138
 
73
- const engine = createEngine({client, workflowResource, tag})
74
- const {descriptors, impls} = buildTools(engine)
139
+ ```ts
140
+ import {toolInputJsonSchema, WORKFLOW_TOOLS} from '@sanity/workflow-mcp'
75
141
 
76
- // descriptors feed to Anthropic API's `tools:` param, or to an MCP
77
- // `tools/list` handler.
78
- // impls → call from a `tools/call` handler, or from the eval
79
- // harness directly (in-process, no transport).
142
+ for (const def of WORKFLOW_TOOLS) {
143
+ // def.name, def.description, def.inputSchema (zod), def.annotations
144
+ // def.handler(contextThunk, input) plain projected data
145
+ // toolInputJsonSchema(def) `input_schema` for the Anthropic Messages API
146
+ }
80
147
  ```
81
148
 
149
+ A handler's context is `{engine, access?}` — `access` overrides the
150
+ engine's per-call actor resolution for hosts whose token can't resolve a
151
+ real user (the stdio host pins a system actor this way). The thunk is
152
+ lazy: the guide and validate tools never invoke it, so a per-request host
153
+ never builds an engine for them.
154
+
82
155
  ## Open gaps
83
156
 
157
+ - **No environment discovery.** Every operational call names its workflow
158
+ environment, but there is no tool to enumerate the environments that
159
+ exist across the org — the agent (or its operator prompt) must know the
160
+ resource + tag. Stays out until the evals demand it.
84
161
  - **No `set_stage` tool.** Admin override; intentionally omitted from
85
162
  the LLM surface until we have an eval case that needs it (and a
86
163
  story for the actor identity that would authorise the override).
87
- - **No `start_instance` tool.** Starting an instance is an operator
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.
91
- - **No effect completion.** Same logic — effects are queued by the
92
- engine and drained by the runtime, not by a human or LLM.
164
+ - **No effect completion.** Effects are queued by the engine and
165
+ drained by the runtime, not by a human or LLM.
93
166
  - **No subscription / streaming.** MCP supports it; we haven't needed
94
167
  it yet. Pull-based polling via `get_workflow_state` covers the eval
95
168
  cases.
96
- - **No `deploy_workflow_definition`.** `validate_workflow_definition` is
97
- the authoring tool; a human deploys the validated definition via the
98
- CLI. Deploy is a Content Lake write that inherits the actor-identity
99
- story, so it stays out of the LLM surface for now.
package/bin/run.js CHANGED
@@ -1,9 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Production entry for the published package: loads the compiled boot
3
- // path from ./dist/stdio.js (built by pkg-utils via the `./stdio`
4
- // export). The dev counterpart, bin/workflow-mcp.ts, loads the TS
5
- // source through tsx instead.
6
-
7
2
  import {runStdioServer} from '../dist/stdio.js'
8
3
 
9
4
  await runStdioServer()