@wootsup/mcp 0.1.0 → 0.4.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 (208) hide show
  1. package/CHANGELOG.md +157 -83
  2. package/README.md +31 -27
  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/catalog/build-catalog.d.ts +31 -0
  8. package/dist/catalog/build-catalog.js +68 -0
  9. package/dist/catalog/build-catalog.js.map +1 -0
  10. package/dist/cli-hint.d.ts +22 -0
  11. package/dist/cli-hint.js +55 -0
  12. package/dist/cli-hint.js.map +1 -0
  13. package/dist/index.js +129 -22
  14. package/dist/index.js.map +1 -1
  15. package/dist/install-skill.js +1 -1
  16. package/dist/modules/apimapper/auto-layout.d.ts +21 -0
  17. package/dist/modules/apimapper/auto-layout.js +54 -0
  18. package/dist/modules/apimapper/auto-layout.js.map +1 -0
  19. package/dist/modules/apimapper/cache.js +25 -17
  20. package/dist/modules/apimapper/cache.js.map +1 -1
  21. package/dist/modules/apimapper/client.d.ts +115 -4
  22. package/dist/modules/apimapper/client.js +699 -304
  23. package/dist/modules/apimapper/client.js.map +1 -1
  24. package/dist/modules/apimapper/connections-format.d.ts +31 -1
  25. package/dist/modules/apimapper/connections-format.js +97 -5
  26. package/dist/modules/apimapper/connections-format.js.map +1 -1
  27. package/dist/modules/apimapper/connections.d.ts +9 -7
  28. package/dist/modules/apimapper/connections.js +449 -127
  29. package/dist/modules/apimapper/connections.js.map +1 -1
  30. package/dist/modules/apimapper/credential-sanitizer.d.ts +5 -0
  31. package/dist/modules/apimapper/credential-sanitizer.js +60 -1
  32. package/dist/modules/apimapper/credential-sanitizer.js.map +1 -1
  33. package/dist/modules/apimapper/credentials.js +105 -61
  34. package/dist/modules/apimapper/credentials.js.map +1 -1
  35. package/dist/modules/apimapper/diagnose.js +21 -2
  36. package/dist/modules/apimapper/diagnose.js.map +1 -1
  37. package/dist/modules/apimapper/elicitation.d.ts +29 -0
  38. package/dist/modules/apimapper/elicitation.js +62 -0
  39. package/dist/modules/apimapper/elicitation.js.map +1 -1
  40. package/dist/modules/apimapper/example-extract.d.ts +13 -0
  41. package/dist/modules/apimapper/example-extract.js +111 -0
  42. package/dist/modules/apimapper/example-extract.js.map +1 -0
  43. package/dist/modules/apimapper/filter-operators.d.ts +24 -0
  44. package/dist/modules/apimapper/filter-operators.js +103 -0
  45. package/dist/modules/apimapper/filter-operators.js.map +1 -0
  46. package/dist/modules/apimapper/flows-format.js +92 -22
  47. package/dist/modules/apimapper/flows-format.js.map +1 -1
  48. package/dist/modules/apimapper/flows.d.ts +8 -7
  49. package/dist/modules/apimapper/flows.js +275 -120
  50. package/dist/modules/apimapper/flows.js.map +1 -1
  51. package/dist/modules/apimapper/gateway/advanced-read-tool.d.ts +9 -0
  52. package/dist/modules/apimapper/gateway/advanced-read-tool.js +172 -0
  53. package/dist/modules/apimapper/gateway/advanced-read-tool.js.map +1 -0
  54. package/dist/modules/apimapper/gateway/advanced-tool.js +66 -106
  55. package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -1
  56. package/dist/modules/apimapper/gateway/collect-module-tools.d.ts +17 -0
  57. package/dist/modules/apimapper/gateway/collect-module-tools.js +44 -0
  58. package/dist/modules/apimapper/gateway/collect-module-tools.js.map +1 -0
  59. package/dist/modules/apimapper/gateway/essentials.d.ts +1 -1
  60. package/dist/modules/apimapper/gateway/essentials.js +21 -2
  61. package/dist/modules/apimapper/gateway/essentials.js.map +1 -1
  62. package/dist/modules/apimapper/gateway/gateway-shared.d.ts +21 -0
  63. package/dist/modules/apimapper/gateway/gateway-shared.js +124 -0
  64. package/dist/modules/apimapper/gateway/gateway-shared.js.map +1 -0
  65. package/dist/modules/apimapper/gateway/test-support.d.ts +1 -17
  66. package/dist/modules/apimapper/gateway/test-support.js +4 -33
  67. package/dist/modules/apimapper/gateway/test-support.js.map +1 -1
  68. package/dist/modules/apimapper/get-skill-cores.d.ts +4 -0
  69. package/dist/modules/apimapper/get-skill-cores.js +220 -0
  70. package/dist/modules/apimapper/get-skill-cores.js.map +1 -0
  71. package/dist/modules/apimapper/get-skill.d.ts +1 -1
  72. package/dist/modules/apimapper/get-skill.js +74 -9
  73. package/dist/modules/apimapper/get-skill.js.map +1 -1
  74. package/dist/modules/apimapper/graph-builder.d.ts +85 -2
  75. package/dist/modules/apimapper/graph-builder.js +151 -15
  76. package/dist/modules/apimapper/graph-builder.js.map +1 -1
  77. package/dist/modules/apimapper/graph.js +152 -48
  78. package/dist/modules/apimapper/graph.js.map +1 -1
  79. package/dist/modules/apimapper/index.js +27 -13
  80. package/dist/modules/apimapper/index.js.map +1 -1
  81. package/dist/modules/apimapper/jmespath-test.d.ts +4 -0
  82. package/dist/modules/apimapper/jmespath-test.js +152 -0
  83. package/dist/modules/apimapper/jmespath-test.js.map +1 -0
  84. package/dist/modules/apimapper/library.js +553 -88
  85. package/dist/modules/apimapper/library.js.map +1 -1
  86. package/dist/modules/apimapper/license.js +12 -36
  87. package/dist/modules/apimapper/license.js.map +1 -1
  88. package/dist/modules/apimapper/list-footer.d.ts +27 -0
  89. package/dist/modules/apimapper/list-footer.js +57 -0
  90. package/dist/modules/apimapper/list-footer.js.map +1 -0
  91. package/dist/modules/apimapper/local-sources.js +100 -57
  92. package/dist/modules/apimapper/local-sources.js.map +1 -1
  93. package/dist/modules/apimapper/mcp-client-identity.d.ts +32 -0
  94. package/dist/modules/apimapper/mcp-client-identity.js +70 -0
  95. package/dist/modules/apimapper/mcp-client-identity.js.map +1 -0
  96. package/dist/modules/apimapper/merge-constants.d.ts +6 -0
  97. package/dist/modules/apimapper/merge-constants.js +26 -0
  98. package/dist/modules/apimapper/merge-constants.js.map +1 -0
  99. package/dist/modules/apimapper/misc.js +13 -27
  100. package/dist/modules/apimapper/misc.js.map +1 -1
  101. package/dist/modules/apimapper/node-schema.d.ts +52 -2
  102. package/dist/modules/apimapper/node-schema.js +95 -4
  103. package/dist/modules/apimapper/node-schema.js.map +1 -1
  104. package/dist/modules/apimapper/onboarding.d.ts +59 -1
  105. package/dist/modules/apimapper/onboarding.js +231 -28
  106. package/dist/modules/apimapper/onboarding.js.map +1 -1
  107. package/dist/modules/apimapper/read-cache.d.ts +16 -3
  108. package/dist/modules/apimapper/read-cache.js +59 -4
  109. package/dist/modules/apimapper/read-cache.js.map +1 -1
  110. package/dist/modules/apimapper/render/index.js +26 -5
  111. package/dist/modules/apimapper/render/index.js.map +1 -1
  112. package/dist/modules/apimapper/resource-id.d.ts +13 -0
  113. package/dist/modules/apimapper/resource-id.js +69 -0
  114. package/dist/modules/apimapper/resource-id.js.map +1 -0
  115. package/dist/modules/apimapper/schema.js +9 -18
  116. package/dist/modules/apimapper/schema.js.map +1 -1
  117. package/dist/modules/apimapper/settings.js +49 -52
  118. package/dist/modules/apimapper/settings.js.map +1 -1
  119. package/dist/modules/apimapper/sites-tools.d.ts +29 -0
  120. package/dist/modules/apimapper/sites-tools.js +165 -0
  121. package/dist/modules/apimapper/sites-tools.js.map +1 -0
  122. package/dist/modules/apimapper/tool-result.d.ts +66 -0
  123. package/dist/modules/apimapper/tool-result.js +125 -0
  124. package/dist/modules/apimapper/tool-result.js.map +1 -0
  125. package/dist/modules/apimapper/toolslist-size.d.ts +12 -11
  126. package/dist/modules/apimapper/toolslist-size.js +34 -21
  127. package/dist/modules/apimapper/toolslist-size.js.map +1 -1
  128. package/dist/modules/apimapper/types.d.ts +34 -0
  129. package/dist/modules/apimapper/types.js +1 -1
  130. package/dist/modules/apimapper/types.js.map +1 -1
  131. package/dist/modules/apimapper/whitelist-drift.d.ts +85 -0
  132. package/dist/modules/apimapper/whitelist-drift.js +375 -0
  133. package/dist/modules/apimapper/whitelist-drift.js.map +1 -0
  134. package/dist/modules/apimapper/workflows.js +302 -58
  135. package/dist/modules/apimapper/workflows.js.map +1 -1
  136. package/dist/modules/apimapper/yootheme-binding.d.ts +35 -0
  137. package/dist/modules/apimapper/yootheme-binding.js +267 -0
  138. package/dist/modules/apimapper/yootheme-binding.js.map +1 -0
  139. package/dist/platform/index.d.ts +56 -0
  140. package/dist/platform/index.js +158 -2
  141. package/dist/platform/index.js.map +1 -1
  142. package/dist/proxy/bridge.d.ts +35 -0
  143. package/dist/proxy/bridge.js +129 -0
  144. package/dist/proxy/bridge.js.map +1 -0
  145. package/dist/proxy/mode.d.ts +9 -0
  146. package/dist/proxy/mode.js +20 -0
  147. package/dist/proxy/mode.js.map +1 -0
  148. package/dist/setup/detect-clients.d.ts +40 -1
  149. package/dist/setup/detect-clients.js +148 -1
  150. package/dist/setup/detect-clients.js.map +1 -1
  151. package/dist/setup/probe-auth.d.ts +51 -0
  152. package/dist/setup/probe-auth.js +141 -0
  153. package/dist/setup/probe-auth.js.map +1 -0
  154. package/dist/setup/probe-handshake.js +40 -7
  155. package/dist/setup/probe-handshake.js.map +1 -1
  156. package/dist/setup/remove-config.d.ts +8 -0
  157. package/dist/setup/remove-config.js +145 -0
  158. package/dist/setup/remove-config.js.map +1 -0
  159. package/dist/setup/uninstall.d.ts +34 -0
  160. package/dist/setup/uninstall.js +147 -0
  161. package/dist/setup/uninstall.js.map +1 -0
  162. package/dist/setup-cli.d.ts +16 -0
  163. package/dist/setup-cli.js +63 -1
  164. package/dist/setup-cli.js.map +1 -1
  165. package/dist/sites/loader.d.ts +48 -0
  166. package/dist/sites/loader.js +134 -0
  167. package/dist/sites/loader.js.map +1 -0
  168. package/dist/sites/schema.d.ts +69 -0
  169. package/dist/sites/schema.js +71 -0
  170. package/dist/sites/schema.js.map +1 -0
  171. package/dist/sites/secret-resolver.d.ts +47 -0
  172. package/dist/sites/secret-resolver.js +150 -0
  173. package/dist/sites/secret-resolver.js.map +1 -0
  174. package/dist/skill-instructions.d.ts +14 -1
  175. package/dist/skill-instructions.js +35 -6
  176. package/dist/skill-instructions.js.map +1 -1
  177. package/dist/transports/stdio.js +4 -4
  178. package/dist/transports/stdio.js.map +1 -1
  179. package/dist/uninstall-skill.d.ts +27 -0
  180. package/dist/uninstall-skill.js +89 -0
  181. package/dist/uninstall-skill.js.map +1 -0
  182. package/docs/architecture.md +21 -21
  183. package/docs/customgraph-internal-migration.md +4 -4
  184. package/docs/security.md +2 -21
  185. package/docs/tools.md +40 -12
  186. package/manifest.json +77 -79
  187. package/package.json +69 -65
  188. package/skills/apimapper/SKILL.md +128 -7
  189. package/skills/apimapper/reference/conditional-style-multi-items.md +114 -0
  190. package/skills/apimapper/reference/dynamize-existing-layout.md +158 -0
  191. package/skills/apimapper/reference/jmespath-cookbook.md +241 -0
  192. package/skills/apimapper/reference/jmespath-pitfalls.md +189 -0
  193. package/skills/apimapper/reference/joomla.md +1 -1
  194. package/skills/apimapper/reference/library-template-discovery.md +65 -0
  195. package/skills/apimapper/reference/merge-two-sources-on-key.md +204 -0
  196. package/skills/apimapper/reference/oauth.md +143 -52
  197. package/skills/apimapper/reference/troubleshooting.md +22 -2
  198. package/skills/apimapper/reference/yootheme-source-to-builder-handoff.md +348 -0
  199. package/skills/apimapper/reference/yootheme.md +75 -44
  200. package/dist/auth/oauth-provider.d.ts +0 -68
  201. package/dist/auth/oauth-provider.js +0 -232
  202. package/dist/auth/oauth-provider.js.map +0 -1
  203. package/dist/server-http.d.ts +0 -22
  204. package/dist/server-http.js +0 -159
  205. package/dist/server-http.js.map +0 -1
  206. package/dist/transports/http.d.ts +0 -29
  207. package/dist/transports/http.js +0 -267
  208. package/dist/transports/http.js.map +0 -1
@@ -1,8 +1,11 @@
1
1
  import { z } from "zod";
2
- import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, createProgressReporter, elicitChoice, } from "@getimo/mcp-toolkit";
2
+ import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, createProgressReporter, } from "@getimo/mcp-toolkit";
3
3
  import { request, hintFor } from "./client.js";
4
+ import { restErrorResult } from "./tool-result.js";
4
5
  import { toRows } from "./types.js";
5
- import { nodeSchema, edgeSchema, filterByNameQuery } from "./node-schema.js";
6
+ import { nodeSchema, edgeSchema, ensureEdgeIds, filterByNameQuery } from "./node-schema.js";
7
+ import { autoLayoutNodes } from "./auto-layout.js";
8
+ import { withOverflowFooter } from "./list-footer.js";
6
9
  import { unwrapEntity } from "./envelope.js";
7
10
  import { ambiguityFallbackError, } from "./elicitation.js";
8
11
  import { buildFlowVisualization, SIDECAR_RESPONSE_MAX_CHARS, } from "./render/sidecar.js";
@@ -11,13 +14,15 @@ import { FLOW_TABLE_COLUMNS, FLOW_COMPACT_COLUMNS, FLOW_LIST_NEXT_STEPS, isFlowC
11
14
  * Register the flow CRUD + compile + detect-schema + trace + export/import
12
15
  * tools.
13
16
  *
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.
17
+ * Chat-first disambiguation (decision 2026-06-15): on genuine flow ambiguity
18
+ * `flow_compile` returns a structured candidate-list error (the assistant
19
+ * asks in plain chat) rather than driving the native elicitation picker. The
20
+ * picker is reserved for OAuth authorization (credentials.ts), so this module
21
+ * no longer takes an elicitation capability.
22
+ *
23
+ * @param server the tool registrar (essentials forward, rest captured).
19
24
  */
20
- export function registerFlowTools(server, elicitation) {
25
+ export function registerFlowTools(server) {
21
26
  // ── apimapper_flow_list ────────────────────────────────────────────
22
27
  server.registerTool("apimapper_flow_list", {
23
28
  title: "List Flows",
@@ -49,12 +54,7 @@ export function registerFlowTools(server, elicitation) {
49
54
  }, async ({ source, compiled, limit, name_query }) => {
50
55
  const r = await request("/flows");
51
56
  if (!r.success) {
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
- });
57
+ return restErrorResult(r, { source, compiled, limit, name_query }, { message: "flow list failed" });
58
58
  }
59
59
  let items = Array.isArray(r.data?.flows) ? r.data.flows : [];
60
60
  // `is_compiled` is derived from the REST shape (compiledAt / compiled[])
@@ -82,13 +82,20 @@ export function registerFlowTools(server, elicitation) {
82
82
  map: mapFlowRow,
83
83
  compactMap: compactFlowRow,
84
84
  header: (n) => `${n} flows`,
85
- footer: FLOW_LIST_NEXT_STEPS,
85
+ // Minor (2026-06-10): overflow-aware footer (see list-footer.ts).
86
+ footer: withOverflowFooter(FLOW_LIST_NEXT_STEPS, items.length, "flows"),
86
87
  });
87
88
  });
88
89
  // ── apimapper_flow_get ─────────────────────────────────────────────
89
90
  server.registerTool("apimapper_flow_get", {
90
91
  title: "Get Flow",
91
- description: "Fetch full flow definition (nodes, edges, viewport, compiled artifact)." +
92
+ description: "Get the full definition of one flow by ID (nodes, edges, viewport, and the " +
93
+ "compiled artifact). Use to inspect or debug a flow's graph before editing " +
94
+ "nodes/edges, changing a merge strategy, or re-compiling. " +
95
+ "Keywords: get flow, flow detail, inspect flow, show graph, nodes and edges, by ID. " +
96
+ "When NOT to use: to enumerate all flows use apimapper_flow_list; to change the " +
97
+ "graph use apimapper_flow_update then apimapper_flow_compile; to see the last " +
98
+ "run's per-step timing use apimapper_flow_trace." +
92
99
  "\n\nExample:\n apimapper_flow_get({ id: 'flow_Z2fLg70M84' })",
93
100
  inputSchema: {
94
101
  id: z.string().describe('Flow ID (e.g., "flow_aCfOEpwCtVZnPFwPOwO61"). Use apimapper_flow_list.'),
@@ -99,12 +106,7 @@ export function registerFlowTools(server, elicitation) {
99
106
  // `{success:true, flow:{…}}`. Audit: F-A2-01.
100
107
  const r = await request(`/flows/${encodeURIComponent(id)}`);
101
108
  if (!r.success) {
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
- });
109
+ return restErrorResult(r, { id }, { message: "flow get failed" });
108
110
  }
109
111
  const flow = unwrapEntity(r.data, "flow");
110
112
  if (!flow || Object.keys(flow).length === 0) {
@@ -127,7 +129,7 @@ export function registerFlowTools(server, elicitation) {
127
129
  title: "Create Flow",
128
130
  description: "Create a new flow with full nodes + edges JSON. " +
129
131
  "For declarative multi-source flow creation use apimapper_flow_setup_with_sources instead." +
130
- "\n\nExample:\n apimapper_flow_create({ name: 'My Pexels Gallery', nodes: [{ id: 'src1', type: 'source' }, { id: 'out1', type: 'output' }], edges: [{ source: 'src1', target: 'out1' }] })",
132
+ "\n\nExample:\n apimapper_flow_create({ name: 'My Pexels Gallery', nodes: [{ id: 'src1', type: 'source', position: { x: 0, y: 0 }, data: { connectionId: 'con_abc123' } }, { id: 'out1', type: 'output-yootheme', position: { x: 400, y: 0 }, data: { name: 'My Pexels Gallery' } }], edges: [{ source: 'src1', target: 'out1' }] })",
131
133
  inputSchema: {
132
134
  name: z.string().min(1).describe('Flow name (appears in YOOtheme dropdown)'),
133
135
  description: z.string().optional().describe("Optional description"),
@@ -149,22 +151,47 @@ export function registerFlowTools(server, elicitation) {
149
151
  .object({ x: z.number(), y: z.number(), zoom: z.number() })
150
152
  .optional()
151
153
  .describe('React-Flow viewport (e.g., {"x":0,"y":0,"zoom":1})'),
154
+ // Phase 2.7 (change protocol): an optional AI one-liner recorded
155
+ // alongside the create. The action is IMPLICIT (`flow.created`) so the
156
+ // handler defaults it — only `summary` is needed here. Threaded into
157
+ // the POST body as `change: { summary }` (conditional-spread: absent
158
+ // when omitted, so the body stays byte-identical to today).
159
+ summary: z
160
+ .string()
161
+ .max(280)
162
+ .optional()
163
+ .describe("Optional one-line summary of this change for the flow's change history " +
164
+ '(e.g., "Created a Pexels gallery flow with 2 sources").'),
152
165
  },
153
166
  annotations: creating({ title: "Create Flow", openWorld: true }),
154
- }, async (input) => {
167
+ }, async ({ summary, ...input }) => {
168
+ // F5 (2026-06-09): stamp a stable id on any id-less edge BEFORE POST. The
169
+ // schema marks edge.id optional (the AI legitimately omits it), but the
170
+ // React-Flow runtime + backend graph validator key edges by id and reject
171
+ // an id-less edge with a misleading "connectivity" error. ensureEdgeIds
172
+ // makes both layers accept the graph. buildFlowGraph already does this for
173
+ // its synthetic graphs; this closes the gap for hand-authored flows.
174
+ // Task X: tidy the graph on every AI write — re-flow node positions into a
175
+ // clean layered left-to-right grid (orphans trail in their own column).
176
+ // AI-path only; a human's manual positions (saved by the admin-ui
177
+ // autosave) are never touched.
178
+ const payload = {
179
+ ...input,
180
+ nodes: autoLayoutNodes(input.nodes, input.edges),
181
+ edges: ensureEdgeIds(input.edges),
182
+ // Phase 2.7 — implicit action (`flow.created`), so only `summary` is
183
+ // attached; the handler defaults the action. Conditional-spread keeps
184
+ // the body byte-identical to today when no summary is supplied.
185
+ ...(summary !== undefined ? { change: { summary } } : {}),
186
+ };
155
187
  // PHP fromControllerResponse(_, 'flow', 201) → {success, flow:{…}}.
156
188
  // Audit: F-A2-02.
157
189
  const r = await request("/flows", {
158
190
  method: "POST",
159
- body: JSON.stringify(input),
191
+ body: JSON.stringify(payload),
160
192
  });
161
193
  if (!r.success) {
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
- });
194
+ return restErrorResult(r, { name: input.name, node_count: input.nodes.length }, { message: "flow create failed" });
168
195
  }
169
196
  const flow = unwrapEntity(r.data, "flow");
170
197
  // Sidecar: flow_create always has input.nodes + input.edges
@@ -172,8 +199,8 @@ export function registerFlowTools(server, elicitation) {
172
199
  // APIMAPPER_AUTO_VISUALIZE_FLOWS="false". Additive + defensive —
173
200
  // a render failure returns null and is omitted silently.
174
201
  const viz = buildFlowVisualization({
175
- nodes: input.nodes,
176
- edges: input.edges,
202
+ nodes: payload.nodes,
203
+ edges: payload.edges,
177
204
  id: flow?.id,
178
205
  name: flow?.name,
179
206
  });
@@ -188,7 +215,7 @@ export function registerFlowTools(server, elicitation) {
188
215
  // (array, even if single-element) matches the structured
189
216
  // next-step convention used by the list-result footers and the
190
217
  // detailResult builders. Flat `next: string` was the holdout.
191
- next_steps: ["Call apimapper_flow_compile to publish to YOOtheme."],
218
+ next_steps: ["Publish to YOOtheme via apimapper_flow_full_recompile_publish (or apimapper_advanced({ tool: \"apimapper_flow_compile\" }))."],
192
219
  }, false,
193
220
  // Headroom for the sidecar — see SIDECAR_RESPONSE_MAX_CHARS.
194
221
  { maxChars: SIDECAR_RESPONSE_MAX_CHARS });
@@ -197,49 +224,133 @@ export function registerFlowTools(server, elicitation) {
197
224
  server.registerTool("apimapper_flow_update", {
198
225
  title: "Update Flow",
199
226
  description: "Update flow fields. Use to modify nodes/edges (e.g., toggle merge.strategy, change joinKey). " +
200
- "After update, call apimapper_flow_compile." +
201
- "\n\nExample:\n apimapper_flow_update({ id: 'flow_Z2fLg70M84', name: 'My Pexels Gallery (v2)' })",
227
+ "After update, recompile via apimapper_flow_full_recompile_publish." +
228
+ "\n\nExample:\n apimapper_flow_update({ id: 'flow_Z2fLg70M84', patch: { name: 'My Pexels Gallery (v2)' } })",
202
229
  inputSchema: {
203
230
  id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
204
231
  patch: z
205
232
  .record(z.string(), z.unknown())
206
233
  .describe('Fields to patch (e.g., {"nodes":[...updated nodes...]} or {"name":"Renamed"})'),
234
+ // Phase 2.7 (change protocol): flow_update has NO implicit action — the
235
+ // edit could be anything, so the caller names it explicitly. `action`
236
+ // is a verb-ish string (e.g. "node.added", "filter.set",
237
+ // "merge.changed"); `detail` is optional structured context.
238
+ change: z
239
+ .object({
240
+ action: z
241
+ .string()
242
+ .describe('What changed, e.g. "node.added" / "filter.set" / "merge.changed".'),
243
+ detail: z
244
+ .record(z.string(), z.unknown())
245
+ .optional()
246
+ .describe('Optional structured context (e.g. { node_id: "filter1" }).'),
247
+ })
248
+ .optional()
249
+ .describe("Optional change-protocol record for the flow's change history. Provide an " +
250
+ "explicit `action` (this tool has no implicit one)."),
251
+ // Phase 2.7 — optional AI one-liner alongside the explicit action.
252
+ summary: z
253
+ .string()
254
+ .max(280)
255
+ .optional()
256
+ .describe('Optional one-line summary of this change (e.g., "Added a price filter > 10").'),
207
257
  },
208
258
  annotations: mutating({ title: "Update Flow", openWorld: true }),
209
- }, async ({ id, patch }) => {
259
+ }, async ({ id, patch, change, summary }) => {
260
+ // F5 (2026-06-09): when the patch carries an `edges` array, stamp a stable
261
+ // id on any id-less edge before PUT — same fix as flow_create. Done only
262
+ // when `patch.edges` is actually an array so a name-only / metadata-only
263
+ // patch is forwarded byte-for-byte (no `edges` key injected).
264
+ const patchObj = (patch && typeof patch === "object" ? patch : {});
265
+ const requestPatch = Array.isArray(patchObj.edges)
266
+ ? {
267
+ ...patchObj,
268
+ // Task X: a graph-touching update (nodes + edges) is re-laid-out so
269
+ // the AI's edit lands tidy (orphans included). A node-less /
270
+ // edges-only patch is untouched. AI-path only — never the human
271
+ // autosave path, so manual positions survive.
272
+ ...(Array.isArray(patchObj.nodes)
273
+ ? {
274
+ nodes: autoLayoutNodes(patchObj.nodes, patchObj.edges),
275
+ }
276
+ : {}),
277
+ edges: ensureEdgeIds(patchObj.edges),
278
+ }
279
+ : patch;
280
+ // Phase 2.7 (change protocol): attach the explicit change record + the
281
+ // optional summary as a sibling `change` key on the PUT body. Built
282
+ // conditionally so a caller that supplied neither sends a body
283
+ // byte-identical to today (no empty `change` key injected). When only
284
+ // `summary` is given (no explicit action), the handler defaults the
285
+ // action — mirrors the implicit-action tools.
286
+ const changePayload = change !== undefined || summary !== undefined
287
+ ? {
288
+ ...(change ?? {}),
289
+ ...(summary !== undefined ? { summary } : {}),
290
+ }
291
+ : undefined;
292
+ const requestBody = changePayload !== undefined
293
+ ? { ...requestPatch, change: changePayload }
294
+ : requestPatch;
210
295
  // PHP fromControllerResponse(_, 'flow') → {success, flow:{…}}. On 409 the
211
296
  // body shape is {currentVersion, error, code}. Audit: F-A2-03.
212
297
  const r = await request(`/flows/${encodeURIComponent(id)}`, {
213
298
  method: "PUT",
214
- body: JSON.stringify(patch),
299
+ body: JSON.stringify(requestBody),
215
300
  });
216
301
  if (!r.success) {
217
302
  // Surface currentVersion (when present on 409) so the agent can
218
303
  // re-read + retry without a separate fetch.
219
304
  const envelope = (r.data && typeof r.data === "object" ? r.data : {});
220
305
  const currentVersion = typeof envelope.currentVersion === "number" ? envelope.currentVersion : undefined;
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
- });
306
+ return restErrorResult(r, { id, ...(currentVersion !== undefined ? { currentVersion } : {}) }, { message: "flow update failed" });
227
307
  }
228
308
  const flow = unwrapEntity(r.data, "flow");
229
309
  // Sidecar: flow_update's patch is a generic record. The
230
310
  // visualization is CONDITIONAL — render only when the patch
231
311
  // 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 : {});
312
+ // name-only / metadata-only patch has no graph to draw. Use the
313
+ // id-stamped edges (requestPatch.edges) so the diagram matches what
314
+ // was actually PUT.
234
315
  const patchHasGraph = Array.isArray(patchObj.nodes) && Array.isArray(patchObj.edges);
316
+ // F148 — a patch that touches `nodes` can CHANGE THE OUTPUT SCHEMA (e.g.
317
+ // a transform expression that derives new fields). Those new fields are
318
+ // NOT bindable in the YOOtheme Builder until the flow is recompiled — and
319
+ // an already-bound element keeps the OLD field set until re-bound. The
320
+ // F41+F44 next-steps below surface that schema→recompile→re-bind chain.
235
321
  const viz = patchHasGraph
236
322
  ? buildFlowVisualization({
237
323
  nodes: patchObj.nodes,
238
- edges: patchObj.edges,
324
+ edges: requestPatch.edges,
239
325
  id: flow?.id,
240
326
  name: flow?.name,
241
327
  })
242
328
  : null;
329
+ // F41 + F44 + F148: the next-step guidance must be
330
+ // (a) gateway-SAFE — `apimapper_flow_compile` is gateway-only (not in
331
+ // tools/list); a bare reference returns "No such tool". The
332
+ // recompile-publish tool IS essential, so point at it.
333
+ // (b) schema-change-AWARE — when the patch carried `nodes`, the output
334
+ // schema may have changed (new/renamed derived fields). Those
335
+ // fields are invisible to apimapper_yootheme_binding_for_flow until
336
+ // a recompile-publish, and any already-bound YOOtheme element keeps
337
+ // the OLD field set and must be re-bound against the fresh schema.
338
+ const patchTouchedGraph = Array.isArray(patchObj.nodes);
339
+ let nextSteps;
340
+ if (patchTouchedGraph) {
341
+ nextSteps = [
342
+ "Graph changed — run apimapper_flow_full_recompile_publish to recompile and re-register the YOOtheme source.",
343
+ "Schema may have changed: new/renamed fields are invisible to apimapper_yootheme_binding_for_flow until that recompile. After it, re-read bindings and RE-BIND the YOOtheme element (field_mappings is a full replace) — the old binding keeps the previous field set.",
344
+ ];
345
+ }
346
+ else if (flow?.is_compiled) {
347
+ nextSteps = [
348
+ "Metadata changed on a compiled flow — recompile with apimapper_flow_full_recompile_publish (or apimapper_advanced({ tool: \"apimapper_flow_compile\", arguments: { id } }), which is gateway-only).",
349
+ ];
350
+ }
351
+ else {
352
+ nextSteps = [];
353
+ }
243
354
  return formatResult({
244
355
  updated: true,
245
356
  id: flow?.id,
@@ -250,9 +361,7 @@ export function registerFlowTools(server, elicitation) {
250
361
  // F-3.1 (Pass-1 consolidation): IA-10 uniformity — flat next has
251
362
  // been migrated to next_steps[]. Empty array when no recompile is
252
363
  // 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
- : [],
364
+ next_steps: nextSteps,
256
365
  }, false,
257
366
  // Headroom for the (conditional) sidecar — see SIDECAR_RESPONSE_MAX_CHARS.
258
367
  { maxChars: SIDECAR_RESPONSE_MAX_CHARS });
@@ -277,6 +386,26 @@ export function registerFlowTools(server, elicitation) {
277
386
  // Audit: F-A2-04.
278
387
  const preview = await request(`/flows/${encodeURIComponent(id)}`);
279
388
  const flow = preview.success ? unwrapEntity(preview.data, "flow") : undefined;
389
+ // F204 — ownership posture. The flow carries `created_by` (the kid of
390
+ // the MCP key that created it; null for cookie / Admin-UI flows). A
391
+ // write-scoped key may delete ONLY a flow it created; a null-owned flow
392
+ // requires the admin scope. The client can't compare its own kid (the
393
+ // token is opaque here), so this is an INFORMATIONAL signal — the server
394
+ // enforces it and returns a 403 (insufficient_scope_ownership) on a real
395
+ // mismatch (surfaced by restErrorResult's scopeAware path below).
396
+ const ownership = flow && flow.created_by != null
397
+ ? {
398
+ can_delete: "owner_or_admin",
399
+ delete_reason: "A write-scope key can delete this flow only if it created it " +
400
+ `(created_by: ${flow.created_by}); an admin-scope key deletes any flow.`,
401
+ }
402
+ : flow
403
+ ? {
404
+ can_delete: "admin_only",
405
+ delete_reason: "This flow has no owner kid (created via the dashboard or before " +
406
+ "ownership tracking), so deleting it requires the admin scope.",
407
+ }
408
+ : null;
280
409
  return formatResult({
281
410
  preview: true,
282
411
  warning: "DESTRUCTIVE — Flow delete cannot be undone. Sources disappear from YOOtheme.",
@@ -287,19 +416,19 @@ export function registerFlowTools(server, elicitation) {
287
416
  node_count: flow.node_count,
288
417
  is_compiled: flow.is_compiled,
289
418
  node_types: flow.node_types,
419
+ created_by: flow.created_by ?? null,
290
420
  }
291
421
  : { id, note: "could not preview — id may not exist" },
422
+ ...(ownership ?? {}),
292
423
  instruction: "Ask user to confirm, then call again with confirm: true.",
293
424
  });
294
425
  }
295
426
  const r = await request(`/flows/${encodeURIComponent(id)}`, { method: "DELETE" });
296
427
  if (!r.success) {
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
- });
428
+ // F6 (2026-06-09): a 403 here means the MCP key lacks the admin scope
429
+ // delete requires NOT that the key is invalid/revoked. scopeAware
430
+ // swaps the generic key-recovery walkthrough for a scope-upgrade hint.
431
+ return restErrorResult(r, { id }, { message: "flow delete failed", scopeAware: true });
303
432
  }
304
433
  return formatResult({ deleted: true, id }, false, { maxChars: 1500 });
305
434
  });
@@ -318,9 +447,19 @@ export function registerFlowTools(server, elicitation) {
318
447
  .optional()
319
448
  .describe("Flow ID. Use apimapper_flow_list to find. Omit to resolve automatically " +
320
449
  "when the install has a single flow."),
450
+ // Phase 2.7 (change protocol): compile = publish, so the action is
451
+ // IMPLICIT (`flow.published`) — only `summary` is needed; the handler
452
+ // defaults the action. Threaded into the POST body as
453
+ // `change: { summary }` (the compile POST otherwise has no body).
454
+ summary: z
455
+ .string()
456
+ .max(280)
457
+ .optional()
458
+ .describe('Optional one-line summary of this publish for the change history ' +
459
+ '(e.g., "Published the gallery after adding a city filter").'),
321
460
  },
322
461
  annotations: mutating({ title: "Compile Flow", openWorld: true }),
323
- }, async ({ id }, extra) => {
462
+ }, async ({ id, summary }, extra) => {
324
463
  // W3.5 — coarse progress side-channel. `null` when the caller sent no
325
464
  // progressToken; `progress?.report(...)` then no-ops.
326
465
  const progress = extra ? createProgressReporter(extra) : null;
@@ -333,19 +472,14 @@ export function registerFlowTools(server, elicitation) {
333
472
  if (!resolvedId) {
334
473
  const lr = await request("/flows");
335
474
  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
- });
475
+ return restErrorResult(lr, {}, { message: "flow lookup failed" });
342
476
  }
343
477
  const flows = Array.isArray(lr.data?.flows) ? lr.data.flows : [];
344
478
  if (flows.length === 0) {
345
479
  return errorResult({
346
480
  message: "No flows exist to compile.",
347
481
  code: "flow_not_found",
348
- suggestion: "Create one with apimapper_flow_create, then compile it.",
482
+ suggestion: 'Create + publish one in a single call via apimapper_flow_setup_with_sources, or build raw via apimapper_advanced({ tool: "apimapper_flow_create" }).',
349
483
  details: {},
350
484
  });
351
485
  }
@@ -353,22 +487,21 @@ export function registerFlowTools(server, elicitation) {
353
487
  resolvedId = flows[0].id;
354
488
  }
355
489
  else {
490
+ // Chat-first disambiguation (decision 2026-06-15): skip the native
491
+ // elicitation picker (clunky collapsed TUI in some hosts) and
492
+ // return the structured candidate-list error directly. The
493
+ // `candidates` carry id+label, so the assistant sees the flow names
494
+ // and asks the user in plain chat which flow to compile.
356
495
  const candidates = flows.map((f) => ({
357
496
  id: f.id,
358
497
  label: f.name,
359
498
  }));
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;
499
+ return ambiguityFallbackError({
500
+ code: "flow_ambiguous",
501
+ paramName: "id",
502
+ what: "flows",
503
+ candidates,
504
+ });
372
505
  }
373
506
  }
374
507
  await progress?.report(0, 1, "Compiling flow…");
@@ -376,14 +509,18 @@ export function registerFlowTools(server, elicitation) {
376
509
  // surfaced via HandlerResult::error(), not inside the 200 body. Audit:
377
510
  // F-A2-05 — the previous `errors[]` + `is_compiled===false` branch was
378
511
  // dead code.
379
- const r = await request(`/flows/${encodeURIComponent(resolvedId)}/compile`, { method: "POST" });
512
+ // Phase 2.7 implicit action (`flow.published`). Attach `change:
513
+ // { summary }` only when a summary was supplied; otherwise the compile
514
+ // POST carries no body, exactly as before.
515
+ const r = await request(`/flows/${encodeURIComponent(resolvedId)}/compile`, summary !== undefined
516
+ ? {
517
+ method: "POST",
518
+ body: JSON.stringify({ change: { summary } }),
519
+ headers: { "Content-Type": "application/json" },
520
+ }
521
+ : { method: "POST" });
380
522
  if (!r.success) {
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
- });
523
+ return restErrorResult(r, { id: resolvedId }, { message: "flow compile failed" });
387
524
  }
388
525
  await progress?.report(1, 1, "Flow compiled");
389
526
  const dataObj = r.data && typeof r.data === "object" ? r.data : {};
@@ -401,13 +538,31 @@ export function registerFlowTools(server, elicitation) {
401
538
  server.registerTool("apimapper_flow_detect_schema", {
402
539
  title: "Detect Flow Schema",
403
540
  description: "Re-detect/refresh the output schema by sampling the upstream API. " +
404
- "Use after upstream endpoint changes to surface new fields in the YOOtheme Builder." +
405
- "\n\nExample:\n apimapper_flow_detect_schema({ id: 'flow_Z2fLg70M84' })",
541
+ "Use after upstream endpoint changes to surface new fields in the YOOtheme Builder. " +
542
+ "For library-template connections that carry `{{placeholder}}` substitutions in their " +
543
+ "endpoint path (Google Sheets `{{spreadsheet_id}}`/`{{range}}`, Calendly `{{user_uri}}`, " +
544
+ "etc.), pass `template_fields` to override or supply the values at detect-time — " +
545
+ "mirrors the `apimapper_connection_data` contract." +
546
+ "\n\nExample:\n apimapper_flow_detect_schema({ id: 'flow_Z2fLg70M84' })" +
547
+ "\n apimapper_flow_detect_schema({ id: 'flow_X', template_fields: { spreadsheet_id: '1abc', range: 'Sheet1!A:G' } })",
406
548
  inputSchema: {
407
549
  id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
550
+ endpoint: z
551
+ .string()
552
+ .min(1)
553
+ .optional()
554
+ .describe('Override the source node\'s endpoint selection (e.g. "Get Values"). ' +
555
+ "Empty-string and whitespace-only values are rejected at the schema boundary."),
556
+ template_fields: z
557
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
558
+ .optional()
559
+ .describe('Override values for `{{placeholder}}` substitutions in the endpoint URL ' +
560
+ '(e.g. { spreadsheet_id: "1abc", range: "Sheet1!A:G" }). Partial merge: ' +
561
+ "caller values win, persisted values for keys not mentioned here survive. " +
562
+ "Wave-12 F3 — restores parity with apimapper_connection_data."),
408
563
  },
409
564
  annotations: mutating({ title: "Detect Flow Schema", openWorld: true }),
410
- }, async ({ id }, extra) => {
565
+ }, async ({ id, endpoint, template_fields }, extra) => {
411
566
  // W3.5 — coarse progress side-channel. `null` when the caller sent no
412
567
  // progressToken; `progress?.report(...)` then no-ops.
413
568
  const progress = extra ? createProgressReporter(extra) : null;
@@ -417,15 +572,28 @@ export function registerFlowTools(server, elicitation) {
417
572
  // H-2 fix (2026-05-27): backend now persists detected schema onto the
418
573
  // output node so the next compile finds non-empty fields. `persisted`
419
574
  // flag surfaces whether the write actually happened.
575
+ // Wave-12 F3 (Cold-AI #6, 2026-05-31): `endpoint` + `template_fields`
576
+ // now thread through to the PHP handler as sourceContext, restoring
577
+ // contract symmetry with apimapper_connection_data so library-template
578
+ // flows (Google Sheets etc.) can substitute URL placeholders at
579
+ // detect-time.
420
580
  // Audit: F-A2-06.
421
- const r = await request(`/flows/${encodeURIComponent(id)}/detect-schema`, { method: "POST" });
581
+ const body = {};
582
+ if (endpoint !== undefined)
583
+ body.endpoint = endpoint;
584
+ if (template_fields !== undefined)
585
+ body.template_fields = template_fields;
586
+ const r = await request(`/flows/${encodeURIComponent(id)}/detect-schema`, {
587
+ method: "POST",
588
+ ...(Object.keys(body).length > 0
589
+ ? {
590
+ body: JSON.stringify(body),
591
+ headers: { "Content-Type": "application/json" },
592
+ }
593
+ : {}),
594
+ });
422
595
  if (!r.success) {
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
- });
596
+ return restErrorResult(r, { id }, { message: "flow detect-schema failed" });
429
597
  }
430
598
  await progress?.report(1, 1, "Schema detected");
431
599
  const fields = Array.isArray(r.data?.schema?.fields) ? r.data.schema.fields : [];
@@ -454,21 +622,20 @@ export function registerFlowTools(server, elicitation) {
454
622
  title: "Get Flow Execution Trace",
455
623
  description: "Fetch the execution trace from the last run (timing per step, item counts, errors). " +
456
624
  "Useful for debugging slow flows or incorrect outputs." +
457
- "\n\nExample:\n apimapper_flow_trace({ id: 'flow_Z2fLg70M84' })",
625
+ // F213: gateway-only tool — show the call routed THROUGH the read
626
+ // gateway so the first live invocation doesn't fail invalid_arguments.
627
+ "\n\nExample (gateway-routed):\n apimapper_advanced_read({ tool: \"apimapper_flow_trace\", arguments: { id: \"flow_Z2fLg70M84\" } })",
458
628
  inputSchema: {
459
629
  id: z.string().describe("Flow ID. Use apimapper_flow_list to find."),
460
630
  },
461
631
  annotations: readOnly({ title: "Trace Flow", openWorld: true }),
462
632
  }, async ({ id }) => {
463
- // PHP success body: {trace: {steps: [...], total_ms}}. Audit: F-A2-07.
633
+ // PHP success body: {trace: {steps: [...], totalDuration}}. The native
634
+ // TraceService key is `totalDuration` (ms, float); `total_ms` is kept as
635
+ // a back-compat alias for any older backend shape. Audit: F-A2-07 / F185.
464
636
  const r = await request(`/flows/${encodeURIComponent(id)}/trace`);
465
637
  if (!r.success) {
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
- });
638
+ return restErrorResult(r, { id }, { message: "flow trace failed" });
472
639
  }
473
640
  const trace = r.data?.trace;
474
641
  const steps = Array.isArray(trace?.steps) ? trace.steps : [];
@@ -476,7 +643,10 @@ export function registerFlowTools(server, elicitation) {
476
643
  // chronological event timeline (display="event-timeline"). The step
477
644
  // cap is preserved (50) so a pathological trace cannot bloat the
478
645
  // response. buildFlowTraceTimeline does the defensive field reads.
479
- return buildFlowTraceTimeline(id, steps.slice(0, 50), trace?.total_ms);
646
+ const totalMs = typeof trace?.totalDuration === "number"
647
+ ? trace.totalDuration
648
+ : trace?.total_ms;
649
+ return buildFlowTraceTimeline(id, steps.slice(0, 50), totalMs);
480
650
  });
481
651
  // ── apimapper_flow_export ──────────────────────────────────────────
482
652
  // F-24 (W1.13): the export bundle contains connection REFERENCES only —
@@ -512,12 +682,7 @@ export function registerFlowTools(server, elicitation) {
512
682
  // belt-and-braces pattern at credentials.ts (credential_list / get).
513
683
  const r = await request(`/flows/${encodeURIComponent(id)}/export`, {}, { sanitize: true });
514
684
  if (!r.success) {
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
- });
685
+ return restErrorResult(r, { id }, { message: "flow export failed" });
521
686
  }
522
687
  const envelope = (r.data && typeof r.data === "object" ? r.data : {});
523
688
  const bundle = envelope.data ?? envelope.export ?? envelope;
@@ -546,12 +711,7 @@ export function registerFlowTools(server, elicitation) {
546
711
  // Audit: F-A2-09.
547
712
  const r = await request("/flows/import/validate", { method: "POST", body: JSON.stringify(bundle) });
548
713
  if (!r.success) {
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
- });
714
+ return restErrorResult(r, {}, { message: "flow import-validate failed" });
555
715
  }
556
716
  // W3.1 — detailResult: the validation result is a flat envelope
557
717
  // ({valid, flowName, exportedFrom, connectionReferences}). The
@@ -600,12 +760,7 @@ export function registerFlowTools(server, elicitation) {
600
760
  body: JSON.stringify(body),
601
761
  });
602
762
  if (!r.success) {
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
- });
763
+ return restErrorResult(r, { rename_to }, { message: "flow import failed" });
609
764
  }
610
765
  const flow = unwrapEntity(r.data, "flow");
611
766
  return formatResult({ imported: true, id: flow?.id, name: flow?.name }, false, { maxChars: 2000 });