@process-cart/mcp 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +21 -0
  5. package/dist/cli.d.ts +45 -0
  6. package/dist/cli.js +196 -0
  7. package/dist/index.d.ts +4 -0
  8. package/dist/index.js +6 -0
  9. package/dist/render.d.ts +139 -0
  10. package/dist/render.js +582 -0
  11. package/dist/server.d.ts +57 -0
  12. package/dist/server.js +261 -0
  13. package/dist/tools/add-comment.d.ts +6 -0
  14. package/dist/tools/add-comment.js +27 -0
  15. package/dist/tools/complete-task.d.ts +5 -0
  16. package/dist/tools/complete-task.js +21 -0
  17. package/dist/tools/decide-approval.d.ts +8 -0
  18. package/dist/tools/decide-approval.js +37 -0
  19. package/dist/tools/find-runs.d.ts +18 -0
  20. package/dist/tools/find-runs.js +74 -0
  21. package/dist/tools/get-activity.d.ts +10 -0
  22. package/dist/tools/get-activity.js +61 -0
  23. package/dist/tools/get-run.d.ts +3 -0
  24. package/dist/tools/get-run.js +16 -0
  25. package/dist/tools/get-workflow-schema.d.ts +3 -0
  26. package/dist/tools/get-workflow-schema.js +16 -0
  27. package/dist/tools/index.d.ts +3 -0
  28. package/dist/tools/index.js +33 -0
  29. package/dist/tools/list-people.d.ts +7 -0
  30. package/dist/tools/list-people.js +55 -0
  31. package/dist/tools/list-workflows.d.ts +9 -0
  32. package/dist/tools/list-workflows.js +70 -0
  33. package/dist/tools/reopen-task.d.ts +5 -0
  34. package/dist/tools/reopen-task.js +21 -0
  35. package/dist/tools/run-output.d.ts +6 -0
  36. package/dist/tools/run-output.js +13 -0
  37. package/dist/tools/set-fields.d.ts +5 -0
  38. package/dist/tools/set-fields.js +33 -0
  39. package/dist/tools/shared.d.ts +94 -0
  40. package/dist/tools/shared.js +97 -0
  41. package/dist/tools/start-run.d.ts +9 -0
  42. package/dist/tools/start-run.js +29 -0
  43. package/dist/tools/update-run.d.ts +10 -0
  44. package/dist/tools/update-run.js +31 -0
  45. package/dist/tools/whoami.d.ts +1 -0
  46. package/dist/tools/whoami.js +34 -0
  47. package/dist/version.d.ts +8 -0
  48. package/dist/version.js +8 -0
  49. package/package.json +58 -0
  50. package/server.json +39 -0
package/dist/render.js ADDED
@@ -0,0 +1,582 @@
1
+ /** Caps that keep one answer readable. */
2
+ export const RENDER_LIMITS = {
3
+ /** Tasks listed in a run before "… and N more tasks". */
4
+ tasks: 60,
5
+ /** Fields listed in a run (across its tasks) before "… and N more fields". */
6
+ fields: 120,
7
+ /** Fields listed in a workflow schema. */
8
+ schemaFields: 150,
9
+ /** Characters of one value, description or comment before it is cut. */
10
+ value: 300,
11
+ };
12
+ // ── Small helpers ───────────────────────────────────────────────────────────
13
+ /** C0 and C1 control characters other than the line terminators and tab / VT / FF. */
14
+ // eslint-disable-next-line no-control-regex -- matching control characters is the point.
15
+ const CONTROL_CHARS = /[\u0000-\u0008\u000E-\u001F\u007F-\u0084\u0086-\u009F]/g;
16
+ /**
17
+ * A run of whitespace holding at least one line terminator: LF, CR (CRLF too),
18
+ * NEL (U+0085), LINE SEPARATOR (U+2028) or PARAGRAPH SEPARATOR (U+2029).
19
+ */
20
+ const LINE_BREAK_RUN = /[\s\u0085]*[\n\r\u0085\u2028\u2029][\s\u0085]*/g;
21
+ /**
22
+ * Free text written by a person — a name, title, label, choice, value or
23
+ * comment — made safe to interpolate into one Markdown line: every line
24
+ * terminator becomes " / ", other control characters are dropped and runs of
25
+ * whitespace collapse, so the text can never start a line of its own (a
26
+ * heading, a list item, a line posing as an instruction). With `max`, it is
27
+ * also cut to that many characters.
28
+ */
29
+ export function text(value, max) {
30
+ const line = value
31
+ .replace(/[\t\v\f]/g, " ")
32
+ .replace(CONTROL_CHARS, "")
33
+ .replace(LINE_BREAK_RUN, " / ")
34
+ .replace(/\s+/g, " ")
35
+ .trim();
36
+ return max === undefined ? line : truncate(line, max);
37
+ }
38
+ /** Cuts `text` to `max` characters with an ellipsis. */
39
+ export function truncate(text, max = RENDER_LIMITS.value) {
40
+ if (text.length <= max)
41
+ return text;
42
+ return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
43
+ }
44
+ /** A count as people write it: 1,000. */
45
+ function count(n) {
46
+ return n.toLocaleString("en-US");
47
+ }
48
+ /** An ISO instant without its milliseconds (`2026-10-13T01:09:28Z`); a dash for null. */
49
+ export function instant(value) {
50
+ if (!value)
51
+ return "—";
52
+ return value.replace(/\.\d{1,6}Z$/, "Z");
53
+ }
54
+ function plural(n, one, many = `${one}s`) {
55
+ return `${n} ${n === 1 ? one : many}`;
56
+ }
57
+ function code(value) {
58
+ return `\`${text(value).replace(/`/g, "'")}\``;
59
+ }
60
+ /** The "pass this cursor" sentence every paged answer ends with. */
61
+ export function pagingLine(nextCursor, what) {
62
+ return nextCursor
63
+ ? `More ${what} available: call again with cursor ${code(nextCursor)} for the next page.`
64
+ : `No more ${what}.`;
65
+ }
66
+ // ── Runs ────────────────────────────────────────────────────────────────────
67
+ /** A canonical value that counts as blank (what a required field may not be). */
68
+ export function isBlankValue(field) {
69
+ const value = field.value;
70
+ if (value === null || value === undefined)
71
+ return true;
72
+ if (typeof value === "string")
73
+ return value.trim() === "";
74
+ if (Array.isArray(value))
75
+ return value.length === 0;
76
+ if (field.type === "checkbox")
77
+ return value === false;
78
+ return false;
79
+ }
80
+ const TASK_STATE_BOX = {
81
+ todo: "[ ]",
82
+ done: "[x]",
83
+ rejected_redo: "[!]",
84
+ };
85
+ function assigneeText(assignee) {
86
+ switch (assignee.kind) {
87
+ case "user":
88
+ return code(`u:${assignee.userId}`);
89
+ case "group":
90
+ return code(`g:${assignee.groupId}`);
91
+ case "role":
92
+ return `every ${assignee.role}`;
93
+ case "guest":
94
+ return `guest ${text(assignee.email)}`;
95
+ default:
96
+ return "unknown";
97
+ }
98
+ }
99
+ function taskLine(task) {
100
+ const box = TASK_STATE_BOX[task.state] ?? `[${task.state}]`;
101
+ const parts = [`${box} ${text(task.title)} (${code(task.key)})`];
102
+ if (task.kind !== "standard")
103
+ parts.push(task.kind);
104
+ if (task.state === "rejected_redo")
105
+ parts.push("sent back for redo");
106
+ if (!task.visible)
107
+ parts.push("hidden");
108
+ if (task.locked)
109
+ parts.push("locked");
110
+ if (task.dueAt)
111
+ parts.push(`due ${instant(task.dueAt)}`);
112
+ if (task.assignees.length > 0) {
113
+ parts.push(`assigned: ${task.assignees.map(assigneeText).join(", ")}`);
114
+ }
115
+ if (task.completedAt)
116
+ parts.push(`done ${instant(task.completedAt)}`);
117
+ return `- ${parts.join(" · ")}`;
118
+ }
119
+ function fieldLine(field, task, required) {
120
+ const blank = isBlankValue(field);
121
+ const shown = field.formatted !== ""
122
+ ? text(field.formatted, RENDER_LIMITS.value)
123
+ : blank
124
+ ? "(blank)"
125
+ : text(JSON.stringify(field.value), RENDER_LIMITS.value);
126
+ const notes = [];
127
+ if (field.type === "formula")
128
+ notes.push("formula, read-only");
129
+ if (field.type === "file")
130
+ notes.push("file, not writable here");
131
+ if (field.type === "hidden")
132
+ notes.push("hidden variable");
133
+ else if (!field.visible)
134
+ notes.push("hidden");
135
+ if (required &&
136
+ blank &&
137
+ field.visible &&
138
+ (task === undefined || (task.visible && task.state !== "done"))) {
139
+ notes.push("**required — blank**");
140
+ }
141
+ else if (required) {
142
+ notes.push("required");
143
+ }
144
+ if (field.validationError)
145
+ notes.push(`invalid: ${text(field.validationError)}`);
146
+ if (field.choices && field.choices.length > 0) {
147
+ notes.push(`choices: ${text(field.choices.join(" | "), 160)}`);
148
+ }
149
+ if (field.items && field.items.length > 0) {
150
+ notes.push(`items: ${text(field.items.map((i) => `${i.key} = ${i.label}`).join("; "), 160)}`);
151
+ }
152
+ return `${text(field.label)} (${code(field.key)}): ${shown}${notes.length ? ` — ${notes.join(" · ")}` : ""}`;
153
+ }
154
+ /** Task counts over the visible tasks (what "progress" means in the app). */
155
+ export function runProgress(run) {
156
+ const visible = run.tasks.filter((t) => t.visible);
157
+ return { done: visible.filter((t) => t.state === "done").length, total: visible.length };
158
+ }
159
+ /** The full run: header, then its tasks grouped by section with each task's fields beneath it. */
160
+ export function renderRun(run, options = {}) {
161
+ const required = options.requiredKeys ?? null;
162
+ const lines = [`# Run: ${text(run.name)}`];
163
+ if (options.lead)
164
+ lines.push("", options.lead);
165
+ const progress = runProgress(run);
166
+ const labels = run.labelDetails.length > 0
167
+ ? run.labelDetails.map((l) => `${text(l.name)} (${code(l.id)})`).join(", ")
168
+ : run.labels.length > 0
169
+ ? run.labels.map(code).join(", ")
170
+ : "none";
171
+ lines.push("", `- Run id: ${code(run.id)} · **revision ${run.revision}** (send it as expectedRevision on your next write to this run)`, `- Workflow: ${text(run.workflow.name)} (${code(run.workflow.id)})`, `- Status: ${run.status} · Priority: ${run.priority} · Labels: ${labels}`, `- Due: ${instant(run.dueAt)} · Key date: ${run.keyDate ?? "—"}`, `- Progress: ${progress.done} of ${plural(progress.total, "visible task")} done`, `- Started: ${instant(run.startedAt)} (${run.startedBy}${run.schedule ? `, schedule "${text(run.schedule.name)}"` : ""}) · Completed: ${instant(run.completedAt)}`);
172
+ if (run.deletedAt)
173
+ lines.push(`- In Trash since ${instant(run.deletedAt)} (read-only)`);
174
+ lines.push(`- Open in the app: ${run.url}`);
175
+ // Fields by task key, in the run's order.
176
+ const fieldsByTask = new Map();
177
+ for (const field of run.fields) {
178
+ const list = fieldsByTask.get(field.taskKey);
179
+ if (list)
180
+ list.push(field);
181
+ else
182
+ fieldsByTask.set(field.taskKey, [field]);
183
+ }
184
+ const taskKeys = new Set(run.tasks.map((t) => t.key));
185
+ // Sections in order of first appearance.
186
+ const sections = [];
187
+ for (const task of run.tasks) {
188
+ const last = sections.at(-1);
189
+ const existing = last && last.title === task.section ? last : sections.find((s) => s.title === task.section);
190
+ if (existing)
191
+ existing.tasks.push(task);
192
+ else
193
+ sections.push({ title: task.section, tasks: [task] });
194
+ }
195
+ lines.push("", `## Tasks`);
196
+ if (run.tasks.length === 0)
197
+ lines.push("", "This run has no tasks.");
198
+ let tasksShown = 0;
199
+ let fieldsShown = 0;
200
+ for (const section of sections) {
201
+ if (tasksShown >= RENDER_LIMITS.tasks)
202
+ break;
203
+ lines.push("", `### ${text(section.title) || "Untitled section"}`);
204
+ for (const task of section.tasks) {
205
+ if (tasksShown >= RENDER_LIMITS.tasks)
206
+ break;
207
+ tasksShown += 1;
208
+ lines.push(taskLine(task));
209
+ for (const field of fieldsByTask.get(task.key) ?? []) {
210
+ if (fieldsShown >= RENDER_LIMITS.fields)
211
+ break;
212
+ fieldsShown += 1;
213
+ lines.push(` - ${fieldLine(field, task, required?.has(field.key) ?? false)}`);
214
+ }
215
+ }
216
+ }
217
+ const orphans = run.fields.filter((f) => !taskKeys.has(f.taskKey));
218
+ if (orphans.length > 0 && fieldsShown < RENDER_LIMITS.fields) {
219
+ lines.push("", "### Other fields");
220
+ for (const field of orphans) {
221
+ if (fieldsShown >= RENDER_LIMITS.fields)
222
+ break;
223
+ fieldsShown += 1;
224
+ lines.push(`- ${fieldLine(field, undefined, required?.has(field.key) ?? false)}`);
225
+ }
226
+ }
227
+ const tasksSkipped = run.tasks.length - tasksShown;
228
+ const fieldsSkipped = run.fields.length - fieldsShown;
229
+ if (tasksSkipped > 0 || fieldsSkipped > 0) {
230
+ const cut = [];
231
+ if (tasksSkipped > 0)
232
+ cut.push(plural(tasksSkipped, "more task"));
233
+ if (fieldsSkipped > 0)
234
+ cut.push(plural(fieldsSkipped, "more field"));
235
+ lines.push("", `… and ${cut.join(" and ")} not shown (the structured result carries all of them).`);
236
+ }
237
+ if (required === null) {
238
+ lines.push("", "Required fields are not flagged here; complete_task names any required field that is still blank.");
239
+ }
240
+ return lines.join("\n");
241
+ }
242
+ export function summarizeRun(run) {
243
+ return {
244
+ id: run.id,
245
+ name: run.name,
246
+ status: run.status,
247
+ priority: run.priority,
248
+ labels: run.labels,
249
+ dueAt: run.dueAt,
250
+ keyDate: run.keyDate,
251
+ revision: run.revision,
252
+ updatedAt: run.updatedAt,
253
+ startedAt: run.startedAt,
254
+ completedAt: run.completedAt,
255
+ deletedAt: run.deletedAt,
256
+ workflow: run.workflow,
257
+ url: run.url,
258
+ progress: runProgress(run),
259
+ openTasks: run.tasks
260
+ .filter((t) => t.visible && !t.locked && t.state !== "done")
261
+ .map((t) => ({ key: t.key, title: t.title })),
262
+ };
263
+ }
264
+ /** One page of runs, one entry each (get_run shows a run's tasks and fields). */
265
+ export function renderRunList(runs, nextCursor) {
266
+ if (runs.length === 0)
267
+ return `No runs match.\n\n${pagingLine(nextCursor, "runs")}`;
268
+ const lines = [`Found ${plural(runs.length, "run")} on this page.`, ""];
269
+ runs.forEach((run, i) => {
270
+ const s = summarizeRun(run);
271
+ const meta = [
272
+ run.status,
273
+ `priority ${run.priority}`,
274
+ run.dueAt ? `due ${instant(run.dueAt)}` : null,
275
+ run.keyDate ? `key date ${run.keyDate}` : null,
276
+ `${s.progress.done} of ${s.progress.total} tasks done`,
277
+ `revision ${run.revision}`,
278
+ run.deletedAt ? "in Trash" : null,
279
+ ].filter(Boolean);
280
+ lines.push(`${i + 1}. **${text(run.name)}** — ${meta.join(" · ")}`);
281
+ lines.push(` id ${code(run.id)} · ${text(run.workflow.name)} · ${run.url}`);
282
+ if (s.openTasks.length > 0) {
283
+ const open = s.openTasks.slice(0, 5).map((t) => `${text(t.title)} (${code(t.key)})`);
284
+ const more = s.openTasks.length > 5 ? `, … and ${s.openTasks.length - 5} more` : "";
285
+ lines.push(` open tasks: ${open.join(", ")}${more}`);
286
+ }
287
+ });
288
+ lines.push("", pagingLine(nextCursor, "runs"), "Use get_run for a run's tasks and fields.");
289
+ return lines.join("\n");
290
+ }
291
+ // ── Workflows ───────────────────────────────────────────────────────────────
292
+ export function renderWorkflowList(workflows, nextCursor, options = {}) {
293
+ const lines = [];
294
+ const query = options.query === undefined ? undefined : text(options.query);
295
+ if (workflows.length === 0) {
296
+ lines.push(query ? `No workflow matches "${query}".` : "No workflows found.");
297
+ }
298
+ else if (query) {
299
+ const total = options.totalMatches ?? workflows.length;
300
+ lines.push(total > workflows.length
301
+ ? `${plural(total, "workflow")} match "${query}"; showing the first ${workflows.length}.`
302
+ : `${plural(total, "workflow")} match "${query}".`);
303
+ }
304
+ else {
305
+ lines.push(`Found ${plural(workflows.length, "workflow")} on this page.`);
306
+ }
307
+ workflows.forEach((w, i) => {
308
+ const state = [];
309
+ if (w.publishedVersion)
310
+ state.push(`published v${w.publishedVersion.number}`);
311
+ else
312
+ state.push("not published — runs cannot be started");
313
+ if (w.status === "archived")
314
+ state.push("archived — runs cannot be started");
315
+ state.push(plural(w.taskCount, "task"));
316
+ if (w.kickoffFormEnabled)
317
+ state.push("public kickoff form live");
318
+ if (w.folder)
319
+ state.push(`folder "${text(w.folder.name)}"`);
320
+ if (w.keyDateFieldKey)
321
+ state.push(`key date field ${code(w.keyDateFieldKey)}`);
322
+ lines.push("", `${i + 1}. **${text(w.name)}** (${code(w.id)}) — ${state.join(" · ")}`);
323
+ if (w.description)
324
+ lines.push(` ${text(w.description, 200)}`);
325
+ });
326
+ if (!query)
327
+ lines.push("", pagingLine(nextCursor, "workflows"));
328
+ lines.push("", "Next: get_workflow_schema with a workflow's id to learn its field keys.");
329
+ return lines.join("\n");
330
+ }
331
+ function asObject(value) {
332
+ return typeof value === "object" && value !== null && !Array.isArray(value)
333
+ ? value
334
+ : null;
335
+ }
336
+ function stringList(value) {
337
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
338
+ }
339
+ const KICKOFF_ABSENT = "Not a kickoff field";
340
+ const REQUIRED_UNLESS_HIDDEN = "Required at start unless a rule hides it.";
341
+ /** Reads the API's per-workflow JSON Schemas into one summary per writable field, start fields first. */
342
+ export function summarizeSchema(schema) {
343
+ const startFields = asObject(asObject(asObject(schema.start)?.properties)?.fields);
344
+ const startProps = asObject(startFields?.properties) ?? {};
345
+ const startRequired = new Set(stringList(startFields?.required));
346
+ const allProps = asObject(asObject(schema.fields)?.properties) ?? {};
347
+ // Every writable field is in `fields`; `start` holds the same set (the API takes any at start).
348
+ const keys = [...new Set([...Object.keys(allProps), ...Object.keys(startProps)])];
349
+ const out = keys.map((key) => {
350
+ const prop = asObject(allProps[key]) ?? asObject(startProps[key]) ?? {};
351
+ const startProp = asObject(startProps[key]);
352
+ const startDescription = typeof startProp?.description === "string" ? startProp.description : "";
353
+ const description = typeof prop.description === "string" ? prop.description : null;
354
+ const items = asObject(prop.items);
355
+ const choices = [...stringList(prop.enum), ...stringList(items?.enum)];
356
+ const constraints = [];
357
+ if (typeof prop.minimum === "number")
358
+ constraints.push(`min ${prop.minimum}`);
359
+ if (typeof prop.maximum === "number")
360
+ constraints.push(`max ${prop.maximum}`);
361
+ if (typeof prop.minLength === "number")
362
+ constraints.push(`at least ${prop.minLength} characters`);
363
+ if (typeof prop.maxLength === "number")
364
+ constraints.push(`at most ${prop.maxLength} characters`);
365
+ if (typeof prop.pattern === "string")
366
+ constraints.push(`pattern ${prop.pattern}`);
367
+ if (typeof prop.format === "string")
368
+ constraints.push(`format ${prop.format}`);
369
+ return {
370
+ key,
371
+ label: typeof prop.title === "string" ? prop.title : key,
372
+ type: typeof prop["x-processcart-type"] === "string" ? prop["x-processcart-type"] : "unknown",
373
+ format: typeof prop["x-processcart-format"] === "string" ? prop["x-processcart-format"] : null,
374
+ taskKey: typeof prop["x-task"] === "string" ? prop["x-task"] : null,
375
+ sectionKey: typeof prop["x-section"] === "string" ? prop["x-section"] : null,
376
+ kickoff: startProp !== null && !startDescription.includes(KICKOFF_ABSENT),
377
+ requiredAtStart: startRequired.has(key),
378
+ requiredUnlessHidden: startDescription.includes(REQUIRED_UNLESS_HIDDEN),
379
+ choices,
380
+ constraints,
381
+ notes: description,
382
+ };
383
+ });
384
+ const rank = (f) => f.requiredAtStart ? 0 : f.requiredUnlessHidden ? 1 : f.kickoff ? 2 : 3;
385
+ // A stable sort keeps the workflow's own order inside each rank.
386
+ return out
387
+ .map((f, i) => ({ f, i }))
388
+ .sort((a, b) => rank(a.f) - rank(b.f) || a.i - b.i)
389
+ .map((x) => x.f);
390
+ }
391
+ function schemaFieldLine(f) {
392
+ const kind = f.format && f.format !== "none" && f.format !== f.type ? `${f.type}, ${f.format}` : f.type;
393
+ const parts = [`${text(f.label)} (${code(f.key)}) · ${text(kind)}`];
394
+ if (f.requiredAtStart)
395
+ parts.push("**required at start**");
396
+ else if (f.requiredUnlessHidden)
397
+ parts.push("**required at start unless a rule hides it**");
398
+ else if (f.kickoff)
399
+ parts.push("kickoff field");
400
+ if (f.taskKey)
401
+ parts.push(`task ${code(f.taskKey)}`);
402
+ if (f.choices.length > 0)
403
+ parts.push(`values: ${text(f.choices.join(" | "), 200)}`);
404
+ if (f.constraints.length > 0)
405
+ parts.push(text(f.constraints.join(", ")));
406
+ const line = `- ${parts.join(" · ")}`;
407
+ return f.notes ? `${line}\n ${text(f.notes, 240)}` : line;
408
+ }
409
+ export function renderSchema(schema) {
410
+ const fields = summarizeSchema(schema);
411
+ const start = fields.filter((f) => f.requiredAtStart || f.requiredUnlessHidden || f.kickoff);
412
+ const lines = [
413
+ `# Workflow schema`,
414
+ "",
415
+ `- Workflow id: ${code(schema.workflowId)} · published version ${schema.versionNumber} (${code(schema.versionId)})`,
416
+ `- ${plural(fields.length, "writable field")}. start_run accepts any of them in \`fields\`; set_fields writes them on an existing run. Keys are the identity — never the labels.`,
417
+ "",
418
+ "## Start fields (the run-start form)",
419
+ ];
420
+ if (start.length === 0)
421
+ lines.push("", "No kickoff fields: start_run needs only a name.");
422
+ else
423
+ lines.push("", ...start.map(schemaFieldLine));
424
+ lines.push("", "## Every writable field");
425
+ const shown = fields.slice(0, RENDER_LIMITS.schemaFields);
426
+ lines.push("", ...shown.map(schemaFieldLine));
427
+ if (fields.length > shown.length) {
428
+ lines.push("", `… and ${plural(fields.length - shown.length, "more field")} not shown (the structured result carries the full JSON Schemas).`);
429
+ }
430
+ lines.push("", "Values: a JSON number for number / currency / percent fields (a percent is a fraction: 0.125 = 12.5 %), YYYY-MM-DD for dates, HH:mm for times, a list of strings for multi-choice, subtask and members fields, true / false for checkboxes, null to clear. Formula and file fields are not writable.");
431
+ return lines.join("\n");
432
+ }
433
+ // ── Activity and comments ───────────────────────────────────────────────────
434
+ function authorName(author) {
435
+ if (typeof author.name === "string" && author.name)
436
+ return author.name;
437
+ switch (author.kind) {
438
+ case "guest":
439
+ return author.email;
440
+ case "user":
441
+ return `user ${author.userId}`;
442
+ case "system":
443
+ return "Process Cart";
444
+ default:
445
+ return author.kind;
446
+ }
447
+ }
448
+ export function renderComment(comment) {
449
+ return `- ${instant(comment.createdAt)} · ${text(authorName(comment.author), 120)} on ${code(comment.taskKey)}: ${text(comment.text, RENDER_LIMITS.value)}`;
450
+ }
451
+ function commentsHeading(comments) {
452
+ const { shown, total } = comments;
453
+ if (comments.complete === false) {
454
+ const first = total - shown.length + 1;
455
+ return `Comments ${count(first)}–${count(total)} of more than ${count(total)}, oldest first. The API lists comments oldest first and this server reads only the first ${count(total)}, so the run's newest comments are not shown:`;
456
+ }
457
+ return total > shown.length
458
+ ? `The latest ${shown.length} of ${count(total)} comments, oldest first:`
459
+ : `${plural(total, "comment")}, oldest first:`;
460
+ }
461
+ export function renderActivity(items, nextCursor, comments) {
462
+ const lines = ["## Activity (newest first)", ""];
463
+ if (items.length === 0)
464
+ lines.push("No activity on this page.");
465
+ for (const item of items) {
466
+ const task = item.taskKey ? ` · task ${code(item.taskKey)}` : "";
467
+ lines.push(`- ${instant(item.createdAt)} · ${text(item.description, RENDER_LIMITS.value)} (${text(item.type)}${task})`);
468
+ }
469
+ lines.push("", pagingLine(nextCursor, "activity"));
470
+ if (comments) {
471
+ lines.push("", "## Comments");
472
+ if (comments.total === 0) {
473
+ lines.push("", "No comments on this run.");
474
+ }
475
+ else {
476
+ lines.push("", commentsHeading(comments), "", ...comments.shown.map(renderComment));
477
+ }
478
+ }
479
+ return lines.join("\n");
480
+ }
481
+ // ── People ──────────────────────────────────────────────────────────────────
482
+ export function renderPeople(users, groups, options = {}) {
483
+ const lines = [];
484
+ const filter = options.query ? ` matching "${text(options.query)}"` : "";
485
+ lines.push(`## Members${filter} (${users.length})`, "");
486
+ if (users.length === 0)
487
+ lines.push("None.");
488
+ for (const user of users) {
489
+ lines.push(`- ${user.name ? text(user.name) : "(no name)"} · ${user.role} · ${code(`u:${user.id}`)}`);
490
+ }
491
+ lines.push("", `## Groups${filter} (${groups.length})`, "");
492
+ if (groups.length === 0)
493
+ lines.push("None.");
494
+ for (const group of groups) {
495
+ lines.push(`- ${text(group.name)} · ${plural(group.memberIds.length, "member")} · ${code(`g:${group.id}`)}`);
496
+ }
497
+ if (options.truncated)
498
+ lines.push("", "The directory is larger than this server reads at once; narrow it with q.");
499
+ lines.push("", "Pass `u:<id>` or `g:<id>` as find_runs' assignee. A members field takes the bare user ids (without `u:`).");
500
+ return lines.join("\n");
501
+ }
502
+ export function renderMe(me, mode) {
503
+ const labels = me.org.labels.length > 0
504
+ ? me.org.labels.map((l) => `${text(l.name)} (${code(l.id)})`).join(", ")
505
+ : "none defined";
506
+ const scopes = me.key.scopes.includes("*")
507
+ ? "all scopes (`*`)"
508
+ : me.key.scopes.map(code).join(", ");
509
+ const lines = [
510
+ `# Connected to Process Cart`,
511
+ "",
512
+ `- Workspace: **${text(me.org.name)}** (${code(me.org.slug)}) · time zone ${text(me.org.timezone)}${me.org.sandbox ? " · **sandbox**" : ""}`,
513
+ `- Status labels: ${labels}`,
514
+ `- API key: "${text(me.key.name)}" (${code(`${me.key.prefix}…`)}) · ${me.key.readOnly ? "**read-only key**" : "read and write"} · ${scopes}`,
515
+ `- Rate limit: ${me.rateLimit.limit} requests per ${me.rateLimit.windowSeconds} seconds`,
516
+ "",
517
+ "## This server",
518
+ "",
519
+ `- API: ${mode.baseUrl}`,
520
+ `- Mode: ${mode.readOnlyFlag ? "read-only (started with --read-only)" : me.key.readOnly ? "read-only (the key is read-only)" : "read and write, as the key's scopes allow"}`,
521
+ `- Tools: ${mode.tools.map(code).join(", ")}`,
522
+ ];
523
+ if (mode.startupProblem) {
524
+ lines.push("", `At startup the server could not read the key (${text(mode.startupProblem)}), so it registered the read tools only. Restart the server to load the tools this key allows.`);
525
+ }
526
+ return lines.join("\n");
527
+ }
528
+ const HINTS = {
529
+ required_fields_unsatisfied: "Set the listed fields with set_fields (or the app), then complete the task again.",
530
+ insufficient_scope: "This API key does not have the scope that action needs; an Admin can change the key under Settings → API.",
531
+ read_only_key: "This API key is read-only; it can read but not change anything.",
532
+ not_allowed_for_key: "This API key is limited to certain workflows or folders, and this one is outside them.",
533
+ not_found: "Check the id: list_workflows and find_runs list the ones this key can see.",
534
+ workflow_not_published: "Publish the workflow in the app before starting runs of it.",
535
+ task_locked: "Complete the earlier tasks that lock this one first.",
536
+ task_hidden: "A visibility rule hides this task right now; it cannot be completed.",
537
+ approver_only: "Only the task's approvers can decide it; an API key needs the approvals:decide scope.",
538
+ wrong_task_kind: "Approval tasks are decided with decide_approval; standard tasks are completed with complete_task.",
539
+ field_not_writable: "Formula, file and display-text fields cannot be written.",
540
+ run_archived: "The run is archived: set its status back with update_run before changing it.",
541
+ run_trashed: "The run is in Trash; restore it in the app first.",
542
+ invalid_api_key: "The API key is not valid. Check PROCESSCART_API_KEY.",
543
+ key_revoked: "The API key was revoked. An Admin can create a new one under Settings → API.",
544
+ unauthorized: "The API key is missing or not valid. Check PROCESSCART_API_KEY.",
545
+ network_error: "Could not reach the Process Cart API. Check the network and PROCESSCART_BASE_URL.",
546
+ timeout: "The Process Cart API did not answer in time; try again.",
547
+ unavailable: "The Process Cart API is briefly unavailable; try again shortly.",
548
+ };
549
+ /** The error as a model reads it: what failed, every field problem, and what to do next. */
550
+ export function renderProblem(problem) {
551
+ const status = problem.status > 0 ? `HTTP ${problem.status} · ` : "";
552
+ const lines = [
553
+ `**Process Cart error** (${status}${code(problem.code)}): ${text(problem.title)}${problem.detail && problem.detail !== problem.title ? ` — ${text(problem.detail)}` : ""}`,
554
+ ];
555
+ if (problem.errors && problem.errors.length > 0) {
556
+ lines.push("", problem.code === "required_fields_unsatisfied" ? "Missing:" : "Problems:");
557
+ for (const e of problem.errors.slice(0, 50)) {
558
+ lines.push(`- ${code(e.field)}: ${text(e.code)} — ${text(e.message)}`);
559
+ }
560
+ if (problem.errors.length > 50)
561
+ lines.push(`- … and ${problem.errors.length - 50} more`);
562
+ }
563
+ if (problem.code === "revision_mismatch") {
564
+ lines.push("", problem.currentRevision !== undefined
565
+ ? `The run changed since you read it: it is now at **revision ${problem.currentRevision}**. Read it again with get_run, check that your change still makes sense, and retry with expectedRevision ${problem.currentRevision}.`
566
+ : "The run changed since you read it. Read it again with get_run and retry with its new revision.");
567
+ }
568
+ if (problem.code === "rate_limited" || problem.status === 429) {
569
+ lines.push("", problem.retryAfter !== undefined
570
+ ? `Rate limited: wait ${plural(problem.retryAfter, "second")} before the next request.`
571
+ : "Rate limited: wait a minute before the next request.");
572
+ }
573
+ else if (problem.retryAfter !== undefined) {
574
+ lines.push("", `The API asked to retry after ${plural(problem.retryAfter, "second")}.`);
575
+ }
576
+ const hint = HINTS[problem.code];
577
+ if (hint)
578
+ lines.push("", hint);
579
+ if (problem.requestId)
580
+ lines.push("", `Request id: ${code(problem.requestId)} (quote it to support).`);
581
+ return lines.join("\n");
582
+ }
@@ -0,0 +1,57 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type ClientOptions } from "@process-cart/sdk";
3
+ import { type ProblemView } from "./render.js";
4
+ import { type ToolDefinition } from "./tools/shared.js";
5
+ /**
6
+ * The startup `getMe` gets one attempt of at most this long, so a slow or
7
+ * unreachable API never holds back the client's `initialize` (clients give
8
+ * up after about 60 s) — the server then starts with the read tools only.
9
+ */
10
+ export declare const STARTUP_TIMEOUT_MS = 10000;
11
+ /** Replaces every API key in a diagnostic with its prefix (`pc_live_…`). */
12
+ export declare function redactKeys(message: string): string;
13
+ export interface CreateServerOptions {
14
+ /** A Process Cart API key (`pc_live_…` or `pc_test_…`), from Settings → API. */
15
+ apiKey: string;
16
+ /** Default `https://app.processcart.com/api`. */
17
+ baseUrl?: string;
18
+ /** Register the read tools only, whatever the key's scopes allow. */
19
+ readOnly?: boolean;
20
+ /** A WHATWG-compatible fetch (default `globalThis.fetch`). */
21
+ fetch?: typeof fetch;
22
+ /**
23
+ * Where diagnostics go (default: one line on stderr). Never handed the API
24
+ * key, field values or comment text.
25
+ */
26
+ log?: (message: string) => void;
27
+ /** Log every tool call: its name, outcome and duration. */
28
+ debug?: boolean;
29
+ /** The SDK client's retry policy (see `@process-cart/sdk`). */
30
+ retry?: ClientOptions["retry"];
31
+ /** The SDK client's per-attempt timeout, in milliseconds. */
32
+ timeoutMs?: number;
33
+ /** The startup `getMe`'s one attempt, in milliseconds (default `STARTUP_TIMEOUT_MS`). */
34
+ startupTimeoutMs?: number;
35
+ }
36
+ /**
37
+ * What the model is told about working with this server (the `instructions`
38
+ * of `initialize`), built from the tools it registered: a read-only server
39
+ * says so and names no write tool.
40
+ */
41
+ export declare function buildInstructions(tools: readonly Pick<ToolDefinition, "name" | "access">[]): string;
42
+ /** The instructions of a server that registered every tool (a full-access key). */
43
+ export declare const INSTRUCTIONS: string;
44
+ /** Which tools a key may use: read tools by scope; write tools only for a writable key, and never with `--read-only`. */
45
+ export declare function selectTools(tools: readonly ToolDefinition[], key: {
46
+ scopes: readonly string[];
47
+ readOnly: boolean;
48
+ } | null, readOnlyFlag: boolean): ToolDefinition[];
49
+ /** Any failure as the renderer reads it. */
50
+ export declare function problemOf(error: unknown): ProblemView;
51
+ /**
52
+ * Builds the server: reads the key's identity, then registers the tools it may
53
+ * use. Connect it to a transport (`StdioServerTransport` in the CLI) to serve.
54
+ * Rejects only for an option the SDK refuses (a malformed API key); an API
55
+ * failure at startup is logged and leaves the read tools registered.
56
+ */
57
+ export declare function createServer(options: CreateServerOptions): Promise<McpServer>;