@sanity/workflow-mcp 0.20.0 → 0.22.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 +100 -0
- package/README.md +5 -3
- package/dist/_chunks-es/index.js +72 -19
- package/dist/index.cjs +70 -19
- package/dist/stdio.js +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# @sanity/workflow-mcp
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [88ba4ba]
|
|
8
|
+
- @sanity/workflow-engine@0.22.0
|
|
9
|
+
|
|
10
|
+
## 0.21.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- f4bc057: **BREAKING:** `list_workflow_instances` now returns one page with `has_more` and an optional `next_cursor`; callers that need every match must continue with the cursor. The optional `limit` accepts 1–100 rows and defaults to 25.
|
|
15
|
+
|
|
16
|
+
Bound instance and definition list reads with lake-side filtering and consumer-specific projections.
|
|
17
|
+
|
|
18
|
+
Document filtering now excludes exited-stage references and includes live unresolved child-workflow references. Definition discovery selects and model-gates only the latest deployed version of each workflow name; historical versions remain stored and available to history-oriented APIs.
|
|
19
|
+
|
|
20
|
+
The MCP eval suite now verifies that agents follow list cursors to find a target beyond the first default page, including in persisted Braintrust runs.
|
|
21
|
+
|
|
22
|
+
MCP tool telemetry reports a `cursorUsed` boolean so continued list-page adoption is measurable without sending cursor values, arguments, or results.
|
|
23
|
+
|
|
24
|
+
- bcc30fe: **BREAKING:** Definition deploys through the MCP tool acknowledge data model 4
|
|
25
|
+
(`expectedMinReaderModel: 4`): deploys assert a model-4-capable reader fleet
|
|
26
|
+
and require the model-4 engine.
|
|
27
|
+
- fa9c796: **BREAKING:** Replace the singular `start.allowed` and activity requirement record with ordered, named requirement arrays.
|
|
28
|
+
|
|
29
|
+
Start readiness now accepts polymorphic `groq` and `singleSubject` nodes:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// Before
|
|
33
|
+
start: {allowed: '$fields.approved == true'}
|
|
34
|
+
|
|
35
|
+
// After
|
|
36
|
+
start: {
|
|
37
|
+
requirements: [
|
|
38
|
+
{type: 'groq', name: 'approved', title: 'Approval required', query: '$fields.approved == true'},
|
|
39
|
+
],
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use `singleSubject` instead of `$subjectHasInFlightInstance` to allow at most one in-flight run of the same definition for a subject. The requirement is definition-scoped and version-blind across deployments:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Before
|
|
47
|
+
start: {allowed: '!$subjectHasInFlightInstance'}
|
|
48
|
+
|
|
49
|
+
// After
|
|
50
|
+
start: {
|
|
51
|
+
requirements: [
|
|
52
|
+
{type: 'singleSubject', name: 'single-subject', description: 'Finish the existing run first.'},
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Activity readiness uses the same ordered descriptor model with `groq` nodes:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// Before
|
|
61
|
+
requirements: {
|
|
62
|
+
approved: 'defined($fields.approval)'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// After
|
|
66
|
+
requirements: [{type: 'groq', name: 'approved', query: 'defined($fields.approval)'}]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Requirement names must be unique within their owning array. Evaluation preserves author order and reports every unmet requirement with its `name` and optional editor-facing `title` and `description`. Fresh standalone starts enforce the requirements after validating inputs. Resuming an unfinished start and parent-owned spawning continue to bypass start requirements.
|
|
70
|
+
|
|
71
|
+
`evaluateStart()` now returns ordered `requirements` entries containing each descriptor, outcome, and GROQ insight where applicable. Its singular top-level `insight` is removed. `StartNotAllowedError.insight` is replaced by `StartNotAllowedError.unmetRequirements`. Activity evaluation likewise reports unmet requirement descriptors instead of names alone.
|
|
72
|
+
|
|
73
|
+
Rename the public start-requirement analysis helpers and constants:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// Before
|
|
77
|
+
explainStartAllowed(args)
|
|
78
|
+
unboundAllowedReads(query, fields)
|
|
79
|
+
START_ALLOWED_VARS
|
|
80
|
+
|
|
81
|
+
// After
|
|
82
|
+
explainStartRequirement(args)
|
|
83
|
+
unboundRequirementReads(query, fields)
|
|
84
|
+
START_REQUIREMENT_VARS
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`definitionsForDocument({document, subject})` becomes `definitionsForDocument({document})`. Start applicability no longer needs a separately supplied subject. The engine-owned start dataset projection exposes only `definition`, `subject`, and `completedAt`; authors no longer query raw instance storage for deduplication.
|
|
88
|
+
|
|
89
|
+
Studio pre-flights both requirement kinds against the live document session, disables starts whose requirements are unmet, and renders the authored description or title with a humanized requirement-name fallback. Controls re-enable on the same mount when a blocking run completes.
|
|
90
|
+
|
|
91
|
+
Persisted data model 4 unconditionally raises `minReaderModel` to 4 for new definitions and instances because older readers would ignore readiness arrays and could commit invalid transitions. Upgrade every reader and Function before deploying model-4 writers. Legacy deployed definitions are not normalized; prerelease environments with incompatible definitions may use `sanity-workflows nuke` before redeploying.
|
|
92
|
+
|
|
93
|
+
### Patch Changes
|
|
94
|
+
|
|
95
|
+
- Updated dependencies [d9394e5]
|
|
96
|
+
- Updated dependencies [92e28bd]
|
|
97
|
+
- Updated dependencies [f4bc057]
|
|
98
|
+
- Updated dependencies [bcc30fe]
|
|
99
|
+
- Updated dependencies [e7392af]
|
|
100
|
+
- Updated dependencies [fa9c796]
|
|
101
|
+
- @sanity/workflow-engine@0.21.0
|
|
102
|
+
|
|
3
103
|
## 0.20.0
|
|
4
104
|
|
|
5
105
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Operate a running instance:
|
|
|
15
15
|
| --------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
16
|
| `list_workflow_definitions` | read | The catalogue: which workflow types are deployed (latest version each), and whether they're startable. |
|
|
17
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),
|
|
18
|
+
| `list_workflow_instances` | read | Discover what's running. Filterable (`definition`, `document`, `include_completed` — in-flight by default), with configurable cursor pagination. |
|
|
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). |
|
|
@@ -94,8 +94,10 @@ Definition content is unaffected (deploy expands and vets it).
|
|
|
94
94
|
|
|
95
95
|
The stdio server reports adoption telemetry to Sanity: one
|
|
96
96
|
`Editorial Workflows MCP Tool Called` event per tool invocation carrying the
|
|
97
|
-
tool name and a
|
|
98
|
-
|
|
97
|
+
tool name, a success flag, and whether a list cursor was supplied. Cursor
|
|
98
|
+
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
|
|
99
101
|
events. Nothing is sent unless the account's telemetry consent resolves to
|
|
100
102
|
granted (the account-wide status `sanity telemetry status` shows). Set
|
|
101
103
|
`DO_NOT_TRACK=1` to opt a process out; CI environments (`CI` set) never
|
package/dist/_chunks-es/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTelemetryIntake, isTelemetryEnvDenied, processShellUserProperties, errorMessage, validateTag, parseResourceGdr, parseDefinitionInput, extractDocumentId, parseDefinitionSnapshot, displayTitle, autonomySummary, actionRendering, isTerminalStage, describeCondition, actionVerdict, narrateAutonomyWaits, subjectDenialLabels, deniedGuardLabels, definitionLookupGroq, assertReadableModel, unsatisfiedTransitionSummaries, describeSite,
|
|
1
|
+
import { createTelemetryIntake, isTelemetryEnvDenied, processShellUserProperties, errorMessage, validateTag, parseResourceGdr, parseDefinitionInput, extractDocumentId, parseDefinitionSnapshot, displayTitle, autonomySummary, actionRendering, isTerminalStage, describeCondition, actionVerdict, narrateAutonomyWaits, subjectDenialLabels, deniedGuardLabels, definitionLookupGroq, assertReadableModel, unsatisfiedTransitionSummaries, describeSite, latestDefinitionsGroq, startKindOf, isStartableDefinition, instancesQuery, instanceWatchesDocument, parseGdr, startRefusal, buildInitialFields, StartNotPrimedError, StartNotSettledError, validateDefinition, WorkflowError } from "@sanity/workflow-engine";
|
|
2
2
|
|
|
3
3
|
import { defineEvent, createBatchedStore, createSessionId } from "@sanity/telemetry";
|
|
4
4
|
|
|
@@ -6,10 +6,12 @@ import { z } from "zod";
|
|
|
6
6
|
|
|
7
7
|
import { defineWorkflow } from "@sanity/workflow-engine/define";
|
|
8
8
|
|
|
9
|
+
import { Buffer } from "node:buffer";
|
|
10
|
+
|
|
9
11
|
const WorkflowMcpToolCalled = defineEvent({
|
|
10
12
|
name: "Editorial Workflows MCP Tool Called",
|
|
11
|
-
version:
|
|
12
|
-
description: "An MCP tool was invoked — payload is the tool name and
|
|
13
|
+
version: 2,
|
|
14
|
+
description: "An MCP tool was invoked — payload is the tool name, success, and list-cursor usage; never arguments, cursor values, or results"
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
function createMcpTelemetry(args) {
|
|
@@ -120,7 +122,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
|
|
|
120
122
|
if (problems.length > 0) throw new Error(problems.join(`\n`));
|
|
121
123
|
const {engine: engine} = await context();
|
|
122
124
|
return engine.deployDefinitions({
|
|
123
|
-
expectedMinReaderModel:
|
|
125
|
+
expectedMinReaderModel: 4,
|
|
124
126
|
definitions: stored
|
|
125
127
|
});
|
|
126
128
|
}
|
|
@@ -332,7 +334,7 @@ async function fetchDeployedDefinition({engine: engine, definition: definition,
|
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
function isInstanceDone(instance) {
|
|
335
|
-
if (instance.completedAt
|
|
337
|
+
if (instance.completedAt != null) return !0;
|
|
336
338
|
try {
|
|
337
339
|
const stage = parseDefinitionSnapshot(instance).stages.find(s => s.name === instance.currentStage);
|
|
338
340
|
return stage !== void 0 && isTerminalStage(stage);
|
|
@@ -717,11 +719,11 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
717
719
|
readOnlyHint: !0
|
|
718
720
|
},
|
|
719
721
|
run: async context => {
|
|
720
|
-
const {engine: engine} = await context()
|
|
721
|
-
groq: definitionsListGroq("desc")
|
|
722
|
-
})).map(assertReadableModel);
|
|
722
|
+
const {engine: engine} = await context();
|
|
723
723
|
return {
|
|
724
|
-
definitions:
|
|
724
|
+
definitions: (await engine.query({
|
|
725
|
+
groq: latestDefinitionsGroq()
|
|
726
|
+
})).map(assertReadableModel).map(def => ({
|
|
725
727
|
name: def.name,
|
|
726
728
|
title: def.title,
|
|
727
729
|
...def.description !== void 0 ? {
|
|
@@ -733,14 +735,44 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
733
735
|
}))
|
|
734
736
|
};
|
|
735
737
|
}
|
|
736
|
-
}),
|
|
738
|
+
}), DEFAULT_LIST_LIMIT = 25, MAX_LIST_LIMIT = 100, cursorPayloadSchema = z.object({
|
|
739
|
+
version: z.literal(1),
|
|
740
|
+
lastChangedAt: z.string(),
|
|
741
|
+
instanceId: z.string(),
|
|
742
|
+
scope: z.string()
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
function cursorScope(input) {
|
|
746
|
+
return JSON.stringify([ input.workflow_resource, input.tag, input.definition ?? null, input.document ?? null, input.include_completed ]);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function encodeCursor(payload) {
|
|
750
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function decodeCursor(cursor, expectedScope) {
|
|
754
|
+
let decoded;
|
|
755
|
+
try {
|
|
756
|
+
decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
757
|
+
} catch {
|
|
758
|
+
throw new Error("list_workflow_instances: invalid cursor");
|
|
759
|
+
}
|
|
760
|
+
const parsed = cursorPayloadSchema.safeParse(decoded);
|
|
761
|
+
if (!parsed.success) throw new Error("list_workflow_instances: invalid cursor");
|
|
762
|
+
if (parsed.data.scope !== expectedScope) throw new Error("list_workflow_instances: cursor does not match the current filters");
|
|
763
|
+
return parsed.data;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderModel,\n workflowResource,\n definition,\n definitionSnapshot,\n fields,\n ancestors,\n subworkflows,\n stages,\n currentStage,\n lastChangedAt,\n completedAt,\n abortedAt\n}`, listWorkflowInstancesTool = defineWorkflowTool({
|
|
737
767
|
name: "list_workflow_instances",
|
|
738
|
-
description: `List workflow instances in one workflow environment. 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 — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow declares a 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").
|
|
768
|
+
description: `List workflow instances in one workflow environment. 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 — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow declares a 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"). Returns up to ${DEFAULT_LIST_LIMIT} results per page by default, ordered by most recently changed; set \`limit\` up to ${MAX_LIST_LIMIT}. Defensive document verification can leave a page underfilled. When \`has_more\` is true, call this tool again with the same filters and \`next_cursor\` as \`cursor\`; never claim the list is complete until \`has_more\` is false. Do NOT use this to inspect a single known instance — use get_workflow_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
|
|
739
769
|
inputSchema: {
|
|
740
770
|
...workflowAddressFields,
|
|
741
771
|
definition: z.string().describe("Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review').").optional(),
|
|
742
772
|
document: z.string().describe(`Optional. Only instances that reference this document — the workflow's subject or any other doc its fields point at — as a resource-qualified GDR URI (e.g. "dataset:proj:ds:article-1"). Use this to answer "which workflows are about this document?".`).superRefine(zodCheck(parseGdr)).optional(),
|
|
743
|
-
include_completed: z.boolean().describe("Optional. `true` includes completed/aborted instances. Defaults to false — in-flight instances only.").default(!1)
|
|
773
|
+
include_completed: z.boolean().describe("Optional. `true` includes completed/aborted instances. Defaults to false — in-flight instances only.").default(!1),
|
|
774
|
+
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).describe(`Optional. Results per page, from 1 to ${MAX_LIST_LIMIT}. Defaults to ${DEFAULT_LIST_LIMIT}.`).default(DEFAULT_LIST_LIMIT),
|
|
775
|
+
cursor: z.string().describe("Optional. Opaque `next_cursor` from the previous response. Reuse the same address and filters when continuing.").optional()
|
|
744
776
|
},
|
|
745
777
|
annotations: {
|
|
746
778
|
readOnlyHint: !0
|
|
@@ -757,12 +789,28 @@ const instanceIdField = z.string().min(1).describe("The workflow instance id."),
|
|
|
757
789
|
}, {query: query, params: params} = instancesQuery({
|
|
758
790
|
tag: engine.tag,
|
|
759
791
|
filter: filter
|
|
760
|
-
}), recency = `${query} | order(lastChangedAt desc)`, docs = (await engine.query({
|
|
761
|
-
groq:
|
|
762
|
-
params:
|
|
763
|
-
|
|
792
|
+
}), scope = cursorScope(input), cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, scope), recency = `${query}${cursor === void 0 ? "" : "[lastChangedAt < $cursorLastChangedAt || (lastChangedAt == $cursorLastChangedAt && _id > $cursorInstanceId)]"} | order(lastChangedAt desc, _id asc)`, docs = (await engine.query({
|
|
793
|
+
groq: `${recency}[0...$pageSize]${SUMMARY_PROJECTION}`,
|
|
794
|
+
params: {
|
|
795
|
+
...params,
|
|
796
|
+
pageSize: input.limit + 1,
|
|
797
|
+
...cursor === void 0 ? {} : {
|
|
798
|
+
cursorLastChangedAt: cursor.lastChangedAt,
|
|
799
|
+
cursorInstanceId: cursor.instanceId
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
})).map(assertReadableModel), rawPage = docs.slice(0, input.limit), page = document === void 0 ? rawPage : rawPage.filter(doc => instanceWatchesDocument(doc, document)), hasMore = docs.length > input.limit, subjectTitles = await fetchSubjectTitles(engine, page), boundary = rawPage.at(-1);
|
|
764
803
|
return {
|
|
765
|
-
instances:
|
|
804
|
+
instances: page.map(doc => projectSummary(doc, subjectTitles)),
|
|
805
|
+
has_more: hasMore,
|
|
806
|
+
...hasMore && boundary !== void 0 ? {
|
|
807
|
+
next_cursor: encodeCursor({
|
|
808
|
+
version: 1,
|
|
809
|
+
lastChangedAt: boundary.lastChangedAt,
|
|
810
|
+
instanceId: boundary._id,
|
|
811
|
+
scope: scope
|
|
812
|
+
})
|
|
813
|
+
} : {}
|
|
766
814
|
};
|
|
767
815
|
}
|
|
768
816
|
});
|
|
@@ -884,10 +932,11 @@ function registerWorkflowTools(server, getContext, options) {
|
|
|
884
932
|
}
|
|
885
933
|
|
|
886
934
|
async function runToolHandler({def: def, context: context, input: input, telemetry: telemetry}) {
|
|
887
|
-
const logCalled = success => {
|
|
935
|
+
const cursorUsed = listCursorWasSupplied(def.name, input), logCalled = success => {
|
|
888
936
|
telemetry?.log(WorkflowMcpToolCalled, {
|
|
889
937
|
tool: def.name,
|
|
890
|
-
success: success
|
|
938
|
+
success: success,
|
|
939
|
+
cursorUsed: cursorUsed
|
|
891
940
|
});
|
|
892
941
|
};
|
|
893
942
|
try {
|
|
@@ -911,4 +960,8 @@ async function runToolHandler({def: def, context: context, input: input, telemet
|
|
|
911
960
|
}
|
|
912
961
|
}
|
|
913
962
|
|
|
963
|
+
function listCursorWasSupplied(toolName, input) {
|
|
964
|
+
return toolName !== "list_workflow_instances" || typeof input != "object" || input === null ? !1 : "cursor" in input && typeof input.cursor == "string" && input.cursor.length > 0;
|
|
965
|
+
}
|
|
966
|
+
|
|
914
967
|
export { WORKFLOW_TOOLS, createMcpTelemetry, registerWorkflowTools, toolInputJsonSchema, workflowAddressFromInput };
|
package/dist/index.cjs
CHANGED
|
@@ -4,12 +4,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: !0
|
|
5
5
|
});
|
|
6
6
|
|
|
7
|
-
var workflowEngine = require("@sanity/workflow-engine"), telemetry = require("@sanity/telemetry"), zod = require("zod"), define = require("@sanity/workflow-engine/define");
|
|
7
|
+
var workflowEngine = require("@sanity/workflow-engine"), telemetry = require("@sanity/telemetry"), zod = require("zod"), define = require("@sanity/workflow-engine/define"), node_buffer = require("node:buffer");
|
|
8
8
|
|
|
9
9
|
const WorkflowMcpToolCalled = telemetry.defineEvent({
|
|
10
10
|
name: "Editorial Workflows MCP Tool Called",
|
|
11
|
-
version:
|
|
12
|
-
description: "An MCP tool was invoked — payload is the tool name and
|
|
11
|
+
version: 2,
|
|
12
|
+
description: "An MCP tool was invoked — payload is the tool name, success, and list-cursor usage; never arguments, cursor values, or results"
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
function zodCheck(validate) {
|
|
@@ -103,7 +103,7 @@ const deployWorkflowDefinitionTool = defineWorkflowTool({
|
|
|
103
103
|
if (problems.length > 0) throw new Error(problems.join(`\n`));
|
|
104
104
|
const {engine: engine} = await context();
|
|
105
105
|
return engine.deployDefinitions({
|
|
106
|
-
expectedMinReaderModel:
|
|
106
|
+
expectedMinReaderModel: 4,
|
|
107
107
|
definitions: stored
|
|
108
108
|
});
|
|
109
109
|
}
|
|
@@ -315,7 +315,7 @@ async function fetchDeployedDefinition({engine: engine, definition: definition,
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
function isInstanceDone(instance) {
|
|
318
|
-
if (instance.completedAt
|
|
318
|
+
if (instance.completedAt != null) return !0;
|
|
319
319
|
try {
|
|
320
320
|
const stage = workflowEngine.parseDefinitionSnapshot(instance).stages.find(s => s.name === instance.currentStage);
|
|
321
321
|
return stage !== void 0 && workflowEngine.isTerminalStage(stage);
|
|
@@ -700,11 +700,11 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
700
700
|
readOnlyHint: !0
|
|
701
701
|
},
|
|
702
702
|
run: async context => {
|
|
703
|
-
const {engine: engine} = await context()
|
|
704
|
-
groq: workflowEngine.definitionsListGroq("desc")
|
|
705
|
-
})).map(workflowEngine.assertReadableModel);
|
|
703
|
+
const {engine: engine} = await context();
|
|
706
704
|
return {
|
|
707
|
-
definitions:
|
|
705
|
+
definitions: (await engine.query({
|
|
706
|
+
groq: workflowEngine.latestDefinitionsGroq()
|
|
707
|
+
})).map(workflowEngine.assertReadableModel).map(def => ({
|
|
708
708
|
name: def.name,
|
|
709
709
|
title: def.title,
|
|
710
710
|
...def.description !== void 0 ? {
|
|
@@ -716,14 +716,44 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
716
716
|
}))
|
|
717
717
|
};
|
|
718
718
|
}
|
|
719
|
-
}),
|
|
719
|
+
}), DEFAULT_LIST_LIMIT = 25, MAX_LIST_LIMIT = 100, cursorPayloadSchema = zod.z.object({
|
|
720
|
+
version: zod.z.literal(1),
|
|
721
|
+
lastChangedAt: zod.z.string(),
|
|
722
|
+
instanceId: zod.z.string(),
|
|
723
|
+
scope: zod.z.string()
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
function cursorScope(input) {
|
|
727
|
+
return JSON.stringify([ input.workflow_resource, input.tag, input.definition ?? null, input.document ?? null, input.include_completed ]);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function encodeCursor(payload) {
|
|
731
|
+
return node_buffer.Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function decodeCursor(cursor, expectedScope) {
|
|
735
|
+
let decoded;
|
|
736
|
+
try {
|
|
737
|
+
decoded = JSON.parse(node_buffer.Buffer.from(cursor, "base64url").toString("utf8"));
|
|
738
|
+
} catch {
|
|
739
|
+
throw new Error("list_workflow_instances: invalid cursor");
|
|
740
|
+
}
|
|
741
|
+
const parsed = cursorPayloadSchema.safeParse(decoded);
|
|
742
|
+
if (!parsed.success) throw new Error("list_workflow_instances: invalid cursor");
|
|
743
|
+
if (parsed.data.scope !== expectedScope) throw new Error("list_workflow_instances: cursor does not match the current filters");
|
|
744
|
+
return parsed.data;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const SUMMARY_PROJECTION = `{\n _type,\n _id,\n modelVersion,\n minReaderModel,\n workflowResource,\n definition,\n definitionSnapshot,\n fields,\n ancestors,\n subworkflows,\n stages,\n currentStage,\n lastChangedAt,\n completedAt,\n abortedAt\n}`, listWorkflowInstancesTool = defineWorkflowTool({
|
|
720
748
|
name: "list_workflow_instances",
|
|
721
|
-
description: `List workflow instances in one workflow environment. 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 — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow declares a 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").
|
|
749
|
+
description: `List workflow instances in one workflow environment. 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 — id, \`definition\` (the workflow definition's \`name\`) and human-readable \`workflowTitle\`, current stage, whether the instance is done, and (when the workflow declares a 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"). Returns up to ${DEFAULT_LIST_LIMIT} results per page by default, ordered by most recently changed; set \`limit\` up to ${MAX_LIST_LIMIT}. Defensive document verification can leave a page underfilled. When \`has_more\` is true, call this tool again with the same filters and \`next_cursor\` as \`cursor\`; never claim the list is complete until \`has_more\` is false. Do NOT use this to inspect a single known instance — use get_workflow_state for that, the response will be richer. ` + UNTRUSTED_AUTHORED_DATA_NOTE,
|
|
722
750
|
inputSchema: {
|
|
723
751
|
...workflowAddressFields,
|
|
724
752
|
definition: zod.z.string().describe("Optional. Restrict to instances of this workflow definition, by its `name` (e.g. 'article-review').").optional(),
|
|
725
753
|
document: zod.z.string().describe(`Optional. Only instances that reference this document — the workflow's subject or any other doc its fields point at — as a resource-qualified GDR URI (e.g. "dataset:proj:ds:article-1"). Use this to answer "which workflows are about this document?".`).superRefine(zodCheck(workflowEngine.parseGdr)).optional(),
|
|
726
|
-
include_completed: zod.z.boolean().describe("Optional. `true` includes completed/aborted instances. Defaults to false — in-flight instances only.").default(!1)
|
|
754
|
+
include_completed: zod.z.boolean().describe("Optional. `true` includes completed/aborted instances. Defaults to false — in-flight instances only.").default(!1),
|
|
755
|
+
limit: zod.z.number().int().min(1).max(MAX_LIST_LIMIT).describe(`Optional. Results per page, from 1 to ${MAX_LIST_LIMIT}. Defaults to ${DEFAULT_LIST_LIMIT}.`).default(DEFAULT_LIST_LIMIT),
|
|
756
|
+
cursor: zod.z.string().describe("Optional. Opaque `next_cursor` from the previous response. Reuse the same address and filters when continuing.").optional()
|
|
727
757
|
},
|
|
728
758
|
annotations: {
|
|
729
759
|
readOnlyHint: !0
|
|
@@ -740,12 +770,28 @@ const instanceIdField = zod.z.string().min(1).describe("The workflow instance id
|
|
|
740
770
|
}, {query: query, params: params} = workflowEngine.instancesQuery({
|
|
741
771
|
tag: engine.tag,
|
|
742
772
|
filter: filter
|
|
743
|
-
}), recency = `${query} | order(lastChangedAt desc)`, docs = (await engine.query({
|
|
744
|
-
groq:
|
|
745
|
-
params:
|
|
746
|
-
|
|
773
|
+
}), scope = cursorScope(input), cursor = input.cursor === void 0 ? void 0 : decodeCursor(input.cursor, scope), recency = `${query}${cursor === void 0 ? "" : "[lastChangedAt < $cursorLastChangedAt || (lastChangedAt == $cursorLastChangedAt && _id > $cursorInstanceId)]"} | order(lastChangedAt desc, _id asc)`, docs = (await engine.query({
|
|
774
|
+
groq: `${recency}[0...$pageSize]${SUMMARY_PROJECTION}`,
|
|
775
|
+
params: {
|
|
776
|
+
...params,
|
|
777
|
+
pageSize: input.limit + 1,
|
|
778
|
+
...cursor === void 0 ? {} : {
|
|
779
|
+
cursorLastChangedAt: cursor.lastChangedAt,
|
|
780
|
+
cursorInstanceId: cursor.instanceId
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
})).map(workflowEngine.assertReadableModel), rawPage = docs.slice(0, input.limit), page = document === void 0 ? rawPage : rawPage.filter(doc => workflowEngine.instanceWatchesDocument(doc, document)), hasMore = docs.length > input.limit, subjectTitles = await fetchSubjectTitles(engine, page), boundary = rawPage.at(-1);
|
|
747
784
|
return {
|
|
748
|
-
instances:
|
|
785
|
+
instances: page.map(doc => projectSummary(doc, subjectTitles)),
|
|
786
|
+
has_more: hasMore,
|
|
787
|
+
...hasMore && boundary !== void 0 ? {
|
|
788
|
+
next_cursor: encodeCursor({
|
|
789
|
+
version: 1,
|
|
790
|
+
lastChangedAt: boundary.lastChangedAt,
|
|
791
|
+
instanceId: boundary._id,
|
|
792
|
+
scope: scope
|
|
793
|
+
})
|
|
794
|
+
} : {}
|
|
749
795
|
};
|
|
750
796
|
}
|
|
751
797
|
});
|
|
@@ -867,10 +913,11 @@ function registerWorkflowTools(server, getContext, options) {
|
|
|
867
913
|
}
|
|
868
914
|
|
|
869
915
|
async function runToolHandler({def: def, context: context, input: input, telemetry: telemetry2}) {
|
|
870
|
-
const logCalled = success => {
|
|
916
|
+
const cursorUsed = listCursorWasSupplied(def.name, input), logCalled = success => {
|
|
871
917
|
telemetry2?.log(WorkflowMcpToolCalled, {
|
|
872
918
|
tool: def.name,
|
|
873
|
-
success: success
|
|
919
|
+
success: success,
|
|
920
|
+
cursorUsed: cursorUsed
|
|
874
921
|
});
|
|
875
922
|
};
|
|
876
923
|
try {
|
|
@@ -894,6 +941,10 @@ async function runToolHandler({def: def, context: context, input: input, telemet
|
|
|
894
941
|
}
|
|
895
942
|
}
|
|
896
943
|
|
|
944
|
+
function listCursorWasSupplied(toolName, input) {
|
|
945
|
+
return toolName !== "list_workflow_instances" || typeof input != "object" || input === null ? !1 : "cursor" in input && typeof input.cursor == "string" && input.cursor.length > 0;
|
|
946
|
+
}
|
|
947
|
+
|
|
897
948
|
exports.WORKFLOW_TOOLS = WORKFLOW_TOOLS;
|
|
898
949
|
|
|
899
950
|
exports.registerWorkflowTools = registerWorkflowTools;
|
package/dist/stdio.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/workflow-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
"@sanity/pkg-utils": "^10.5.2",
|
|
62
62
|
"@types/node": "^24.12.4",
|
|
63
63
|
"vitest": "^4.1.8",
|
|
64
|
-
"@sanity/workflow-engine": "0.
|
|
65
|
-
"@sanity/workflow-engine-test": "0.
|
|
66
|
-
"@sanity/workflow-examples": "0.
|
|
64
|
+
"@sanity/workflow-engine": "0.22.0",
|
|
65
|
+
"@sanity/workflow-engine-test": "0.22.0",
|
|
66
|
+
"@sanity/workflow-examples": "0.10.1"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@sanity/workflow-engine": "0.
|
|
69
|
+
"@sanity/workflow-engine": "0.22.0"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=20"
|