@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.
- package/CHANGELOG.md +157 -83
- package/README.md +31 -27
- package/SECURITY.md +15 -6
- package/dist/auth/keychain.d.ts +27 -1
- package/dist/auth/keychain.js +48 -2
- package/dist/auth/keychain.js.map +1 -1
- package/dist/catalog/build-catalog.d.ts +31 -0
- package/dist/catalog/build-catalog.js +68 -0
- package/dist/catalog/build-catalog.js.map +1 -0
- package/dist/cli-hint.d.ts +22 -0
- package/dist/cli-hint.js +55 -0
- package/dist/cli-hint.js.map +1 -0
- package/dist/index.js +129 -22
- package/dist/index.js.map +1 -1
- package/dist/install-skill.js +1 -1
- package/dist/modules/apimapper/auto-layout.d.ts +21 -0
- package/dist/modules/apimapper/auto-layout.js +54 -0
- package/dist/modules/apimapper/auto-layout.js.map +1 -0
- package/dist/modules/apimapper/cache.js +25 -17
- package/dist/modules/apimapper/cache.js.map +1 -1
- package/dist/modules/apimapper/client.d.ts +115 -4
- package/dist/modules/apimapper/client.js +699 -304
- package/dist/modules/apimapper/client.js.map +1 -1
- package/dist/modules/apimapper/connections-format.d.ts +31 -1
- package/dist/modules/apimapper/connections-format.js +97 -5
- package/dist/modules/apimapper/connections-format.js.map +1 -1
- package/dist/modules/apimapper/connections.d.ts +9 -7
- package/dist/modules/apimapper/connections.js +449 -127
- package/dist/modules/apimapper/connections.js.map +1 -1
- package/dist/modules/apimapper/credential-sanitizer.d.ts +5 -0
- package/dist/modules/apimapper/credential-sanitizer.js +60 -1
- package/dist/modules/apimapper/credential-sanitizer.js.map +1 -1
- package/dist/modules/apimapper/credentials.js +105 -61
- package/dist/modules/apimapper/credentials.js.map +1 -1
- package/dist/modules/apimapper/diagnose.js +21 -2
- package/dist/modules/apimapper/diagnose.js.map +1 -1
- package/dist/modules/apimapper/elicitation.d.ts +29 -0
- package/dist/modules/apimapper/elicitation.js +62 -0
- package/dist/modules/apimapper/elicitation.js.map +1 -1
- package/dist/modules/apimapper/example-extract.d.ts +13 -0
- package/dist/modules/apimapper/example-extract.js +111 -0
- package/dist/modules/apimapper/example-extract.js.map +1 -0
- package/dist/modules/apimapper/filter-operators.d.ts +24 -0
- package/dist/modules/apimapper/filter-operators.js +103 -0
- package/dist/modules/apimapper/filter-operators.js.map +1 -0
- package/dist/modules/apimapper/flows-format.js +92 -22
- package/dist/modules/apimapper/flows-format.js.map +1 -1
- package/dist/modules/apimapper/flows.d.ts +8 -7
- package/dist/modules/apimapper/flows.js +275 -120
- package/dist/modules/apimapper/flows.js.map +1 -1
- package/dist/modules/apimapper/gateway/advanced-read-tool.d.ts +9 -0
- package/dist/modules/apimapper/gateway/advanced-read-tool.js +172 -0
- package/dist/modules/apimapper/gateway/advanced-read-tool.js.map +1 -0
- package/dist/modules/apimapper/gateway/advanced-tool.js +66 -106
- package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -1
- package/dist/modules/apimapper/gateway/collect-module-tools.d.ts +17 -0
- package/dist/modules/apimapper/gateway/collect-module-tools.js +44 -0
- package/dist/modules/apimapper/gateway/collect-module-tools.js.map +1 -0
- package/dist/modules/apimapper/gateway/essentials.d.ts +1 -1
- package/dist/modules/apimapper/gateway/essentials.js +21 -2
- package/dist/modules/apimapper/gateway/essentials.js.map +1 -1
- package/dist/modules/apimapper/gateway/gateway-shared.d.ts +21 -0
- package/dist/modules/apimapper/gateway/gateway-shared.js +124 -0
- package/dist/modules/apimapper/gateway/gateway-shared.js.map +1 -0
- package/dist/modules/apimapper/gateway/test-support.d.ts +1 -17
- package/dist/modules/apimapper/gateway/test-support.js +4 -33
- package/dist/modules/apimapper/gateway/test-support.js.map +1 -1
- package/dist/modules/apimapper/get-skill-cores.d.ts +4 -0
- package/dist/modules/apimapper/get-skill-cores.js +220 -0
- package/dist/modules/apimapper/get-skill-cores.js.map +1 -0
- package/dist/modules/apimapper/get-skill.d.ts +1 -1
- package/dist/modules/apimapper/get-skill.js +74 -9
- package/dist/modules/apimapper/get-skill.js.map +1 -1
- package/dist/modules/apimapper/graph-builder.d.ts +85 -2
- package/dist/modules/apimapper/graph-builder.js +151 -15
- package/dist/modules/apimapper/graph-builder.js.map +1 -1
- package/dist/modules/apimapper/graph.js +152 -48
- package/dist/modules/apimapper/graph.js.map +1 -1
- package/dist/modules/apimapper/index.js +27 -13
- package/dist/modules/apimapper/index.js.map +1 -1
- package/dist/modules/apimapper/jmespath-test.d.ts +4 -0
- package/dist/modules/apimapper/jmespath-test.js +152 -0
- package/dist/modules/apimapper/jmespath-test.js.map +1 -0
- package/dist/modules/apimapper/library.js +553 -88
- package/dist/modules/apimapper/library.js.map +1 -1
- package/dist/modules/apimapper/license.js +12 -36
- package/dist/modules/apimapper/license.js.map +1 -1
- package/dist/modules/apimapper/list-footer.d.ts +27 -0
- package/dist/modules/apimapper/list-footer.js +57 -0
- package/dist/modules/apimapper/list-footer.js.map +1 -0
- package/dist/modules/apimapper/local-sources.js +100 -57
- package/dist/modules/apimapper/local-sources.js.map +1 -1
- package/dist/modules/apimapper/mcp-client-identity.d.ts +32 -0
- package/dist/modules/apimapper/mcp-client-identity.js +70 -0
- package/dist/modules/apimapper/mcp-client-identity.js.map +1 -0
- package/dist/modules/apimapper/merge-constants.d.ts +6 -0
- package/dist/modules/apimapper/merge-constants.js +26 -0
- package/dist/modules/apimapper/merge-constants.js.map +1 -0
- package/dist/modules/apimapper/misc.js +13 -27
- package/dist/modules/apimapper/misc.js.map +1 -1
- package/dist/modules/apimapper/node-schema.d.ts +52 -2
- package/dist/modules/apimapper/node-schema.js +95 -4
- package/dist/modules/apimapper/node-schema.js.map +1 -1
- package/dist/modules/apimapper/onboarding.d.ts +59 -1
- package/dist/modules/apimapper/onboarding.js +231 -28
- package/dist/modules/apimapper/onboarding.js.map +1 -1
- package/dist/modules/apimapper/read-cache.d.ts +16 -3
- package/dist/modules/apimapper/read-cache.js +59 -4
- package/dist/modules/apimapper/read-cache.js.map +1 -1
- package/dist/modules/apimapper/render/index.js +26 -5
- package/dist/modules/apimapper/render/index.js.map +1 -1
- package/dist/modules/apimapper/resource-id.d.ts +13 -0
- package/dist/modules/apimapper/resource-id.js +69 -0
- package/dist/modules/apimapper/resource-id.js.map +1 -0
- package/dist/modules/apimapper/schema.js +9 -18
- package/dist/modules/apimapper/schema.js.map +1 -1
- package/dist/modules/apimapper/settings.js +49 -52
- package/dist/modules/apimapper/settings.js.map +1 -1
- package/dist/modules/apimapper/sites-tools.d.ts +29 -0
- package/dist/modules/apimapper/sites-tools.js +165 -0
- package/dist/modules/apimapper/sites-tools.js.map +1 -0
- package/dist/modules/apimapper/tool-result.d.ts +66 -0
- package/dist/modules/apimapper/tool-result.js +125 -0
- package/dist/modules/apimapper/tool-result.js.map +1 -0
- package/dist/modules/apimapper/toolslist-size.d.ts +12 -11
- package/dist/modules/apimapper/toolslist-size.js +34 -21
- package/dist/modules/apimapper/toolslist-size.js.map +1 -1
- package/dist/modules/apimapper/types.d.ts +34 -0
- package/dist/modules/apimapper/types.js +1 -1
- package/dist/modules/apimapper/types.js.map +1 -1
- package/dist/modules/apimapper/whitelist-drift.d.ts +85 -0
- package/dist/modules/apimapper/whitelist-drift.js +375 -0
- package/dist/modules/apimapper/whitelist-drift.js.map +1 -0
- package/dist/modules/apimapper/workflows.js +302 -58
- package/dist/modules/apimapper/workflows.js.map +1 -1
- package/dist/modules/apimapper/yootheme-binding.d.ts +35 -0
- package/dist/modules/apimapper/yootheme-binding.js +267 -0
- package/dist/modules/apimapper/yootheme-binding.js.map +1 -0
- package/dist/platform/index.d.ts +56 -0
- package/dist/platform/index.js +158 -2
- package/dist/platform/index.js.map +1 -1
- package/dist/proxy/bridge.d.ts +35 -0
- package/dist/proxy/bridge.js +129 -0
- package/dist/proxy/bridge.js.map +1 -0
- package/dist/proxy/mode.d.ts +9 -0
- package/dist/proxy/mode.js +20 -0
- package/dist/proxy/mode.js.map +1 -0
- package/dist/setup/detect-clients.d.ts +40 -1
- package/dist/setup/detect-clients.js +148 -1
- package/dist/setup/detect-clients.js.map +1 -1
- package/dist/setup/probe-auth.d.ts +51 -0
- package/dist/setup/probe-auth.js +141 -0
- package/dist/setup/probe-auth.js.map +1 -0
- package/dist/setup/probe-handshake.js +40 -7
- package/dist/setup/probe-handshake.js.map +1 -1
- package/dist/setup/remove-config.d.ts +8 -0
- package/dist/setup/remove-config.js +145 -0
- package/dist/setup/remove-config.js.map +1 -0
- package/dist/setup/uninstall.d.ts +34 -0
- package/dist/setup/uninstall.js +147 -0
- package/dist/setup/uninstall.js.map +1 -0
- package/dist/setup-cli.d.ts +16 -0
- package/dist/setup-cli.js +63 -1
- package/dist/setup-cli.js.map +1 -1
- package/dist/sites/loader.d.ts +48 -0
- package/dist/sites/loader.js +134 -0
- package/dist/sites/loader.js.map +1 -0
- package/dist/sites/schema.d.ts +69 -0
- package/dist/sites/schema.js +71 -0
- package/dist/sites/schema.js.map +1 -0
- package/dist/sites/secret-resolver.d.ts +47 -0
- package/dist/sites/secret-resolver.js +150 -0
- package/dist/sites/secret-resolver.js.map +1 -0
- package/dist/skill-instructions.d.ts +14 -1
- package/dist/skill-instructions.js +35 -6
- package/dist/skill-instructions.js.map +1 -1
- package/dist/transports/stdio.js +4 -4
- package/dist/transports/stdio.js.map +1 -1
- package/dist/uninstall-skill.d.ts +27 -0
- package/dist/uninstall-skill.js +89 -0
- package/dist/uninstall-skill.js.map +1 -0
- package/docs/architecture.md +21 -21
- package/docs/customgraph-internal-migration.md +4 -4
- package/docs/security.md +2 -21
- package/docs/tools.md +40 -12
- package/manifest.json +77 -79
- package/package.json +69 -65
- package/skills/apimapper/SKILL.md +128 -7
- package/skills/apimapper/reference/conditional-style-multi-items.md +114 -0
- package/skills/apimapper/reference/dynamize-existing-layout.md +158 -0
- package/skills/apimapper/reference/jmespath-cookbook.md +241 -0
- package/skills/apimapper/reference/jmespath-pitfalls.md +189 -0
- package/skills/apimapper/reference/joomla.md +1 -1
- package/skills/apimapper/reference/library-template-discovery.md +65 -0
- package/skills/apimapper/reference/merge-two-sources-on-key.md +204 -0
- package/skills/apimapper/reference/oauth.md +143 -52
- package/skills/apimapper/reference/troubleshooting.md +22 -2
- package/skills/apimapper/reference/yootheme-source-to-builder-handoff.md +348 -0
- package/skills/apimapper/reference/yootheme.md +75 -44
- package/dist/auth/oauth-provider.d.ts +0 -68
- package/dist/auth/oauth-provider.js +0 -232
- package/dist/auth/oauth-provider.js.map +0 -1
- package/dist/server-http.d.ts +0 -22
- package/dist/server-http.js +0 -159
- package/dist/server-http.js.map +0 -1
- package/dist/transports/http.d.ts +0 -29
- package/dist/transports/http.js +0 -267
- 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,
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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(
|
|
191
|
+
body: JSON.stringify(payload),
|
|
160
192
|
});
|
|
161
193
|
if (!r.success) {
|
|
162
|
-
return
|
|
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:
|
|
176
|
-
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: ["
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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: [...],
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 });
|