rapidoc 12.0.0 → 12.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 (77) hide show
  1. package/README.md +127 -25
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +541 -1309
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +21 -18
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +43 -0
  10. package/src/components/api-response.ts +9 -4
  11. package/src/components/schema-base-element.ts +18 -4
  12. package/src/components/schema-table.ts +58 -13
  13. package/src/components/schema-tree.ts +7 -0
  14. package/src/components/tag-input.ts +5 -9
  15. package/src/flow.js +3 -0
  16. package/src/rapi-flow.ts +1178 -0
  17. package/src/rapidoc-mini.ts +27 -6
  18. package/src/rapidoc.ts +108 -62
  19. package/src/styles/README.md +96 -0
  20. package/src/styles/css-bundle.js +2 -2
  21. package/src/styles/endpoint-styles.js +6 -1
  22. package/src/styles/flow-styles.js +248 -0
  23. package/src/styles/nav-styles.js +39 -15
  24. package/src/styles/schema-styles.js +13 -5
  25. package/src/styles/theme-tokens.js +2 -2
  26. package/src/templates/README.md +89 -0
  27. package/src/templates/api-response-template.js +14 -10
  28. package/src/templates/callback-template.js +1 -1
  29. package/src/templates/downloads-template.js +12 -27
  30. package/src/templates/endpoint-template.js +15 -24
  31. package/src/templates/expanded-endpoint-template.js +8 -4
  32. package/src/templates/focused-endpoint-template.js +4 -3
  33. package/src/templates/navbar-template.js +20 -11
  34. package/src/templates/request-body-template.js +29 -7
  35. package/src/templates/request-params-template.js +15 -4
  36. package/src/templates/security-scheme-template.js +11 -2
  37. package/src/templates/server-template.js +3 -2
  38. package/src/utils/README.md +146 -0
  39. package/src/utils/arazzo/README.md +119 -0
  40. package/src/utils/arazzo/components.js +15 -0
  41. package/src/utils/arazzo/graph.js +269 -0
  42. package/src/utils/arazzo/index.js +52 -0
  43. package/src/utils/arazzo/operations.js +106 -0
  44. package/src/utils/arazzo/parse.js +196 -0
  45. package/src/utils/arazzo/refs.js +40 -0
  46. package/src/utils/arazzo/svg.js +707 -0
  47. package/src/utils/curl-utils.js +5 -2
  48. package/src/utils/highlighter.js +3 -0
  49. package/src/utils/mcp/README.md +155 -0
  50. package/src/utils/mcp/common.js +45 -346
  51. package/src/utils/mcp/manifest.js +382 -0
  52. package/src/utils/mcp/project/server.ts +53 -0
  53. package/src/utils/mcp/project.js +88 -0
  54. package/src/utils/mcp/runtime.js +289 -0
  55. package/src/utils/mcp/schema.js +98 -0
  56. package/src/utils/mcp/workflow-runtime.js +374 -0
  57. package/src/utils/mcp/workflows.js +240 -0
  58. package/src/utils/mcp/zip.js +102 -0
  59. package/src/utils/mcp-generator.js +29 -125
  60. package/src/utils/mock-interceptor.js +134 -85
  61. package/src/utils/request-utils.js +61 -0
  62. package/src/utils/schema-utils.js +33 -6
  63. package/src/utils/slugger.js +5 -7
  64. package/src/utils/spec-parser.js +190 -164
  65. package/src/utils/url-utils.js +36 -1
  66. package/dist/rapidoc-static-min.js +0 -13542
  67. package/src/static/index.d.ts +0 -149
  68. package/src/static/index.js +0 -62
  69. package/src/static/navigation.js +0 -497
  70. package/src/static/render.js +0 -490
  71. package/src/static/serializer.js +0 -216
  72. package/src/utils/mcp/generators/csharp.js +0 -242
  73. package/src/utils/mcp/generators/go.js +0 -298
  74. package/src/utils/mcp/generators/java.js +0 -330
  75. package/src/utils/mcp/generators/python.js +0 -248
  76. package/src/utils/mcp/generators/rust.js +0 -281
  77. package/src/utils/mcp/generators/typescript.js +0 -290
@@ -0,0 +1,374 @@
1
+ /**
2
+ * RapiDoc MCP workflow runtime: runs the compiled Arazzo workflows of a manifest (see
3
+ * workflows.js) on top of runtime.js: runtime expressions, success criteria, success and failure
4
+ * actions (end, goto, retry) and outputs. Kept apart from runtime.js so dist/rapidoc-min.js,
5
+ * which exports operation tools only, carries no Arazzo code; dist/rapidoc-flow-min.js and
6
+ * @rapidoc/mcp include it.
7
+ *
8
+ * This file is copied verbatim into exported MCP server projects that have workflows as
9
+ * `workflow-runtime.js`, so it imports only `./runtime.js`.
10
+ */
11
+ import { createMcpRuntime } from "./runtime.js";
12
+
13
+ function serialize(value) {
14
+ if (typeof value === "string") return value;
15
+ if (value && typeof value === "object") return JSON.stringify(value);
16
+ return String(value);
17
+ }
18
+
19
+ function errorResult(text) {
20
+ return { isError: true, content: [{ type: "text", text }] };
21
+ }
22
+
23
+ // ---- Arazzo runtime expressions ----
24
+
25
+ /** RFC 6901 JSON pointer lookup; "" is the whole value. */
26
+ function pointer(value, path) {
27
+ if (!path) return value;
28
+ let node = value;
29
+ for (const raw of path.split("/").slice(1)) {
30
+ const key = raw.replace(/~1/g, "/").replace(/~0/g, "~");
31
+ if (node === null || typeof node !== "object") return undefined;
32
+ node = node[key];
33
+ }
34
+ return node;
35
+ }
36
+
37
+ function walk(value, keys) {
38
+ let node = value;
39
+ for (const key of keys) {
40
+ if (node === null || typeof node !== "object") return undefined;
41
+ node = node[key];
42
+ }
43
+ return node;
44
+ }
45
+
46
+ /**
47
+ * One Arazzo runtime expression against the step context: `$inputs.x`, `$steps.id.outputs.x`,
48
+ * `$statusCode`, `$response.body` (`#/pointer`), `$response.header.Name`, `$request.body`,
49
+ * `$request.header.Name`, `$url`, `$method`.
50
+ */
51
+ export function evaluateExpression(expr, ctx) {
52
+ const m = String(expr).match(/^\$([A-Za-z]+)((?:\.[^.#\s]+)*)(?:#(.*))?$/);
53
+ if (!m) return undefined;
54
+ const [, root, dotted, ptr] = m;
55
+ const keys = dotted ? dotted.slice(1).split(".") : [];
56
+ switch (root) {
57
+ case "inputs":
58
+ return walk(ctx.inputs || {}, keys);
59
+ case "steps":
60
+ return walk(ctx.steps || {}, keys);
61
+ case "statusCode":
62
+ return ctx.response?.status;
63
+ case "url":
64
+ return ctx.request?.url;
65
+ case "method":
66
+ return ctx.request?.method;
67
+ case "response":
68
+ case "request": {
69
+ const msg = ctx[root];
70
+ if (!msg) return undefined;
71
+ const [part, ...rest] = keys;
72
+ if (part === "body") return walk(pointer(msg.body, ptr || ""), rest);
73
+ if (part === "header" || part === "headers") {
74
+ const name = rest.join(".").toLowerCase();
75
+ const headers = msg.headers || {};
76
+ const hit = Object.keys(headers).find((h) => h.toLowerCase() === name);
77
+ return hit === undefined ? undefined : headers[hit];
78
+ }
79
+ if (part === "query" || part === "path") return walk(msg[part] || {}, rest);
80
+ return undefined;
81
+ }
82
+ default:
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ const WHOLE_EXPRESSION = /^\$[A-Za-z][^\s{}]*$/;
88
+
89
+ /** A parameter or payload value with its expressions filled in; `{$expr}` inside text is interpolated. */
90
+ export function resolveValue(value, ctx) {
91
+ if (typeof value === "string") {
92
+ if (WHOLE_EXPRESSION.test(value)) return evaluateExpression(value, ctx);
93
+ return value.replace(/\{(\$[^{}]+)\}/g, (_, expr) => {
94
+ const v = evaluateExpression(expr, ctx);
95
+ return v === undefined || v === null ? "" : serialize(v);
96
+ });
97
+ }
98
+ if (Array.isArray(value)) return value.map((v) => resolveValue(v, ctx));
99
+ if (value && typeof value === "object") {
100
+ const out = {};
101
+ for (const [k, v] of Object.entries(value)) out[k] = resolveValue(v, ctx);
102
+ return out;
103
+ }
104
+ return value;
105
+ }
106
+
107
+ // ---- Simple conditions: `$statusCode == 200 && $response.body#/status != 'placed'` ----
108
+
109
+ function tokenize(condition) {
110
+ const tokens = [];
111
+ const re =
112
+ /\s*(?:(&&|\|\||==|!=|<=|>=|<|>|!|\(|\))|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|(-?\d+(?:\.\d+)?)|(\$[^\s()&|=!<>]+)|(true|false|null))/y;
113
+ let last = 0;
114
+ for (;;) {
115
+ re.lastIndex = last;
116
+ const m = re.exec(condition);
117
+ if (!m) break;
118
+ last = re.lastIndex;
119
+ if (m[1]) tokens.push({ op: m[1] });
120
+ else if (m[2]) tokens.push({ value: m[2].slice(1, -1).replace(/\\(.)/g, "$1") });
121
+ else if (m[3]) tokens.push({ value: Number(m[3]) });
122
+ else if (m[4]) tokens.push({ expr: m[4] });
123
+ else if (m[5]) tokens.push({ value: m[5] === "true" ? true : m[5] === "false" ? false : null });
124
+ }
125
+ if (condition.slice(last).trim()) throw new Error(`Cannot parse condition "${condition}"`);
126
+ return tokens;
127
+ }
128
+
129
+ /** `==` in a simple condition: null and a missing value are the same; numbers and strings compare as text. */
130
+ function looseEquals(a, b) {
131
+ if (a === b) return true;
132
+ const missing = (v) => v === null || v === undefined;
133
+ if (missing(a) || missing(b)) return missing(a) && missing(b);
134
+ if (typeof a === "object" || typeof b === "object")
135
+ return JSON.stringify(a) === JSON.stringify(b);
136
+ return String(a) === String(b);
137
+ }
138
+
139
+ /** Evaluates a simple Arazzo condition. Throws when it cannot be parsed. */
140
+ export function evaluateCondition(condition, ctx) {
141
+ const tokens = tokenize(condition);
142
+ let i = 0;
143
+ const peek = () => tokens[i];
144
+ const takeOp = (...ops) => (peek()?.op && ops.includes(peek().op) ? tokens[i++].op : null);
145
+ const unary = () => {
146
+ if (takeOp("!")) return !unary();
147
+ if (takeOp("(")) {
148
+ const v = or();
149
+ if (!takeOp(")")) throw new Error(`Missing ) in "${condition}"`);
150
+ return v;
151
+ }
152
+ const t = tokens[i++];
153
+ if (!t || t.op) throw new Error(`Unexpected ${t ? t.op : "end"} in "${condition}"`);
154
+ return t.expr !== undefined ? evaluateExpression(t.expr, ctx) : t.value;
155
+ };
156
+ const cmp = () => {
157
+ const left = unary();
158
+ const op = takeOp("==", "!=", "<=", ">=", "<", ">");
159
+ if (!op) return left;
160
+ const right = unary();
161
+ if (op === "==") return looseEquals(left, right);
162
+ if (op === "!=") return !looseEquals(left, right);
163
+ const [a, b] = [Number(left), Number(right)];
164
+ if (op === "<") return a < b;
165
+ if (op === ">") return a > b;
166
+ if (op === "<=") return a <= b;
167
+ return a >= b;
168
+ };
169
+ const and = () => {
170
+ let v = cmp();
171
+ while (takeOp("&&")) v = cmp() && v;
172
+ return v;
173
+ };
174
+ const or = () => {
175
+ let v = and();
176
+ while (takeOp("||")) v = and() || v;
177
+ return v;
178
+ };
179
+ const result = or();
180
+ if (i < tokens.length) throw new Error(`Unexpected ${tokens[i].op ?? "token"} in "${condition}"`);
181
+ return Boolean(result);
182
+ }
183
+
184
+ /** Sets the value at a JSON pointer inside a payload (an Arazzo request body replacement). */
185
+ function replaceAt(payload, target, value) {
186
+ const keys = String(target)
187
+ .split("/")
188
+ .slice(1)
189
+ .map((k) => k.replace(/~1/g, "/").replace(/~0/g, "~"));
190
+ if (!keys.length) return value;
191
+ const root = Array.isArray(payload) ? [...payload] : { ...payload };
192
+ let node = root;
193
+ for (const [i, key] of keys.entries()) {
194
+ if (i === keys.length - 1) {
195
+ node[key] = value;
196
+ break;
197
+ }
198
+ const next = node[key];
199
+ node[key] = Array.isArray(next) ? [...next] : { ...next };
200
+ node = node[key];
201
+ }
202
+ return root;
203
+ }
204
+
205
+ const MAX_STEP_RUNS = 1000;
206
+
207
+ function evaluateAll(criteria, ctx) {
208
+ return (criteria || []).every((c) => {
209
+ try {
210
+ return evaluateCondition(c.condition, ctx);
211
+ } catch {
212
+ return false;
213
+ }
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Runs a workflow tool. Pass as createMcpRuntime's `runWorkflow` option, or use
219
+ * createMcpWorkflowRuntime().
220
+ *
221
+ * @param {object} runtime from createMcpRuntime()
222
+ * @param {object} tool a manifest tool with `workflow`
223
+ * @param {object} args the workflow inputs
224
+ * @param {object} [hooks]
225
+ * @param {Function} [hooks.onStepStart] gets `{ workflowId, stepId, attempt }` before each request
226
+ * @param {Function} [hooks.onStep] gets `{ workflowId, stepId, attempt, outcome, request,
227
+ * response, outputs, failedCriterion, action }` after each step run
228
+ * @param {(ms: number) => Promise<void>} [hooks.sleep] waits between retries (default setTimeout)
229
+ * @param {Function} [hooks.requestHeaders] gets `{ workflowId, stepId, attempt, operation }` and
230
+ * returns extra headers for that request (a mock's `Prefer: code=503`, say), or nothing
231
+ * @returns {Promise<object>} the MCP result: the workflow's outputs as `structuredContent`, or an
232
+ * error result naming the step, the failed criterion and the response
233
+ */
234
+ export async function runWorkflow(runtime, tool, args, hooks = {}) {
235
+ const workflow = runtime.manifest.workflows?.[tool.workflow];
236
+ if (!workflow) return errorResult(`Workflow "${tool.workflow}" is not in the manifest`);
237
+ const sleep = hooks.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
238
+ const inputs = args || {};
239
+ const required = (workflow.inputs?.required || []).filter((name) => inputs[name] === undefined);
240
+ if (required.length) return errorResult(`Missing input ${required.join(", ")}`);
241
+
242
+ const steps = workflow.steps;
243
+ const indexOf = (stepId) => steps.findIndex((s) => s.stepId === stepId);
244
+ const stepsCtx = {};
245
+ const attempts = new Map();
246
+ let index = 0;
247
+ let runs = 0;
248
+ while (index >= 0 && index < steps.length) {
249
+ if (++runs > MAX_STEP_RUNS) {
250
+ return errorResult(`Workflow ${workflow.workflowId} ran more than ${MAX_STEP_RUNS} steps`);
251
+ }
252
+ const step = steps[index];
253
+ const attempt = (attempts.get(step.stepId) || 0) + 1;
254
+ attempts.set(step.stepId, attempt);
255
+ const ctx = { inputs, steps: stepsCtx };
256
+ const stepArgs = {};
257
+ for (const p of step.parameters || []) stepArgs[p.name] = resolveValue(p.value, ctx);
258
+ if (step.requestBody) {
259
+ let payload = resolveValue(step.requestBody.payload, ctx);
260
+ for (const r of step.requestBody.replacements || []) {
261
+ payload = replaceAt(payload, r.target, resolveValue(r.value, ctx));
262
+ }
263
+ stepArgs.body = payload;
264
+ }
265
+ const event = { workflowId: workflow.workflowId, stepId: step.stepId, attempt };
266
+ hooks.onStepStart?.(event);
267
+ const headers = hooks.requestHeaders?.({ ...event, operation: step.operation });
268
+ const sent = await runtime.execute(step.operation, stepArgs, headers ? { headers } : {});
269
+ const stepCtx = {
270
+ ...ctx,
271
+ request: sent.request && {
272
+ url: sent.request.url,
273
+ method: sent.request.method,
274
+ headers: sent.request.headers,
275
+ body: stepArgs.body,
276
+ },
277
+ response: sent.response && {
278
+ status: sent.response.status,
279
+ headers: sent.headers,
280
+ body: sent.data === undefined ? sent.text : sent.data,
281
+ },
282
+ };
283
+ let failedCriterion = sent.error ? sent.error.content[0].text : "";
284
+ // Without criteria of its own, a step succeeds on a 2xx response
285
+ if (!failedCriterion && !(step.successCriteria || []).length && !sent.response.ok) {
286
+ failedCriterion = `HTTP ${sent.response.status}`;
287
+ }
288
+ if (!failedCriterion) {
289
+ for (const c of step.successCriteria || []) {
290
+ let ok;
291
+ try {
292
+ ok = evaluateCondition(c.condition, stepCtx);
293
+ } catch (err) {
294
+ ok = false;
295
+ failedCriterion = err.message;
296
+ }
297
+ if (!ok) {
298
+ failedCriterion ||= c.condition;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ const outcome = failedCriterion ? "failure" : "success";
304
+ let outputs;
305
+ if (outcome === "success") {
306
+ outputs = {};
307
+ for (const [name, expr] of Object.entries(step.outputs || {})) {
308
+ outputs[name] = resolveValue(expr, stepCtx);
309
+ }
310
+ stepsCtx[step.stepId] = { outputs };
311
+ }
312
+ const candidates = outcome === "success" ? step.onSuccess : step.onFailure;
313
+ const action = (candidates || []).find((a) => evaluateAll(a.criteria, stepCtx));
314
+ hooks.onStep?.({
315
+ workflowId: workflow.workflowId,
316
+ stepId: step.stepId,
317
+ attempt,
318
+ outcome,
319
+ request: stepCtx.request,
320
+ response: stepCtx.response,
321
+ outputs,
322
+ failedCriterion: failedCriterion || undefined,
323
+ action: action ? { type: action.type, name: action.name, stepId: action.stepId } : null,
324
+ });
325
+
326
+ const failure = (why) =>
327
+ errorResult(
328
+ `Workflow ${workflow.workflowId} failed at step ${step.stepId}: ${why}` +
329
+ (stepCtx.response
330
+ ? `\nHTTP ${stepCtx.response.status}\n${serialize(stepCtx.response.body)}`
331
+ : ""),
332
+ );
333
+ if (outcome === "success") {
334
+ if (!action) index++;
335
+ else if (action.type === "end") break;
336
+ else if (action.type === "goto") index = indexOf(action.stepId);
337
+ else index++;
338
+ continue;
339
+ }
340
+ if (!action || action.type === "end") return failure(failedCriterion);
341
+ if (action.type === "goto") {
342
+ index = indexOf(action.stepId);
343
+ continue;
344
+ }
345
+ // retry
346
+ const target = action.stepId ? indexOf(action.stepId) : index;
347
+ const key = `${step.stepId}:${action.name}`;
348
+ const tried = (attempts.get(key) || 0) + 1;
349
+ attempts.set(key, tried);
350
+ if (tried > (action.retryLimit ?? 1)) {
351
+ return failure(`${failedCriterion} (after ${action.retryLimit ?? 1} retries)`);
352
+ }
353
+ if (action.retryAfter) await sleep(action.retryAfter * 1000);
354
+ index = target;
355
+ }
356
+
357
+ const outputs = {};
358
+ for (const [name, expr] of Object.entries(workflow.outputs || {})) {
359
+ outputs[name] = resolveValue(expr, { inputs, steps: stepsCtx });
360
+ }
361
+ return {
362
+ content: [{ type: "text", text: JSON.stringify(outputs, null, 2) }],
363
+ structuredContent: outputs,
364
+ };
365
+ }
366
+
367
+ /** createMcpRuntime() with workflow support plugged in; `options.sleep` waits between retries. */
368
+ export function createMcpWorkflowRuntime(manifest, options = {}) {
369
+ return createMcpRuntime(manifest, {
370
+ ...options,
371
+ runWorkflow: (runtime, tool, args, hooks) =>
372
+ runWorkflow(runtime, tool, args, { sleep: options.sleep, ...hooks }),
373
+ });
374
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Compiles the workflows of an Arazzo document into the manifest: one tool per workflow, and a
3
+ * `workflows` entry whose steps name resolved operations (source, method, path), so the runtime
4
+ * needs no Arazzo or OpenAPI document. A workflow that uses something the runtime does not run
5
+ * (another workflow, non-simple criteria, `$workflows` or `$components` expressions) is reported
6
+ * through `onProblem` and left out.
7
+ */
8
+ import { sanitizeDescription, toSnakeCase } from "./common.js";
9
+ import { toJsonSchema } from "./schema.js";
10
+ import { resolveComponentRef } from "../arazzo/components.js";
11
+ import { resolveOperation } from "../arazzo/operations.js";
12
+ import { collectRefs } from "../arazzo/refs.js";
13
+
14
+ const PARAM_LOCATIONS = new Set(["path", "query", "header", "cookie"]);
15
+ const ACTION_TYPES = new Set(["end", "goto", "retry"]);
16
+
17
+ function criteria(list, where, problems) {
18
+ const out = [];
19
+ for (const [i, c] of (Array.isArray(list) ? list : []).entries()) {
20
+ if (!c || typeof c.condition !== "string") continue;
21
+ const type = typeof c.type === "string" ? c.type : c.type?.type || "simple";
22
+ if (type !== "simple") {
23
+ problems.push(
24
+ `${where}[${i}]: criterion type ${type} is not supported, only simple conditions`,
25
+ );
26
+ continue;
27
+ }
28
+ out.push({ condition: c.condition });
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function actions(doc, list, kind, where, stepIds, problems) {
34
+ const out = [];
35
+ for (const [i, raw] of (Array.isArray(list) ? list : []).entries()) {
36
+ const a = resolveComponentRef(doc, raw, kind);
37
+ if (!a || !ACTION_TYPES.has(a.type)) continue;
38
+ const p = `${where}[${i}]`;
39
+ if (a.workflowId) {
40
+ problems.push(`${p}: ${a.type} to another workflow is not supported`);
41
+ continue;
42
+ }
43
+ if (a.stepId && !stepIds.has(a.stepId)) {
44
+ problems.push(`${p}: no step "${a.stepId}"`);
45
+ continue;
46
+ }
47
+ const action = {
48
+ name: a.name || "",
49
+ type: a.type,
50
+ criteria: criteria(a.criteria, p, problems),
51
+ };
52
+ if (a.stepId) action.stepId = a.stepId;
53
+ if (a.type === "retry") {
54
+ action.retryLimit = Number.isFinite(a.retryLimit) ? a.retryLimit : 1;
55
+ action.retryAfter = Number.isFinite(a.retryAfter) ? a.retryAfter : 0;
56
+ }
57
+ out.push(action);
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /** The step's own actions, then the workflow's minus those the step overrides by name. */
63
+ function merge(own, inherited) {
64
+ const names = new Set(own.map((a) => a.name).filter(Boolean));
65
+ return [...own, ...inherited.filter((a) => !a.name || !names.has(a.name))];
66
+ }
67
+
68
+ function unsupportedRefs(value, where, problems) {
69
+ for (const ref of collectRefs(value)) {
70
+ if (ref.kind === "workflow") {
71
+ problems.push(`${where}: ${ref.expr} refers to another workflow, which is not supported`);
72
+ }
73
+ }
74
+ }
75
+
76
+ function compileStep(doc, step, workflow, sources, stepIds, problems) {
77
+ const where = `steps.${step.stepId}`;
78
+ if (step.workflowId) {
79
+ problems.push(`${where}: calls workflow "${step.workflowId}", which is not supported`);
80
+ return null;
81
+ }
82
+ const op = resolveOperation(step, sources);
83
+ if (!op) {
84
+ const ref = step.operationId || step.operationPath || "(no operation)";
85
+ problems.push(
86
+ `${where}: operation ${ref} was not found in ${[...sources.keys()].join(", ") || "any source"}`,
87
+ );
88
+ return null;
89
+ }
90
+ const params = [];
91
+ const parameters = [];
92
+ for (const [i, raw] of (Array.isArray(step.parameters) ? step.parameters : []).entries()) {
93
+ const p = resolveComponentRef(doc, raw, "parameters");
94
+ if (!p?.name) continue;
95
+ if (!PARAM_LOCATIONS.has(p.in)) {
96
+ problems.push(`${where}.parameters[${i}]: "in" must be path, query, header or cookie`);
97
+ continue;
98
+ }
99
+ params.push({ name: p.name, in: p.in });
100
+ parameters.push({ name: p.name, in: p.in, value: p.value });
101
+ unsupportedRefs(p.value, `${where}.parameters[${i}]`, problems);
102
+ }
103
+ let requestBody = null;
104
+ if (step.requestBody && typeof step.requestBody === "object") {
105
+ const contentType = step.requestBody.contentType || "application/json";
106
+ requestBody = { contentType, payload: step.requestBody.payload ?? null };
107
+ if (Array.isArray(step.requestBody.replacements)) {
108
+ requestBody.replacements = step.requestBody.replacements
109
+ .filter((r) => r && typeof r.target === "string")
110
+ .map((r) => ({ target: r.target, value: r.value }));
111
+ }
112
+ params.push({ name: "body", in: "body", contentType });
113
+ unsupportedRefs(step.requestBody, `${where}.requestBody`, problems);
114
+ }
115
+ const onSuccess = actions(
116
+ doc,
117
+ step.onSuccess,
118
+ "successActions",
119
+ `${where}.onSuccess`,
120
+ stepIds,
121
+ problems,
122
+ );
123
+ const onFailure = actions(
124
+ doc,
125
+ step.onFailure,
126
+ "failureActions",
127
+ `${where}.onFailure`,
128
+ stepIds,
129
+ problems,
130
+ );
131
+ const outputs = {};
132
+ for (const [name, expr] of Object.entries(step.outputs || {})) {
133
+ outputs[name] = expr;
134
+ unsupportedRefs(expr, `${where}.outputs.${name}`, problems);
135
+ }
136
+ return {
137
+ stepId: step.stepId,
138
+ description: step.description || "",
139
+ operation: {
140
+ source: op.source,
141
+ method: op.method.toUpperCase(),
142
+ path: op.path,
143
+ params,
144
+ security: Array.isArray(op.security)
145
+ ? op.security.filter((s) => s && typeof s === "object")
146
+ : [],
147
+ },
148
+ parameters,
149
+ requestBody,
150
+ successCriteria: criteria(step.successCriteria, `${where}.successCriteria`, problems),
151
+ onSuccess: merge(onSuccess, workflow.successActions),
152
+ onFailure: merge(onFailure, workflow.failureActions),
153
+ outputs,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * @param {object} arazzo A parsed Arazzo document
159
+ * @param {object} options
160
+ * @param {Map<string, {byId: Map, byPath: Map}>} options.sources operation indexes by Arazzo
161
+ * source description name (`indexOperations` / `indexResolvedSpec`); a step resolves against
162
+ * them and its `operation.source` is the index's name
163
+ * @param {(problem: { level: string, path: string, message: string }) => void} [options.onProblem]
164
+ * called once per workflow that is left out, with every reason joined
165
+ * @returns {{ tools: object[], workflows: Record<string, object> }} a tool is
166
+ * `{ name, description, inputSchema, annotations, workflow }`; a workflow is
167
+ * `{ workflowId, summary, description, inputs, steps, outputs }`
168
+ */
169
+ export function compileWorkflows(arazzo, options = {}) {
170
+ const sources = options.sources || new Map();
171
+ const tools = [];
172
+ const workflows = {};
173
+ const list = Array.isArray(arazzo?.workflows) ? arazzo.workflows : [];
174
+ for (const [wi, w] of list.entries()) {
175
+ if (!w?.workflowId) continue;
176
+ const problems = [];
177
+ const stepsRaw = Array.isArray(w.steps) ? w.steps : [];
178
+ const stepIds = new Set(stepsRaw.map((s) => s?.stepId).filter(Boolean));
179
+ const shared = {
180
+ successActions: actions(
181
+ arazzo,
182
+ w.successActions,
183
+ "successActions",
184
+ "successActions",
185
+ stepIds,
186
+ problems,
187
+ ),
188
+ failureActions: actions(
189
+ arazzo,
190
+ w.failureActions,
191
+ "failureActions",
192
+ "failureActions",
193
+ stepIds,
194
+ problems,
195
+ ),
196
+ };
197
+ const steps = stepsRaw
198
+ .filter((s) => s?.stepId)
199
+ .map((s) => compileStep(arazzo, s, shared, sources, stepIds, problems))
200
+ .filter(Boolean);
201
+ if (!steps.length) problems.push("has no runnable step");
202
+ const outputs = {};
203
+ for (const [name, expr] of Object.entries(w.outputs || {})) {
204
+ outputs[name] = expr;
205
+ unsupportedRefs(expr, `outputs.${name}`, problems);
206
+ }
207
+ if (problems.length) {
208
+ options.onProblem?.({
209
+ level: "error",
210
+ path: `workflows[${wi}]`,
211
+ message: `workflow ${w.workflowId} is left out of the MCP export: ${problems.join("; ")}`,
212
+ });
213
+ continue;
214
+ }
215
+ const inputs = w.inputs ? toJsonSchema(w.inputs, arazzo) : { type: "object", properties: {} };
216
+ if (!inputs.type) inputs.type = "object";
217
+ if (!inputs.properties) inputs.properties = {};
218
+ const methods = steps.map((s) => s.operation.method);
219
+ tools.push({
220
+ name: toSnakeCase(w.workflowId),
221
+ description: sanitizeDescription(w.summary || w.description || w.workflowId),
222
+ inputSchema: inputs,
223
+ annotations: {
224
+ readOnlyHint: methods.every((m) => m === "GET"),
225
+ destructiveHint: methods.some((m) => ["PUT", "PATCH", "DELETE"].includes(m)),
226
+ idempotentHint: false,
227
+ },
228
+ workflow: w.workflowId,
229
+ });
230
+ workflows[w.workflowId] = {
231
+ workflowId: w.workflowId,
232
+ summary: w.summary || "",
233
+ description: w.description || "",
234
+ inputs,
235
+ steps,
236
+ outputs,
237
+ };
238
+ }
239
+ return { tools, workflows };
240
+ }