@wootsup/mcp 0.1.0-rc.8 → 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 (171) hide show
  1. package/README.md +7 -7
  2. package/dist/index.d.ts +19 -0
  3. package/dist/index.js +72 -6
  4. package/dist/index.js.map +1 -1
  5. package/dist/modules/apimapper/cache.d.ts +2 -2
  6. package/dist/modules/apimapper/cache.js +107 -25
  7. package/dist/modules/apimapper/cache.js.map +1 -1
  8. package/dist/modules/apimapper/client.d.ts +40 -0
  9. package/dist/modules/apimapper/client.js +82 -12
  10. package/dist/modules/apimapper/client.js.map +1 -1
  11. package/dist/modules/apimapper/connections-format.d.ts +51 -0
  12. package/dist/modules/apimapper/connections-format.js +261 -0
  13. package/dist/modules/apimapper/connections-format.js.map +1 -0
  14. package/dist/modules/apimapper/connections-trim.d.ts +82 -0
  15. package/dist/modules/apimapper/connections-trim.js +224 -0
  16. package/dist/modules/apimapper/connections-trim.js.map +1 -0
  17. package/dist/modules/apimapper/connections.d.ts +14 -2
  18. package/dist/modules/apimapper/connections.js +447 -143
  19. package/dist/modules/apimapper/connections.js.map +1 -1
  20. package/dist/modules/apimapper/credentials-format.d.ts +21 -0
  21. package/dist/modules/apimapper/credentials-format.js +145 -0
  22. package/dist/modules/apimapper/credentials-format.js.map +1 -0
  23. package/dist/modules/apimapper/credentials.d.ts +12 -2
  24. package/dist/modules/apimapper/credentials.js +253 -72
  25. package/dist/modules/apimapper/credentials.js.map +1 -1
  26. package/dist/modules/apimapper/diagnose.d.ts +54 -2
  27. package/dist/modules/apimapper/diagnose.js +193 -11
  28. package/dist/modules/apimapper/diagnose.js.map +1 -1
  29. package/dist/modules/apimapper/elicitation.d.ts +54 -0
  30. package/dist/modules/apimapper/elicitation.js +90 -0
  31. package/dist/modules/apimapper/elicitation.js.map +1 -0
  32. package/dist/modules/apimapper/flows-format.d.ts +50 -0
  33. package/dist/modules/apimapper/flows-format.js +318 -0
  34. package/dist/modules/apimapper/flows-format.js.map +1 -0
  35. package/dist/modules/apimapper/flows.d.ts +13 -2
  36. package/dist/modules/apimapper/flows.js +325 -118
  37. package/dist/modules/apimapper/flows.js.map +1 -1
  38. package/dist/modules/apimapper/gateway/advanced-tool.d.ts +9 -0
  39. package/dist/modules/apimapper/gateway/advanced-tool.js +214 -0
  40. package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -0
  41. package/dist/modules/apimapper/gateway/capturing-server.d.ts +81 -0
  42. package/dist/modules/apimapper/gateway/capturing-server.js +87 -0
  43. package/dist/modules/apimapper/gateway/capturing-server.js.map +1 -0
  44. package/dist/modules/apimapper/gateway/essentials.d.ts +4 -0
  45. package/dist/modules/apimapper/gateway/essentials.js +28 -0
  46. package/dist/modules/apimapper/gateway/essentials.js.map +1 -0
  47. package/dist/modules/apimapper/gateway/test-support.d.ts +17 -0
  48. package/dist/modules/apimapper/gateway/test-support.js +43 -0
  49. package/dist/modules/apimapper/gateway/test-support.js.map +1 -0
  50. package/dist/modules/apimapper/get-skill.d.ts +3 -3
  51. package/dist/modules/apimapper/get-skill.js +4 -2
  52. package/dist/modules/apimapper/get-skill.js.map +1 -1
  53. package/dist/modules/apimapper/graph-builder.js +1 -1
  54. package/dist/modules/apimapper/graph-builder.js.map +1 -1
  55. package/dist/modules/apimapper/graph.d.ts +2 -2
  56. package/dist/modules/apimapper/graph.js +165 -34
  57. package/dist/modules/apimapper/graph.js.map +1 -1
  58. package/dist/modules/apimapper/index.d.ts +17 -1
  59. package/dist/modules/apimapper/index.js +66 -17
  60. package/dist/modules/apimapper/index.js.map +1 -1
  61. package/dist/modules/apimapper/inspect.d.ts +3 -2
  62. package/dist/modules/apimapper/inspect.js +97 -13
  63. package/dist/modules/apimapper/inspect.js.map +1 -1
  64. package/dist/modules/apimapper/library.d.ts +2 -2
  65. package/dist/modules/apimapper/library.js +303 -60
  66. package/dist/modules/apimapper/library.js.map +1 -1
  67. package/dist/modules/apimapper/license-format.d.ts +22 -0
  68. package/dist/modules/apimapper/license-format.js +149 -0
  69. package/dist/modules/apimapper/license-format.js.map +1 -0
  70. package/dist/modules/apimapper/license.d.ts +16 -2
  71. package/dist/modules/apimapper/license.js +85 -37
  72. package/dist/modules/apimapper/license.js.map +1 -1
  73. package/dist/modules/apimapper/local-sources.d.ts +2 -2
  74. package/dist/modules/apimapper/local-sources.js +58 -30
  75. package/dist/modules/apimapper/local-sources.js.map +1 -1
  76. package/dist/modules/apimapper/misc.d.ts +30 -2
  77. package/dist/modules/apimapper/misc.js +129 -50
  78. package/dist/modules/apimapper/misc.js.map +1 -1
  79. package/dist/modules/apimapper/node-schema.d.ts +52 -0
  80. package/dist/modules/apimapper/node-schema.js +70 -2
  81. package/dist/modules/apimapper/node-schema.js.map +1 -1
  82. package/dist/modules/apimapper/normalizers.d.ts +1 -0
  83. package/dist/modules/apimapper/normalizers.js +51 -0
  84. package/dist/modules/apimapper/normalizers.js.map +1 -1
  85. package/dist/modules/apimapper/onboarding.d.ts +48 -2
  86. package/dist/modules/apimapper/onboarding.js +324 -17
  87. package/dist/modules/apimapper/onboarding.js.map +1 -1
  88. package/dist/modules/apimapper/read-cache.d.ts +31 -2
  89. package/dist/modules/apimapper/read-cache.js +20 -6
  90. package/dist/modules/apimapper/read-cache.js.map +1 -1
  91. package/dist/modules/apimapper/render/_shared.d.ts +24 -0
  92. package/dist/modules/apimapper/render/_shared.js +84 -0
  93. package/dist/modules/apimapper/render/_shared.js.map +1 -0
  94. package/dist/modules/apimapper/render/dag.d.ts +18 -0
  95. package/dist/modules/apimapper/render/dag.js +70 -0
  96. package/dist/modules/apimapper/render/dag.js.map +1 -0
  97. package/dist/modules/apimapper/render/index.d.ts +2 -0
  98. package/dist/modules/apimapper/render/index.js +112 -0
  99. package/dist/modules/apimapper/render/index.js.map +1 -0
  100. package/dist/modules/apimapper/render/renderers/chart-bar.d.ts +2 -0
  101. package/dist/modules/apimapper/render/renderers/chart-bar.js +70 -0
  102. package/dist/modules/apimapper/render/renderers/chart-bar.js.map +1 -0
  103. package/dist/modules/apimapper/render/renderers/chart-line.d.ts +2 -0
  104. package/dist/modules/apimapper/render/renderers/chart-line.js +71 -0
  105. package/dist/modules/apimapper/render/renderers/chart-line.js.map +1 -0
  106. package/dist/modules/apimapper/render/renderers/diff.d.ts +2 -0
  107. package/dist/modules/apimapper/render/renderers/diff.js +154 -0
  108. package/dist/modules/apimapper/render/renderers/diff.js.map +1 -0
  109. package/dist/modules/apimapper/render/renderers/flow-diagram.d.ts +1 -0
  110. package/dist/modules/apimapper/render/renderers/flow-diagram.js +180 -0
  111. package/dist/modules/apimapper/render/renderers/flow-diagram.js.map +1 -0
  112. package/dist/modules/apimapper/render/renderers/json-tree.d.ts +2 -0
  113. package/dist/modules/apimapper/render/renderers/json-tree.js +87 -0
  114. package/dist/modules/apimapper/render/renderers/json-tree.js.map +1 -0
  115. package/dist/modules/apimapper/render/renderers/schema-diagram.d.ts +2 -0
  116. package/dist/modules/apimapper/render/renderers/schema-diagram.js +83 -0
  117. package/dist/modules/apimapper/render/renderers/schema-diagram.js.map +1 -0
  118. package/dist/modules/apimapper/render/renderers/table.d.ts +2 -0
  119. package/dist/modules/apimapper/render/renderers/table.js +75 -0
  120. package/dist/modules/apimapper/render/renderers/table.js.map +1 -0
  121. package/dist/modules/apimapper/render/schemas.d.ts +23 -0
  122. package/dist/modules/apimapper/render/schemas.js +56 -0
  123. package/dist/modules/apimapper/render/schemas.js.map +1 -0
  124. package/dist/modules/apimapper/render/secret-masking.d.ts +5 -0
  125. package/dist/modules/apimapper/render/secret-masking.js +51 -0
  126. package/dist/modules/apimapper/render/secret-masking.js.map +1 -0
  127. package/dist/modules/apimapper/render/sidecar.d.ts +21 -0
  128. package/dist/modules/apimapper/render/sidecar.js +66 -0
  129. package/dist/modules/apimapper/render/sidecar.js.map +1 -0
  130. package/dist/modules/apimapper/render/token-cap.d.ts +21 -0
  131. package/dist/modules/apimapper/render/token-cap.js +57 -0
  132. package/dist/modules/apimapper/render/token-cap.js.map +1 -0
  133. package/dist/modules/apimapper/schema.d.ts +2 -2
  134. package/dist/modules/apimapper/schema.js +100 -32
  135. package/dist/modules/apimapper/schema.js.map +1 -1
  136. package/dist/modules/apimapper/settings-format.d.ts +23 -0
  137. package/dist/modules/apimapper/settings-format.js +135 -0
  138. package/dist/modules/apimapper/settings-format.js.map +1 -0
  139. package/dist/modules/apimapper/settings.d.ts +2 -2
  140. package/dist/modules/apimapper/settings.js +101 -40
  141. package/dist/modules/apimapper/settings.js.map +1 -1
  142. package/dist/modules/apimapper/skill-resources.d.ts +2 -2
  143. package/dist/modules/apimapper/skill-resources.js.map +1 -1
  144. package/dist/modules/apimapper/token-baseline.harness.d.ts +91 -0
  145. package/dist/modules/apimapper/token-baseline.harness.js +291 -0
  146. package/dist/modules/apimapper/token-baseline.harness.js.map +1 -0
  147. package/dist/modules/apimapper/toolslist-size.d.ts +55 -0
  148. package/dist/modules/apimapper/toolslist-size.js +190 -0
  149. package/dist/modules/apimapper/toolslist-size.js.map +1 -0
  150. package/dist/modules/apimapper/types.d.ts +23 -8
  151. package/dist/modules/apimapper/types.js +26 -1
  152. package/dist/modules/apimapper/types.js.map +1 -1
  153. package/dist/modules/apimapper/use-profile.d.ts +21 -0
  154. package/dist/modules/apimapper/use-profile.js +56 -2
  155. package/dist/modules/apimapper/use-profile.js.map +1 -1
  156. package/dist/modules/apimapper/workflows.d.ts +2 -2
  157. package/dist/modules/apimapper/workflows.js +143 -16
  158. package/dist/modules/apimapper/workflows.js.map +1 -1
  159. package/dist/platform/index.js +44 -5
  160. package/dist/platform/index.js.map +1 -1
  161. package/dist/setup-cli.d.ts +53 -0
  162. package/dist/setup-cli.js +135 -6
  163. package/dist/setup-cli.js.map +1 -1
  164. package/docs/architecture.md +1 -1
  165. package/docs/tools.md +1 -1
  166. package/manifest.json +12 -3
  167. package/package.json +9 -4
  168. package/skills/apimapper/SKILL.md +1 -1
  169. package/skills/apimapper/reference/render.md +132 -0
  170. package/skills/apimapper/reference/troubleshooting.md +1 -1
  171. package/skills/apimapper/reference/yootheme.md +1 -1
@@ -1,79 +1,88 @@
1
1
  import { z } from "zod";
2
- import { formatResult, autoFormatTable, readOnly, creating, mutating, destructive, } from "@getimo/mcp-toolkit";
2
+ import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, createProgressReporter, elicitChoice, } from "@getimo/mcp-toolkit";
3
3
  import { request, hintFor } from "./client.js";
4
- import { nodeSchema } from "./node-schema.js";
4
+ import { toRows } from "./types.js";
5
+ import { nodeSchema, edgeSchema, filterByNameQuery } from "./node-schema.js";
5
6
  import { unwrapEntity } from "./envelope.js";
6
- export function registerFlowTools(server) {
7
+ import { ambiguityFallbackError, } from "./elicitation.js";
8
+ import { buildFlowVisualization, SIDECAR_RESPONSE_MAX_CHARS, } from "./render/sidecar.js";
9
+ import { FLOW_TABLE_COLUMNS, FLOW_COMPACT_COLUMNS, FLOW_LIST_NEXT_STEPS, isFlowCompiled, mapFlowRow, compactFlowRow, buildFlowDetail, buildFlowTraceTimeline, buildImportValidateDetail, } from "./flows-format.js";
10
+ /**
11
+ * Register the flow CRUD + compile + detect-schema + trace + export/import
12
+ * tools.
13
+ *
14
+ * @param server the tool registrar (essentials forward, rest captured).
15
+ * @param elicitation optional elicitation capability (the real McpServer, or
16
+ * any `{ server: { elicitInput } }`). Supplied only by the host wiring in
17
+ * index.ts. When omitted, `flow_compile` falls back to a structured error
18
+ * on genuine flow ambiguity instead of prompting.
19
+ */
20
+ export function registerFlowTools(server, elicitation) {
7
21
  // ── apimapper_flow_list ────────────────────────────────────────────
8
22
  server.registerTool("apimapper_flow_list", {
9
23
  title: "List Flows",
10
- description: "List all API Mapper flows. Use apimapper_flow_get for full structure." +
11
- "\n\nExample:\n apimapper_flow_list({ limit: 25 })",
24
+ // rc.10 A5 (2026-05-19) anti-thrash hint: AI was defaulting to
25
+ // limit:500 reflexively even though typical installs have <50 flows.
26
+ description: "List all API Mapper flows. The default limit of 100 already covers a typical install — " +
27
+ "most customers have 5-50 flows, increase the limit only if you have a specific reason. " +
28
+ "Use apimapper_flow_get for the full structure (nodes + edges) of a single flow." +
29
+ "\n\nExample:\n apimapper_flow_list({}) // returns up to 100 flows in one call\n" +
30
+ " apimapper_flow_list({ compiled: 'no' }) // filter to draft/incomplete flows",
12
31
  inputSchema: {
13
32
  source: z
14
33
  .enum(["user", "demo", "library", "devtools", "all"])
15
34
  .default("all")
16
35
  .describe("Filter by origin"),
17
36
  compiled: z.enum(["any", "yes", "no"]).default("any").describe("Filter by compile status"),
18
- limit: z.number().min(1).max(500).default(100).describe("Max items (1-500)"),
37
+ limit: z.number().min(1).max(500).default(100).describe("Max items (1-500). Default 100 already covers a typical install — do not raise unless you have evidence the list is larger."),
38
+ // W1.18 (F-32) — case-insensitive substring filter applied
39
+ // in-memory AFTER the upstream fetch (no per-name REST call).
40
+ // Min 2 chars at the schema boundary blocks the single-char
41
+ // probe-thrash pattern observed in the Maria-walkthrough logs.
42
+ name_query: z
43
+ .string()
44
+ .min(2)
45
+ .optional()
46
+ .describe("Case-insensitive substring filter on flow name. Applied in-memory after the upstream fetch — does NOT change the REST query. Must be 2+ characters; single-char probes are blocked. Combine with `source`/`compiled` for narrower results."),
19
47
  },
20
- annotations: readOnly(),
21
- }, async ({ source, compiled, limit }) => {
48
+ annotations: readOnly({ title: "List Flows", openWorld: true }),
49
+ }, async ({ source, compiled, limit, name_query }) => {
22
50
  const r = await request("/flows");
23
51
  if (!r.success) {
24
- return formatResult({
25
- error: r.error,
26
- status: r.status,
27
- errorCode: r.errorCode,
28
- context: { source, compiled, limit },
29
- hint: hintFor(r.errorCode),
30
- }, true);
52
+ return errorResult({
53
+ message: r.error ?? "flow list failed",
54
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
55
+ suggestion: hintFor(r.errorCode),
56
+ details: { source, compiled, limit, name_query },
57
+ });
31
58
  }
32
59
  let items = Array.isArray(r.data?.flows) ? r.data.flows : [];
33
- // Derive `is_compiled` from REST shape: backend returns `compiledAt`
34
- // (ISO timestamp string or null) and a `compiled` array of artifacts.
35
- // Either indicates the flow has been compiled. Normalising here keeps
36
- // downstream filtering + display logic uniform regardless of platform.
37
- const isCompiledOf = (f) => {
38
- if (typeof f.is_compiled === "boolean")
39
- return f.is_compiled;
40
- const raw = f;
41
- if (typeof raw.compiledAt === "string" && raw.compiledAt.length > 0)
42
- return true;
43
- if (Array.isArray(raw.compiled) && raw.compiled.length > 0)
44
- return true;
45
- return false;
46
- };
60
+ // `is_compiled` is derived from the REST shape (compiledAt / compiled[])
61
+ // by isFlowCompiled in flows-format.ts shared between the filter and
62
+ // the row map so display + filtering stay uniform.
47
63
  if (source !== "all")
48
64
  items = items.filter((f) => f.source === source);
49
65
  if (compiled === "yes")
50
- items = items.filter((f) => isCompiledOf(f));
66
+ items = items.filter((f) => isFlowCompiled(f));
51
67
  if (compiled === "no")
52
- items = items.filter((f) => !isCompiledOf(f));
68
+ items = items.filter((f) => !isFlowCompiled(f));
69
+ // W1.18 (F-32) — filter BEFORE slice so limit applies to the matched
70
+ // subset, not the haystack. e.g. limit=10 + name_query='pexels'
71
+ // returns up to 10 pexels-matching flows, not the first 10 flows
72
+ // filtered to pexels-matching.
73
+ items = filterByNameQuery(items, name_query);
53
74
  items = items.slice(0, limit);
54
- return autoFormatTable(items.map((f) => ({
55
- id: f.id,
56
- name: f.name,
57
- source: f.source || "—",
58
- nodes: f.node_count ?? "?",
59
- compiled: isCompiledOf(f) ? "✓" : "✗",
60
- types: (f.node_types || []).join(","),
61
- })), {
62
- columns: [
63
- // Width 36 accommodates 30+ char IDs (e.g. legacy template
64
- // flows like flow_calendly_jla_229fcbce95b7 = 30 chars).
65
- // Truncation produces orphan IDs that downstream tools fail
66
- // to resolve via flow_get. The +6 headroom matches the
67
- // longest historical IDs without bloating the table.
68
- { key: "id", label: "ID", width: 36 },
69
- { key: "name", label: "NAME", width: 32 },
70
- { key: "source", label: "SRC", width: 10 },
71
- { key: "nodes", label: "N", width: 4 },
72
- { key: "compiled", label: "C", width: 3 },
73
- { key: "types", label: "TYPES", width: 36 },
74
- ],
75
+ // W3.1 tableResult: ASCII table for the LLM + typed DataTable payload
76
+ // for the Rich Card. T1 (W3.2): explicit compactColumns/compactMap drop
77
+ // NODES/TYPES at 21+ rows. IA-7: id stays llmOnly. IA-10: the footer
78
+ // carries the next-step guidance.
79
+ return tableResult(toRows(items), {
80
+ columns: FLOW_TABLE_COLUMNS,
81
+ compactColumns: FLOW_COMPACT_COLUMNS,
82
+ map: mapFlowRow,
83
+ compactMap: compactFlowRow,
75
84
  header: (n) => `${n} flows`,
76
- footer: "Use apimapper_flow_get <id> for full structure.",
85
+ footer: FLOW_LIST_NEXT_STEPS,
77
86
  });
78
87
  });
79
88
  // ── apimapper_flow_get ─────────────────────────────────────────────
@@ -84,19 +93,34 @@ export function registerFlowTools(server) {
84
93
  inputSchema: {
85
94
  id: z.string().describe('Flow ID (e.g., "flow_aCfOEpwCtVZnPFwPOwO61"). Use apimapper_flow_list.'),
86
95
  },
87
- annotations: readOnly(),
96
+ annotations: readOnly({ title: "Get Flow", openWorld: true }),
88
97
  }, async ({ id }) => {
89
98
  // PHP wraps via fromControllerResponse($response, 'flow') →
90
99
  // `{success:true, flow:{…}}`. Audit: F-A2-01.
91
100
  const r = await request(`/flows/${encodeURIComponent(id)}`);
92
101
  if (!r.success) {
93
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { id }, hint: hintFor(r.errorCode) }, true);
102
+ return errorResult({
103
+ message: r.error ?? "flow get failed",
104
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
105
+ suggestion: hintFor(r.errorCode),
106
+ details: { id },
107
+ });
94
108
  }
95
109
  const flow = unwrapEntity(r.data, "flow");
96
110
  if (!flow || Object.keys(flow).length === 0) {
97
- return formatResult({ error: "flow not found", status: r.status, context: { id }, hint: hintFor("not_found") }, true);
111
+ return errorResult({
112
+ message: "flow not found",
113
+ code: r.status ? String(r.status) : "not_found",
114
+ suggestion: hintFor("not_found"),
115
+ details: { id },
116
+ });
98
117
  }
99
- return formatResult(flow, false, { maxChars: 8000 });
118
+ // W3.1 detailResult: grouped key-value detail for the Rich Card.
119
+ // IA-7: opaque IDs are copyable code entries. IA-10: a dedicated
120
+ // "Next steps" group carries the follow-up calls. The W2 flow
121
+ // visualization (ASCII diagram) is routed via detailResult.appendText
122
+ // so it reaches the LLM output without cluttering the Rich Card.
123
+ return buildFlowDetail(id, flow);
100
124
  });
101
125
  // ── apimapper_flow_create ──────────────────────────────────────────
102
126
  server.registerTool("apimapper_flow_create", {
@@ -116,15 +140,17 @@ export function registerFlowTools(server) {
116
140
  'source.connectionId, local-source.contentType, filter.conditions, ' +
117
141
  'transform.expression, merge.strategy ∈ {concat, join}.'),
118
142
  edges: z
119
- .array(z.record(z.string(), z.unknown()))
120
- .describe('Edge array. Each: {id, source, target, targetHandle?, sourceHandle?}. ' +
121
- 'For merge: targetHandle="input-A" (first input) or "input-B" (second).'),
143
+ .array(edgeSchema)
144
+ .describe('Edge array. Each: {id?, source, target, sourceHandle?, targetHandle?}. ' +
145
+ 'source + target are required non-empty node-ids. ' +
146
+ 'For merge nodes: targetHandle="input-0" (first input) or "input-1" (second). ' +
147
+ 'Legacy "input-A"/"input-B" still accepted for backward-compat.'),
122
148
  viewport: z
123
149
  .object({ x: z.number(), y: z.number(), zoom: z.number() })
124
150
  .optional()
125
151
  .describe('React-Flow viewport (e.g., {"x":0,"y":0,"zoom":1})'),
126
152
  },
127
- annotations: creating(),
153
+ annotations: creating({ title: "Create Flow", openWorld: true }),
128
154
  }, async (input) => {
129
155
  // PHP fromControllerResponse(_, 'flow', 201) → {success, flow:{…}}.
130
156
  // Audit: F-A2-02.
@@ -133,23 +159,39 @@ export function registerFlowTools(server) {
133
159
  body: JSON.stringify(input),
134
160
  });
135
161
  if (!r.success) {
136
- return formatResult({
137
- error: r.error,
138
- status: r.status,
139
- errorCode: r.errorCode,
140
- context: { name: input.name, node_count: input.nodes.length },
141
- hint: hintFor(r.errorCode),
142
- }, true);
162
+ return errorResult({
163
+ message: r.error ?? "flow create failed",
164
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
165
+ suggestion: hintFor(r.errorCode),
166
+ details: { name: input.name, node_count: input.nodes.length },
167
+ });
143
168
  }
144
169
  const flow = unwrapEntity(r.data, "flow");
170
+ // Sidecar: flow_create always has input.nodes + input.edges
171
+ // (Zod-validated), so the visualization always renders unless
172
+ // APIMAPPER_AUTO_VISUALIZE_FLOWS="false". Additive + defensive —
173
+ // a render failure returns null and is omitted silently.
174
+ const viz = buildFlowVisualization({
175
+ nodes: input.nodes,
176
+ edges: input.edges,
177
+ id: flow?.id,
178
+ name: flow?.name,
179
+ });
145
180
  return formatResult({
146
181
  created: true,
147
182
  id: flow?.id,
148
183
  name: flow?.name,
149
184
  node_count: flow?.node_count,
150
185
  is_compiled: flow?.is_compiled ?? false,
151
- next: "Call apimapper_flow_compile to publish to YOOtheme.",
152
- }, false, { maxChars: 2000 });
186
+ ...(viz && { _visualization: viz }),
187
+ // F-3.1 (Pass-1 consolidation): IA-10 uniformity — next_steps[]
188
+ // (array, even if single-element) matches the structured
189
+ // next-step convention used by the list-result footers and the
190
+ // detailResult builders. Flat `next: string` was the holdout.
191
+ next_steps: ["Call apimapper_flow_compile to publish to YOOtheme."],
192
+ }, false,
193
+ // Headroom for the sidecar — see SIDECAR_RESPONSE_MAX_CHARS.
194
+ { maxChars: SIDECAR_RESPONSE_MAX_CHARS });
153
195
  });
154
196
  // ── apimapper_flow_update ──────────────────────────────────────────
155
197
  server.registerTool("apimapper_flow_update", {
@@ -163,7 +205,7 @@ export function registerFlowTools(server) {
163
205
  .record(z.string(), z.unknown())
164
206
  .describe('Fields to patch (e.g., {"nodes":[...updated nodes...]} or {"name":"Renamed"})'),
165
207
  },
166
- annotations: mutating(),
208
+ annotations: mutating({ title: "Update Flow", openWorld: true }),
167
209
  }, async ({ id, patch }) => {
168
210
  // PHP fromControllerResponse(_, 'flow') → {success, flow:{…}}. On 409 the
169
211
  // body shape is {currentVersion, error, code}. Audit: F-A2-03.
@@ -176,24 +218,44 @@ export function registerFlowTools(server) {
176
218
  // re-read + retry without a separate fetch.
177
219
  const envelope = (r.data && typeof r.data === "object" ? r.data : {});
178
220
  const currentVersion = typeof envelope.currentVersion === "number" ? envelope.currentVersion : undefined;
179
- return formatResult({
180
- error: r.error,
181
- status: r.status,
182
- errorCode: r.errorCode,
183
- currentVersion,
184
- context: { id },
185
- hint: hintFor(r.errorCode),
186
- }, true);
221
+ return errorResult({
222
+ message: r.error ?? "flow update failed",
223
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
224
+ suggestion: hintFor(r.errorCode),
225
+ details: { id, ...(currentVersion !== undefined ? { currentVersion } : {}) },
226
+ });
187
227
  }
188
228
  const flow = unwrapEntity(r.data, "flow");
229
+ // Sidecar: flow_update's patch is a generic record. The
230
+ // visualization is CONDITIONAL — render only when the patch
231
+ // carries a full graph (both nodes and edges as arrays). A
232
+ // name-only / metadata-only patch has no graph to draw.
233
+ const patchObj = (patch && typeof patch === "object" ? patch : {});
234
+ const patchHasGraph = Array.isArray(patchObj.nodes) && Array.isArray(patchObj.edges);
235
+ const viz = patchHasGraph
236
+ ? buildFlowVisualization({
237
+ nodes: patchObj.nodes,
238
+ edges: patchObj.edges,
239
+ id: flow?.id,
240
+ name: flow?.name,
241
+ })
242
+ : null;
189
243
  return formatResult({
190
244
  updated: true,
191
245
  id: flow?.id,
192
246
  name: flow?.name,
193
247
  version: flow?.version,
194
248
  is_compiled: flow?.is_compiled,
195
- next: flow?.is_compiled ? "Compile likely invalidated — call apimapper_flow_compile." : undefined,
196
- }, false, { maxChars: 2000 });
249
+ ...(viz && { _visualization: viz }),
250
+ // F-3.1 (Pass-1 consolidation): IA-10 uniformity — flat next has
251
+ // been migrated to next_steps[]. Empty array when no recompile is
252
+ // needed (so the key shape stays uniform across all paths).
253
+ next_steps: flow?.is_compiled
254
+ ? ["Compile likely invalidated — call apimapper_flow_compile."]
255
+ : [],
256
+ }, false,
257
+ // Headroom for the (conditional) sidecar — see SIDECAR_RESPONSE_MAX_CHARS.
258
+ { maxChars: SIDECAR_RESPONSE_MAX_CHARS });
197
259
  });
198
260
  // ── apimapper_flow_delete ──────────────────────────────────────────
199
261
  server.registerTool("apimapper_flow_delete", {
@@ -208,7 +270,7 @@ export function registerFlowTools(server) {
208
270
  .default(false)
209
271
  .describe("Must be true to execute. On confirm:false, returns a preview."),
210
272
  },
211
- annotations: destructive(),
273
+ annotations: destructive({ title: "Delete Flow", openWorld: true }),
212
274
  }, async ({ id, confirm }) => {
213
275
  if (!confirm) {
214
276
  // PHP fromControllerResponse(_, 'flow') wraps as {success, flow:{…}}.
@@ -232,7 +294,12 @@ export function registerFlowTools(server) {
232
294
  }
233
295
  const r = await request(`/flows/${encodeURIComponent(id)}`, { method: "DELETE" });
234
296
  if (!r.success) {
235
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { id }, hint: hintFor(r.errorCode) }, true);
297
+ return errorResult({
298
+ message: r.error ?? "flow delete failed",
299
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
300
+ suggestion: hintFor(r.errorCode),
301
+ details: { id },
302
+ });
236
303
  }
237
304
  return formatResult({ deleted: true, id }, false, { maxChars: 1500 });
238
305
  });
@@ -240,26 +307,94 @@ export function registerFlowTools(server) {
240
307
  server.registerTool("apimapper_flow_compile", {
241
308
  title: "Compile Flow",
242
309
  description: "Compile flow into executable pipeline (validation + topo sort + step generation + schema). " +
243
- "Required before flow appears in YOOtheme." +
244
- "\n\nExample:\n apimapper_flow_compile({ id: 'flow_Z2fLg70M84' })",
310
+ "Required before flow appears in YOOtheme. " +
311
+ "If `id` is omitted, the flow is resolved automatically — when exactly one flow exists " +
312
+ "it is compiled; when several exist the user is asked to pick." +
313
+ "\n\nExample:\n apimapper_flow_compile({ id: 'flow_Z2fLg70M84' })\n" +
314
+ " apimapper_flow_compile({}) // resolves the flow when only one exists",
245
315
  inputSchema: {
246
- id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
316
+ id: z
317
+ .string()
318
+ .optional()
319
+ .describe("Flow ID. Use apimapper_flow_list to find. Omit to resolve automatically " +
320
+ "when the install has a single flow."),
247
321
  },
248
- annotations: mutating(),
249
- }, async ({ id }) => {
322
+ annotations: mutating({ title: "Compile Flow", openWorld: true }),
323
+ }, async ({ id }, extra) => {
324
+ // W3.5 — coarse progress side-channel. `null` when the caller sent no
325
+ // progressToken; `progress?.report(...)` then no-ops.
326
+ const progress = extra ? createProgressReporter(extra) : null;
327
+ // W3.6 — resolve the flow when `id` is omitted. Exactly 1 flow →
328
+ // auto-pick; >1 → elicitChoice; 0 → structured error; elicitChoice
329
+ // null (unsupported client / declined) → structured candidate-list
330
+ // error. Done BEFORE the first progress report so a discovery round
331
+ // trip is not mistaken for compile progress.
332
+ let resolvedId = id;
333
+ if (!resolvedId) {
334
+ const lr = await request("/flows");
335
+ if (!lr.success) {
336
+ return errorResult({
337
+ message: lr.error ?? "flow lookup failed",
338
+ code: lr.errorCode ?? (lr.status ? String(lr.status) : undefined),
339
+ suggestion: hintFor(lr.errorCode),
340
+ details: {},
341
+ });
342
+ }
343
+ const flows = Array.isArray(lr.data?.flows) ? lr.data.flows : [];
344
+ if (flows.length === 0) {
345
+ return errorResult({
346
+ message: "No flows exist to compile.",
347
+ code: "flow_not_found",
348
+ suggestion: "Create one with apimapper_flow_create, then compile it.",
349
+ details: {},
350
+ });
351
+ }
352
+ if (flows.length === 1) {
353
+ resolvedId = flows[0].id;
354
+ }
355
+ else {
356
+ const candidates = flows.map((f) => ({
357
+ id: f.id,
358
+ label: f.name,
359
+ }));
360
+ const picked = elicitation
361
+ ? await elicitChoice(elicitation, "Several flows exist. Pick the flow to compile.", candidates.map((c) => c.id))
362
+ : null;
363
+ if (picked === null) {
364
+ return ambiguityFallbackError({
365
+ code: "flow_ambiguous",
366
+ paramName: "id",
367
+ what: "flows",
368
+ candidates,
369
+ });
370
+ }
371
+ resolvedId = picked;
372
+ }
373
+ }
374
+ await progress?.report(0, 1, "Compiling flow…");
250
375
  // PHP success body: {compiled: <CompiledFlow>, flowId: <id>}. Errors are
251
376
  // surfaced via HandlerResult::error(), not inside the 200 body. Audit:
252
377
  // F-A2-05 — the previous `errors[]` + `is_compiled===false` branch was
253
378
  // dead code.
254
- const r = await request(`/flows/${encodeURIComponent(id)}/compile`, { method: "POST" });
379
+ const r = await request(`/flows/${encodeURIComponent(resolvedId)}/compile`, { method: "POST" });
255
380
  if (!r.success) {
256
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { id }, hint: hintFor(r.errorCode) }, true);
381
+ return errorResult({
382
+ message: r.error ?? "flow compile failed",
383
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
384
+ suggestion: hintFor(r.errorCode),
385
+ details: { id: resolvedId },
386
+ });
257
387
  }
388
+ await progress?.report(1, 1, "Flow compiled");
258
389
  const dataObj = r.data && typeof r.data === "object" ? r.data : {};
259
390
  return formatResult({
260
391
  compiled: true,
261
- id: dataObj.flowId ?? id,
262
- next: "Source now appears in YOOtheme dropdown. Use apimapper_graph_preview to test items.",
392
+ id: dataObj.flowId ?? resolvedId,
393
+ // F-3.1 (Pass-1 consolidation): IA-10 uniformity next_steps[]
394
+ // matches the structured shape used by other tools.
395
+ next_steps: [
396
+ "Source now appears in YOOtheme dropdown. Use apimapper_graph_preview to test items.",
397
+ ],
263
398
  }, false, { maxChars: 2500 });
264
399
  });
265
400
  // ── apimapper_flow_detect_schema ───────────────────────────────────
@@ -271,15 +406,30 @@ export function registerFlowTools(server) {
271
406
  inputSchema: {
272
407
  id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
273
408
  },
274
- annotations: mutating(),
275
- }, async ({ id }) => {
276
- // PHP success body: {schema:{fields:[...], …}, sourceConnectionId}.
409
+ annotations: mutating({ title: "Detect Flow Schema", openWorld: true }),
410
+ }, async ({ id }, extra) => {
411
+ // W3.5 coarse progress side-channel. `null` when the caller sent no
412
+ // progressToken; `progress?.report(...)` then no-ops.
413
+ const progress = extra ? createProgressReporter(extra) : null;
414
+ await progress?.report(0, 1, "Sampling upstream API to detect schema…");
415
+ // PHP success body: {schema:{fields:[...], …}, sourceConnectionId,
416
+ // persisted, persist_error?}.
417
+ // H-2 fix (2026-05-27): backend now persists detected schema onto the
418
+ // output node so the next compile finds non-empty fields. `persisted`
419
+ // flag surfaces whether the write actually happened.
277
420
  // Audit: F-A2-06.
278
421
  const r = await request(`/flows/${encodeURIComponent(id)}/detect-schema`, { method: "POST" });
279
422
  if (!r.success) {
280
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { id }, hint: hintFor(r.errorCode) }, true);
423
+ return errorResult({
424
+ message: r.error ?? "flow detect-schema failed",
425
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
426
+ suggestion: hintFor(r.errorCode),
427
+ details: { id },
428
+ });
281
429
  }
430
+ await progress?.report(1, 1, "Schema detected");
282
431
  const fields = Array.isArray(r.data?.schema?.fields) ? r.data.schema.fields : [];
432
+ const persisted = r.data?.persisted === true;
283
433
  return formatResult({
284
434
  detected: true,
285
435
  id,
@@ -287,6 +437,16 @@ export function registerFlowTools(server) {
287
437
  field_count: fields.length,
288
438
  fields: fields.slice(0, 50),
289
439
  schema: r.data?.schema,
440
+ persisted,
441
+ ...(r.data?.persist_error ? { persist_error: r.data.persist_error } : {}),
442
+ next_steps: persisted
443
+ ? [
444
+ "Schema persisted to flow. Call apimapper_flow_compile next to publish.",
445
+ ]
446
+ : [
447
+ "Schema detected but NOT persisted (no output node yet, or save failed). " +
448
+ "Add an output node and call detect again, or inspect persist_error.",
449
+ ],
290
450
  }, false, { maxChars: 6000 });
291
451
  });
292
452
  // ── apimapper_flow_trace ───────────────────────────────────────────
@@ -298,43 +458,74 @@ export function registerFlowTools(server) {
298
458
  inputSchema: {
299
459
  id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
300
460
  },
301
- annotations: readOnly(),
461
+ annotations: readOnly({ title: "Trace Flow", openWorld: true }),
302
462
  }, async ({ id }) => {
303
463
  // PHP success body: {trace: {steps: [...], total_ms}}. Audit: F-A2-07.
304
464
  const r = await request(`/flows/${encodeURIComponent(id)}/trace`);
305
465
  if (!r.success) {
306
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { id }, hint: hintFor(r.errorCode) }, true);
466
+ return errorResult({
467
+ message: r.error ?? "flow trace failed",
468
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
469
+ suggestion: hintFor(r.errorCode),
470
+ details: { id },
471
+ });
307
472
  }
308
473
  const trace = r.data?.trace;
309
474
  const steps = Array.isArray(trace?.steps) ? trace.steps : [];
310
- return formatResult({
311
- id,
312
- total_ms: trace?.total_ms,
313
- step_count: steps.length,
314
- steps: steps.slice(0, 50),
315
- }, false, { maxChars: 6000 });
475
+ // W3.1 — timelineResult: the trace's per-step timing maps onto a
476
+ // chronological event timeline (display="event-timeline"). The step
477
+ // cap is preserved (50) so a pathological trace cannot bloat the
478
+ // response. buildFlowTraceTimeline does the defensive field reads.
479
+ return buildFlowTraceTimeline(id, steps.slice(0, 50), trace?.total_ms);
316
480
  });
317
481
  // ── apimapper_flow_export ──────────────────────────────────────────
482
+ // F-24 (W1.13): the export bundle contains connection REFERENCES only —
483
+ // never the linked credentials. Surface that explicitly in the response
484
+ // so downstream agents/customers cannot fatally assume "flow backup" =
485
+ // "credentials backup". The disclaimer is also advertised at tool
486
+ // registration time so AI clients see it without invoking.
487
+ const FLOW_EXPORT_CREDS_DISCLAIMER = "Credentials are NOT included in this bundle. The importing site must " +
488
+ "re-link credentials via apimapper_credential_create / apimapper_oauth_authorize_begin " +
489
+ "or via Admin-UI Credentials tab. Connection references in the bundle preserve the " +
490
+ "credential SHAPE (auth_type) but contain no secrets.";
318
491
  server.registerTool("apimapper_flow_export", {
319
492
  title: "Export Flow",
320
- description: "Export a flow as a portable JSON bundle (incl. connection refs)." +
493
+ description: "Export a flow as a portable bundle for import on another site. " +
494
+ "Response includes `credentials_not_exported: true` and a `warning` string " +
495
+ "— credentials are NOT part of the bundle for security; the importing site " +
496
+ "must re-link them via apimapper_credential_create / apimapper_oauth_authorize_begin." +
321
497
  "\n\nExample:\n apimapper_flow_export({ id: 'flow_Z2fLg70M84' })",
322
498
  inputSchema: {
323
499
  id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
324
500
  },
325
- annotations: readOnly(),
501
+ annotations: readOnly({ title: "Export Flow", openWorld: true }),
326
502
  }, async ({ id }) => {
327
503
  // PHP wraps as `{export: {version, flow, connectionReferences, …}}`. Wave
328
504
  // 1C may rename `export` → `data` for round-trip symmetry; accept both
329
505
  // shapes so the round-trip works regardless of which side ships first.
330
506
  // Audit: F-A2-08.
331
- const r = await request(`/flows/${encodeURIComponent(id)}/export`);
507
+ // Defense-in-depth (A5-P2 / W1.26 follow-up): the PHP backend is expected
508
+ // to strip credentials before export, but if that regresses, secrets must
509
+ // not land in the LLM context. `sanitize: true` routes the response
510
+ // through `credential-sanitizer.ts` (SECRET_KEYS covers auth_data /
511
+ // bearer_token / refresh_token / client_secret / etc.). Mirrors the
512
+ // belt-and-braces pattern at credentials.ts (credential_list / get).
513
+ const r = await request(`/flows/${encodeURIComponent(id)}/export`, {}, { sanitize: true });
332
514
  if (!r.success) {
333
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { id }, hint: hintFor(r.errorCode) }, true);
515
+ return errorResult({
516
+ message: r.error ?? "flow export failed",
517
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
518
+ suggestion: hintFor(r.errorCode),
519
+ details: { id },
520
+ });
334
521
  }
335
522
  const envelope = (r.data && typeof r.data === "object" ? r.data : {});
336
523
  const bundle = envelope.data ?? envelope.export ?? envelope;
337
- return formatResult(bundle ?? {}, false, { maxChars: 12000 });
524
+ return formatResult({
525
+ bundle: bundle ?? {},
526
+ credentials_not_exported: true,
527
+ warning: FLOW_EXPORT_CREDS_DISCLAIMER,
528
+ }, false, { maxChars: 12000 });
338
529
  });
339
530
  // ── apimapper_flow_import_validate ─────────────────────────────────
340
531
  server.registerTool("apimapper_flow_import_validate", {
@@ -347,7 +538,7 @@ export function registerFlowTools(server) {
347
538
  .record(z.string(), z.unknown())
348
539
  .describe("Flow bundle JSON (from apimapper_flow_export or external source)"),
349
540
  },
350
- annotations: readOnly(),
541
+ annotations: readOnly({ title: "Validate Flow Import", openWorld: true }),
351
542
  }, async ({ bundle }) => {
352
543
  // PHP valid body: {valid, flowName, exportedFrom, connectionReferences}.
353
544
  // No `issues`/`warnings` keys — invalid path squashes errors to a single
@@ -355,14 +546,25 @@ export function registerFlowTools(server) {
355
546
  // Audit: F-A2-09.
356
547
  const r = await request("/flows/import/validate", { method: "POST", body: JSON.stringify(bundle) });
357
548
  if (!r.success) {
358
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: {}, hint: hintFor(r.errorCode) }, true);
549
+ return errorResult({
550
+ message: r.error ?? "flow import-validate failed",
551
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
552
+ suggestion: hintFor(r.errorCode),
553
+ details: {},
554
+ });
359
555
  }
360
- return formatResult({
556
+ // W3.1 — detailResult: the validation result is a flat envelope
557
+ // ({valid, flowName, exportedFrom, connectionReferences}). The
558
+ // connection-reference array flattens to a count + per-item entries.
559
+ // IA-10: a "Next steps" group routes the AI to import or fix.
560
+ return buildImportValidateDetail({
361
561
  valid: r.data?.valid ?? true,
362
- flow_name: r.data?.flowName,
363
- exported_from: r.data?.exportedFrom,
364
- connection_references: r.data?.connectionReferences ?? [],
365
- }, false, { maxChars: 4000 });
562
+ flowName: r.data?.flowName,
563
+ exportedFrom: r.data?.exportedFrom,
564
+ connectionReferences: Array.isArray(r.data?.connectionReferences)
565
+ ? r.data.connectionReferences
566
+ : [],
567
+ });
366
568
  });
367
569
  // ── apimapper_flow_import ──────────────────────────────────────────
368
570
  server.registerTool("apimapper_flow_import", {
@@ -376,7 +578,7 @@ export function registerFlowTools(server) {
376
578
  .describe("Flow bundle JSON (from apimapper_flow_export or external source)"),
377
579
  rename_to: z.string().optional().describe("Optional override for imported flow name"),
378
580
  },
379
- annotations: creating(),
581
+ annotations: creating({ title: "Import Flow", openWorld: true }),
380
582
  }, async ({ bundle, rename_to }) => {
381
583
  // Server reads `$exportData = $data['data'] ?? $data` and then
382
584
  // `$exportData['flow']['name']`. Mutate bundle accordingly.
@@ -398,7 +600,12 @@ export function registerFlowTools(server) {
398
600
  body: JSON.stringify(body),
399
601
  });
400
602
  if (!r.success) {
401
- return formatResult({ error: r.error, status: r.status, errorCode: r.errorCode, context: { rename_to }, hint: hintFor(r.errorCode) }, true);
603
+ return errorResult({
604
+ message: r.error ?? "flow import failed",
605
+ code: r.errorCode ?? (r.status ? String(r.status) : undefined),
606
+ suggestion: hintFor(r.errorCode),
607
+ details: { rename_to },
608
+ });
402
609
  }
403
610
  const flow = unwrapEntity(r.data, "flow");
404
611
  return formatResult({ imported: true, id: flow?.id, name: flow?.name }, false, { maxChars: 2000 });