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