@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,12 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive,
|
|
2
|
+
import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, pickFields, createProgressReporter, } from "@getimo/mcp-toolkit";
|
|
3
3
|
import { request, hintFor, WP_BASE, WP_USER, authConfigured } from "./client.js";
|
|
4
|
+
import { restErrorResult } from "./tool-result.js";
|
|
4
5
|
import { toRows } from "./types.js";
|
|
5
6
|
import { unwrapEntity } from "./envelope.js";
|
|
6
7
|
import { ambiguityFallbackError, } from "./elicitation.js";
|
|
7
8
|
import { fetchWithTimeout } from "./diagnose.js";
|
|
8
9
|
import { filterByNameQuery } from "./node-schema.js";
|
|
9
|
-
import {
|
|
10
|
+
import { withOverflowFooter, truncatedByLimitNote } from "./list-footer.js";
|
|
11
|
+
import { CONNECTION_TABLE_COLUMNS, CONNECTION_FULL_COLUMNS, CONNECTION_COMPACT_COLUMNS, CONNECTION_LIST_NEXT_STEPS, RESOURCE_TABLE_COLUMNS, mapConnectionRow, mapConnectionRowFull, compactConnectionRow, mapResourceRow, buildConnectionDetail, buildConnectionTestStats, buildHealthCheckStats, buildHealthStats, } from "./connections-format.js";
|
|
10
12
|
// W3 Stage-2 hardening — applyTrim + extractResourceList live in the
|
|
11
13
|
// sibling connections-trim.ts (pure functions, ~200 lines). Imported for
|
|
12
14
|
// internal use by the connection_data + connection_resources handlers,
|
|
@@ -24,6 +26,7 @@ export { applyTrim, extractResourceList };
|
|
|
24
26
|
// formatter; connection_get / _update / _delete pass through unwrapEntity
|
|
25
27
|
// + the rich-card builder which already reads from the same row map.
|
|
26
28
|
import { normalizeConnectionFromWire } from "./normalizers.js";
|
|
29
|
+
import { normalizeResourceIdFields } from "./resource-id.js";
|
|
27
30
|
/**
|
|
28
31
|
* F-15 (W1.9) — downstream probe of api.wootsup.com from `apimapper_health`.
|
|
29
32
|
*
|
|
@@ -71,13 +74,16 @@ async function probeApimapperApi() {
|
|
|
71
74
|
/**
|
|
72
75
|
* Register the connection CRUD + probe + sample + pipeline tools.
|
|
73
76
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* Chat-first disambiguation (decision 2026-06-15): on genuine credential
|
|
78
|
+
* ambiguity `connection_create` returns a structured candidate-list error
|
|
79
|
+
* (the assistant asks in plain chat) rather than driving the native
|
|
80
|
+
* elicitation picker. The picker is reserved for OAuth authorization
|
|
81
|
+
* (`apimapper_oauth_authorize_begin` in credentials.ts), so this module no
|
|
82
|
+
* longer takes an elicitation capability.
|
|
83
|
+
*
|
|
84
|
+
* @param server the tool registrar (essentials forward, rest captured).
|
|
79
85
|
*/
|
|
80
|
-
export function registerConnectionTools(server
|
|
86
|
+
export function registerConnectionTools(server) {
|
|
81
87
|
// ── apimapper_health ───────────────────────────────────────────────
|
|
82
88
|
server.registerTool("apimapper_health", {
|
|
83
89
|
title: "API Mapper REST Health",
|
|
@@ -136,8 +142,16 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
catch (e) {
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
// A4 (Wave-B audit, 2026-06-03): this catch builds a STRING into the
|
|
146
|
+
// health-snapshot `checks` map and then continues to buildHealthStats —
|
|
147
|
+
// it is NOT an MCP error-result return, so the structured
|
|
148
|
+
// errorResult/restErrorResult builders do not apply here (they would
|
|
149
|
+
// early-return and abandon the snapshot). The lone `errorWithSuggestion`
|
|
150
|
+
// call is dropped in favour of the same raw, already-sanitised message
|
|
151
|
+
// the sibling `FAIL: ${r.error}` branch above surfaces — `request()`
|
|
152
|
+
// sanitises and never throws, so this remains a defensive guard.
|
|
153
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
154
|
+
checks.connectivity = `ERROR: ${message}`;
|
|
141
155
|
}
|
|
142
156
|
// W3.1 — health snapshot is a flat scalar set: statsResult is the
|
|
143
157
|
// goldstandard fit. Every check becomes a stat so an AI client reads
|
|
@@ -150,17 +164,28 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
150
164
|
// rc.10 A5 (2026-05-19) — description states the default limit
|
|
151
165
|
// already covers a typical install. The Maria-walkthrough log
|
|
152
166
|
// showed AI defaulting to limit:200/500 reflexively.
|
|
153
|
-
description: "List
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
167
|
+
description: "List API Mapper connections — a LEAN projection by default (id / name / source / host / health). " +
|
|
168
|
+
"The default limit of 25 already covers a typical install — most customers run with 5-25 " +
|
|
169
|
+
"connections, increase the limit only if you have a specific reason to expect more. " +
|
|
170
|
+
"Pass `fields: ['all']` for the full dump (endpoint URL, method, auth_type, credential_id), " +
|
|
171
|
+
"or a list of specific extra columns. Use apimapper_connection_get for full details of a single connection." +
|
|
172
|
+
"\n\nExample:\n apimapper_connection_list({}) // lean projection, up to 25 connections\n" +
|
|
173
|
+
" apimapper_connection_list({ source: 'user' }) // narrow to user-created only\n" +
|
|
174
|
+
" apimapper_connection_list({ fields: ['all'] }) // full detail dump",
|
|
158
175
|
inputSchema: {
|
|
159
176
|
source: z
|
|
160
177
|
.enum(["library", "demo", "user", "all"])
|
|
161
178
|
.default("all")
|
|
162
179
|
.describe("Filter by origin"),
|
|
163
|
-
limit: z.number().min(1).max(500).default(
|
|
180
|
+
limit: z.number().min(1).max(500).default(25).describe("Max items (1-500). Default 25 already covers a typical install — do not raise unless you have evidence the list is larger."),
|
|
181
|
+
// F198 — opt-in full detail. The default projection is the lean
|
|
182
|
+
// id/name/source/host/health set; `fields` widens it. `['all']` (or
|
|
183
|
+
// any non-empty array) returns the full endpoint/method/auth/cred dump.
|
|
184
|
+
fields: z
|
|
185
|
+
.array(z.string().min(1))
|
|
186
|
+
.optional()
|
|
187
|
+
.describe("Opt into extra columns. Omit for the lean default (id/name/source/host/health). " +
|
|
188
|
+
"Pass ['all'] for the full dump (endpoint URL, method, auth_type, credential_id)."),
|
|
164
189
|
// W1.18 (F-32) — case-insensitive substring filter applied
|
|
165
190
|
// in-memory AFTER the upstream fetch (no per-name REST call).
|
|
166
191
|
// Min 2 chars at the schema boundary blocks the single-char
|
|
@@ -172,15 +197,10 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
172
197
|
.describe("Case-insensitive substring filter on connection name. Applied in-memory after the upstream fetch — does NOT change the REST query. Must be 2+ characters; single-char probes are blocked. Combine with `source` for narrower results."),
|
|
173
198
|
},
|
|
174
199
|
annotations: readOnly({ title: "List Connections", openWorld: true }),
|
|
175
|
-
}, async ({ source, limit, name_query }) => {
|
|
200
|
+
}, async ({ source, limit, name_query, fields }) => {
|
|
176
201
|
const r = await request("/connections");
|
|
177
202
|
if (!r.success) {
|
|
178
|
-
return
|
|
179
|
-
message: r.error ?? "connection list failed",
|
|
180
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
181
|
-
suggestion: hintFor(r.errorCode),
|
|
182
|
-
details: { source, limit, name_query },
|
|
183
|
-
});
|
|
203
|
+
return restErrorResult(r, { source, limit, name_query }, { message: "connection list failed" });
|
|
184
204
|
}
|
|
185
205
|
// F-LS-04 — normalise the camelCase wire shape (authType, credentialId,
|
|
186
206
|
// …) to snake_case BEFORE anything downstream reads it. The
|
|
@@ -195,24 +215,53 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
195
215
|
// subset, not the haystack. See `filterByNameQuery` JSDoc for the
|
|
196
216
|
// case-folding + undefined-skip contract shared with flow_list.
|
|
197
217
|
items = filterByNameQuery(items, name_query);
|
|
218
|
+
// F198 — capture the matched total BEFORE the slice so the footer can
|
|
219
|
+
// report exactly how many rows were dropped by the limit.
|
|
220
|
+
const totalMatched = items.length;
|
|
198
221
|
items = items.slice(0, limit);
|
|
222
|
+
// F198 — opt into the full detail dump only when `fields` is supplied.
|
|
223
|
+
// Any non-empty `fields` array (typically ['all']) widens the table to
|
|
224
|
+
// the full column set; the default stays the lean projection.
|
|
225
|
+
const wantFull = Array.isArray(fields) && fields.length > 0;
|
|
226
|
+
// F198 — the honest "N more — filter with name_query or raise limit"
|
|
227
|
+
// note fires whenever the limit actually dropped matched rows. The
|
|
228
|
+
// compact-render note (withOverflowFooter) still rides along for the
|
|
229
|
+
// truncated-text case.
|
|
230
|
+
const overflowNote = truncatedByLimitNote(totalMatched, limit, "connections");
|
|
231
|
+
const baseFooter = overflowNote
|
|
232
|
+
? `${CONNECTION_LIST_NEXT_STEPS}\n${overflowNote}`
|
|
233
|
+
: CONNECTION_LIST_NEXT_STEPS;
|
|
199
234
|
// W3.1 — tableResult: ASCII table for the LLM + typed DataTable payload
|
|
200
|
-
// for the Rich Card.
|
|
201
|
-
//
|
|
202
|
-
// footer carries the next-step guidance.
|
|
235
|
+
// for the Rich Card. Default = lean projection (id/name/source/host/
|
|
236
|
+
// health); `fields` = full dump. IA-7: id stays llmOnly. IA-10: the
|
|
237
|
+
// footer carries the next-step guidance + the F198 overflow note.
|
|
203
238
|
return tableResult(toRows(items), {
|
|
204
|
-
columns: CONNECTION_TABLE_COLUMNS,
|
|
239
|
+
columns: wantFull ? CONNECTION_FULL_COLUMNS : CONNECTION_TABLE_COLUMNS,
|
|
205
240
|
compactColumns: CONNECTION_COMPACT_COLUMNS,
|
|
206
|
-
map: mapConnectionRow,
|
|
241
|
+
map: wantFull ? mapConnectionRowFull : mapConnectionRow,
|
|
207
242
|
compactMap: compactConnectionRow,
|
|
208
|
-
|
|
209
|
-
|
|
243
|
+
// F198 — the "N more" overflow note rides in the HEADER (rendered
|
|
244
|
+
// first, never truncated) so it survives even when a 21+ row table
|
|
245
|
+
// compacts to 2000 chars and drops the footer. The footer keeps the
|
|
246
|
+
// next-step guidance + the compact-render note for the non-overflow
|
|
247
|
+
// truncation case.
|
|
248
|
+
header: (n) => (overflowNote ? `${n} connections — ${overflowNote}` : `${n} connections`),
|
|
249
|
+
// Minor (2026-06-10): at 21+ rows the toolkit compacts the LLM-visible
|
|
250
|
+
// table to 2000 chars — append an honest "use filters" note so the
|
|
251
|
+
// agent does not mistake a truncated list for the full set.
|
|
252
|
+
footer: withOverflowFooter(baseFooter, items.length, "connections"),
|
|
210
253
|
});
|
|
211
254
|
});
|
|
212
255
|
// ── apimapper_connection_get ───────────────────────────────────────
|
|
213
256
|
server.registerTool("apimapper_connection_get", {
|
|
214
257
|
title: "Get Connection",
|
|
215
|
-
description: "
|
|
258
|
+
description: "Get the full configuration of one connection by ID (endpoints, auth/" +
|
|
259
|
+
"credential link, default params + headers, items_path, pipeline). Use to " +
|
|
260
|
+
"inspect or debug a single connection before editing it or wiring it into a flow. " +
|
|
261
|
+
"Keywords: get connection, connection detail, inspect connection, show config, by ID. " +
|
|
262
|
+
"When NOT to use: to enumerate all connections use apimapper_connection_list; to " +
|
|
263
|
+
"fetch sample rows use apimapper_connection_data; to probe reachability/auth use " +
|
|
264
|
+
"apimapper_connection_test; to change fields use apimapper_connection_update." +
|
|
216
265
|
"\n\nExample:\n apimapper_connection_get({ id: 'con_abc123' })",
|
|
217
266
|
inputSchema: {
|
|
218
267
|
id: z.string().describe('Connection ID (e.g., "conn_Mz33OVPF1z3ap8fbbQtpx"). Use apimapper_connection_list.'),
|
|
@@ -222,14 +271,16 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
222
271
|
// PHP wraps via fromControllerResponse($response, 'connection') →
|
|
223
272
|
// `{success:true, connection:{…}}`. Unwrap defensively so a future
|
|
224
273
|
// flatten on the PHP side doesn't break us. Audit: F-A1-01.
|
|
225
|
-
|
|
274
|
+
// S-MED-1 (Wave-B 2026-06-03): sanitize the connection read. The
|
|
275
|
+
// Connection shape carries `headers`/`params` arrays that can hold an
|
|
276
|
+
// inline Authorization / X-API-Key value; {sanitize:true} runs the
|
|
277
|
+
// response through sanitizeSecrets (which now also scrubs {name,value}
|
|
278
|
+
// header pairs) so the SECURITY.md "every response is sanitized"
|
|
279
|
+
// guarantee holds structurally, not only by buildConnectionDetail's
|
|
280
|
+
// whitelist curation.
|
|
281
|
+
const r = await request(`/connections/${encodeURIComponent(id)}`, {}, { sanitize: true });
|
|
226
282
|
if (!r.success) {
|
|
227
|
-
return
|
|
228
|
-
message: r.error ?? "connection get failed",
|
|
229
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
230
|
-
suggestion: hintFor(r.errorCode),
|
|
231
|
-
details: { id },
|
|
232
|
-
});
|
|
283
|
+
return restErrorResult(r, { id }, { message: "connection get failed" });
|
|
233
284
|
}
|
|
234
285
|
const conn = unwrapEntity(r.data, "connection");
|
|
235
286
|
if (!conn || Object.keys(conn).length === 0) {
|
|
@@ -243,25 +294,70 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
243
294
|
// W3.1 — detailResult: grouped key-value detail for the Rich Card.
|
|
244
295
|
// IA-7: opaque IDs are copyable code entries. IA-10: a dedicated
|
|
245
296
|
// "Next steps" group carries the follow-up calls.
|
|
246
|
-
|
|
297
|
+
// F9 (2026-06-09): run the same camelCase→snake_case wire bridge the
|
|
298
|
+
// list path applies (F-LS-04) — buildConnectionDetail reads
|
|
299
|
+
// c.auth_type/credential_id; the raw wire shape carries authType/
|
|
300
|
+
// credentialId, so without this the detail rendered auth "none"
|
|
301
|
+
// while the list showed the real auth for the SAME connection.
|
|
302
|
+
return buildConnectionDetail(id, normalizeConnectionFromWire(conn));
|
|
247
303
|
});
|
|
248
304
|
// ── apimapper_connection_create ────────────────────────────────────
|
|
249
305
|
server.registerTool("apimapper_connection_create", {
|
|
250
306
|
title: "Create Connection",
|
|
251
|
-
description: "Create a
|
|
307
|
+
description: "Create a custom connection. **Use ONLY when no library template matches your target API.** " +
|
|
308
|
+
"First call `apimapper_library_featured()` or `apimapper_library_list({ search: '<api-name>' })` (min 3 chars) — " +
|
|
309
|
+
"Google Sheets, Calendly, Notion, Airtable, GitHub, Pexels, Unsplash, OpenWeatherMap, REST Countries, " +
|
|
310
|
+
"Google Drive/Docs/Slides/Tasks all have ready-to-use templates with auto-configured auth and " +
|
|
311
|
+
"field-detection. Library activation via `apimapper_library_activate({ id })` is the canonical " +
|
|
312
|
+
"customer path; connection_create is the fallback for niche or unknown APIs. " +
|
|
313
|
+
"The server enforces this: a custom create on an API already covered by a curated template is " +
|
|
314
|
+
"blocked with a 409 naming the template to activate — pass `acknowledge_no_library: true` to " +
|
|
315
|
+
"intentionally override that block. " +
|
|
316
|
+
// A16 (2026-06-10): the guard matches on the HOST, so a genuinely
|
|
317
|
+
// uncovered endpoint never blocks — do NOT set acknowledge_no_library
|
|
318
|
+
// pre-emptively.
|
|
319
|
+
"The guard matches on the request HOST against the curated library; a connection to an " +
|
|
320
|
+
"uncovered host is NEVER blocked and needs no `acknowledge_no_library` — only set that flag " +
|
|
321
|
+
"AFTER you actually receive a 409." +
|
|
322
|
+
"\n\nNote: connection auth_type spellings differ from apimapper_credential_create's: " +
|
|
323
|
+
"here it is `basic` / `oauth2`, whereas credential_create uses `basic_auth` / `oauth2_code` / `oauth2_cc`. " +
|
|
324
|
+
"Use the spelling for the tool you are calling." +
|
|
252
325
|
"\n\nExample:\n apimapper_connection_create({ name: 'Pexels API', endpoint: 'https://api.pexels.com/v1/search', method: 'GET', auth_type: 'bearer', items_path: 'photos' })",
|
|
253
326
|
inputSchema: {
|
|
254
327
|
name: z.string().min(1).describe('Connection name (e.g., "My Blog API")'),
|
|
255
328
|
endpoint: z.string().describe('Endpoint path or full URL (e.g., "https://api.example.com/posts")'),
|
|
256
329
|
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET").describe("HTTP method"),
|
|
257
|
-
auth_type: z
|
|
330
|
+
auth_type: z
|
|
331
|
+
.enum(["none", "api_key", "bearer", "basic", "oauth2"])
|
|
332
|
+
.default("none")
|
|
333
|
+
.describe("Auth type (snake_case wire key). " +
|
|
334
|
+
"NB: credential_create spells these `basic_auth` (here `basic`) and `oauth2_code`/`oauth2_cc` (here `oauth2`)."),
|
|
258
335
|
credential_id: z.string().optional().describe("Credential ID (snake_case wire key) if auth_type requires one"),
|
|
259
336
|
items_path: z.string().optional().describe('JSONPath to items array (snake_case wire key)'),
|
|
260
337
|
cache_ttl: z.number().int().min(0).default(3600).describe("Cache TTL in seconds (snake_case wire key)"),
|
|
261
338
|
description: z.string().optional().describe("Free-text description"),
|
|
339
|
+
// Phase 6 — audited override for the server-side library-first guard.
|
|
340
|
+
// The PHP guard blocks custom creates on APIs that already have a
|
|
341
|
+
// curated library template (returning a 409 whose message names the
|
|
342
|
+
// template via apimapper_library_activate). Setting this flag relays
|
|
343
|
+
// `acknowledge_no_library: true` into the POST body so the guard lets
|
|
344
|
+
// the create through. Flows into the body via `resolvedInput = {...input}`.
|
|
345
|
+
acknowledge_no_library: z.boolean().optional().describe("Set true ONLY when no library template fits and you intentionally need a custom connection. " +
|
|
346
|
+
"The server blocks custom creates on APIs that already have a curated library template; " +
|
|
347
|
+
"this flag is the audited override. Prefer apimapper_library_activate({ id }) when a template exists."),
|
|
348
|
+
// Phase 2.7 (change protocol): the action is IMPLICIT
|
|
349
|
+
// (`connection.created`) — only `summary` is needed; the handler
|
|
350
|
+
// defaults the action. Threaded into the POST body as
|
|
351
|
+
// `change: { summary }` (conditional, never a top-level key).
|
|
352
|
+
summary: z
|
|
353
|
+
.string()
|
|
354
|
+
.max(280)
|
|
355
|
+
.optional()
|
|
356
|
+
.describe('Optional one-line summary of this change for the change history ' +
|
|
357
|
+
'(e.g., "Added a Pexels photo connection").'),
|
|
262
358
|
},
|
|
263
359
|
annotations: creating({ title: "Create Connection", openWorld: true }),
|
|
264
|
-
}, async (input) => {
|
|
360
|
+
}, async ({ summary, ...input }) => {
|
|
265
361
|
// W3.6 — when the connection's auth_type needs a credential and none is
|
|
266
362
|
// given: exactly 1 stored credential → auto-pick; >1 → elicitChoice;
|
|
267
363
|
// elicitChoice null (unsupported client / declined) → structured
|
|
@@ -270,12 +366,7 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
270
366
|
if (input.auth_type !== "none" && !input.credential_id) {
|
|
271
367
|
const lr = await request("/credentials", {}, { sanitize: true });
|
|
272
368
|
if (!lr.success) {
|
|
273
|
-
return
|
|
274
|
-
message: lr.error ?? "credential lookup failed",
|
|
275
|
-
code: lr.errorCode ?? (lr.status ? String(lr.status) : undefined),
|
|
276
|
-
suggestion: hintFor(lr.errorCode),
|
|
277
|
-
details: { name: input.name, auth_type: input.auth_type },
|
|
278
|
-
});
|
|
369
|
+
return restErrorResult(lr, { name: input.name, auth_type: input.auth_type }, { message: "credential lookup failed" });
|
|
279
370
|
}
|
|
280
371
|
const creds = Array.isArray(lr.data?.credentials) ? lr.data.credentials : [];
|
|
281
372
|
if (creds.length === 0) {
|
|
@@ -290,39 +381,67 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
290
381
|
resolvedInput = { ...input, credential_id: creds[0].id };
|
|
291
382
|
}
|
|
292
383
|
else {
|
|
384
|
+
// Chat-first disambiguation (decision 2026-06-15): the native
|
|
385
|
+
// elicitation picker renders as a clunky collapsed TUI in some
|
|
386
|
+
// hosts. For an ordinary credential pick the assistant asking in
|
|
387
|
+
// plain chat is nicer, so we skip the picker and return the
|
|
388
|
+
// structured candidate-list error directly — its `candidates`
|
|
389
|
+
// carry id+label, so the assistant sees the names and can ask.
|
|
390
|
+
// (The OAuth authorize path KEEPS the picker: explicit consent on
|
|
391
|
+
// WHICH credential to authorize is security-relevant.)
|
|
293
392
|
const candidates = creds.map((c) => ({
|
|
294
393
|
id: c.id,
|
|
295
394
|
label: c.name,
|
|
296
395
|
}));
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
paramName: "credential_id",
|
|
305
|
-
what: "credentials",
|
|
306
|
-
candidates,
|
|
307
|
-
extraDetails: { name: input.name, auth_type: input.auth_type },
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
resolvedInput = { ...input, credential_id: picked };
|
|
396
|
+
return ambiguityFallbackError({
|
|
397
|
+
code: "credential_ambiguous",
|
|
398
|
+
paramName: "credential_id",
|
|
399
|
+
what: "credentials",
|
|
400
|
+
candidates,
|
|
401
|
+
extraDetails: { name: input.name, auth_type: input.auth_type },
|
|
402
|
+
});
|
|
311
403
|
}
|
|
312
404
|
}
|
|
405
|
+
// Phase 2.7 — implicit action (`connection.created`); attach `change:
|
|
406
|
+
// { summary }` only when supplied so the body is unchanged otherwise.
|
|
407
|
+
// `summary` was destructured out of `input` above, so it never leaks as a
|
|
408
|
+
// top-level connection column.
|
|
409
|
+
const createBody = summary !== undefined
|
|
410
|
+
? { ...resolvedInput, change: { summary } }
|
|
411
|
+
: resolvedInput;
|
|
313
412
|
// PHP fromControllerResponse(_, 'connection', 201) → {success, connection:{…}}.
|
|
314
413
|
// Audit: F-A1-02.
|
|
315
414
|
const r = await request("/connections", {
|
|
316
415
|
method: "POST",
|
|
317
|
-
body: JSON.stringify(
|
|
416
|
+
body: JSON.stringify(createBody),
|
|
318
417
|
});
|
|
319
418
|
if (!r.success) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
419
|
+
// M1 (MCP-relay audit) — wire-shape parity with the admin-ui. On the
|
|
420
|
+
// library-first guard block the PHP REST body carries a structured
|
|
421
|
+
// `{error, error_code:'library_template_available', library_suggestion:
|
|
422
|
+
// {matched_host, templates[], activate_call}}`. The admin-ui consumes
|
|
423
|
+
// that object; the MCP relay must too. We surface the structured
|
|
424
|
+
// `library_suggestion` + `error_code` in `details` (→ machine-readable
|
|
425
|
+
// _meta.ui.payload.details) IN ADDITION to keeping the actionable
|
|
426
|
+
// `message` string verbatim — the message embeds the activate call so
|
|
427
|
+
// a text-only agent still recovers. `errorBody` is the parsed non-2xx
|
|
428
|
+
// body threaded through the shared client (see client.ts request()).
|
|
429
|
+
const errorBody = r.errorBody;
|
|
430
|
+
const isLibraryBlock = errorBody !== undefined &&
|
|
431
|
+
errorBody.error_code === "library_template_available";
|
|
432
|
+
return restErrorResult(r,
|
|
433
|
+
// Per-site nuance: on a library-first guard block (409 / Joomla
|
|
434
|
+
// com_ajax) the structured `error_code` + `library_suggestion` from
|
|
435
|
+
// the parsed body are surfaced in details IN ADDITION to the verbatim
|
|
436
|
+
// message string. Non-library failures keep the plain identity echo.
|
|
437
|
+
isLibraryBlock
|
|
438
|
+
? {
|
|
439
|
+
name: input.name,
|
|
440
|
+
endpoint: input.endpoint,
|
|
441
|
+
error_code: errorBody.error_code,
|
|
442
|
+
library_suggestion: errorBody.library_suggestion,
|
|
443
|
+
}
|
|
444
|
+
: { name: input.name, endpoint: input.endpoint }, { message: "connection create failed" });
|
|
326
445
|
}
|
|
327
446
|
const conn = unwrapEntity(r.data, "connection");
|
|
328
447
|
// F-3.2 (Pass-1 consolidation) — mutating-tool payload-shape rationale.
|
|
@@ -344,7 +463,7 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
344
463
|
name: conn?.name,
|
|
345
464
|
source: conn?.source,
|
|
346
465
|
next_steps: [
|
|
347
|
-
"
|
|
466
|
+
'Verify reachability via apimapper_advanced({ tool: "apimapper_connection_test", arguments: { id } }), then build a flow with apimapper_flow_setup_with_sources (or apimapper_advanced({ tool: "apimapper_flow_create" })).',
|
|
348
467
|
],
|
|
349
468
|
}, false, { maxChars: 2000 });
|
|
350
469
|
});
|
|
@@ -359,23 +478,39 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
359
478
|
.record(z.string(), z.unknown())
|
|
360
479
|
.describe('Fields to update — snake_case keys. ' +
|
|
361
480
|
'Examples: {"name":"Renamed"}, {"cache_ttl":7200}, {"endpoint":"/v2/posts"}, ' +
|
|
362
|
-
'{"credential_id":"cred_xxx"}'
|
|
481
|
+
'{"credential_id":"cred_xxx"}. ' +
|
|
482
|
+
// DATA-LOW (Wave-B 2026-06-03): kept as an open record on purpose.
|
|
483
|
+
// The settable column set is large and may widen server-side; the
|
|
484
|
+
// PHP backend column-whitelists on write (Wave A), so any unknown
|
|
485
|
+
// key is dropped server-side rather than persisted. Enumerating a
|
|
486
|
+
// .strict() allow-list here risks rejecting a legit field the
|
|
487
|
+
// server accepts, so the schema stays permissive.
|
|
488
|
+
'Unknown keys are dropped by the server (column-whitelisted on write).'),
|
|
489
|
+
// Phase 2.7 (change protocol): the action is IMPLICIT
|
|
490
|
+
// (`connection.updated`) — only `summary` is needed; the handler
|
|
491
|
+
// defaults the action. Threaded into the PUT body as
|
|
492
|
+
// `change: { summary }` (sibling of the patch fields).
|
|
493
|
+
summary: z
|
|
494
|
+
.string()
|
|
495
|
+
.max(280)
|
|
496
|
+
.optional()
|
|
497
|
+
.describe('Optional one-line summary of this change (e.g., "Pointed the endpoint at /v2/posts").'),
|
|
363
498
|
},
|
|
364
499
|
annotations: mutating({ title: "Update Connection", openWorld: true }),
|
|
365
|
-
}, async ({ id, patch }) => {
|
|
500
|
+
}, async ({ id, patch, summary }) => {
|
|
501
|
+
// Phase 2.7 — implicit action (`connection.updated`); attach `change:
|
|
502
|
+
// { summary }` only when supplied so the body is unchanged otherwise.
|
|
503
|
+
const updateBody = summary !== undefined
|
|
504
|
+
? { ...patch, change: { summary } }
|
|
505
|
+
: patch;
|
|
366
506
|
// PHP fromControllerResponse(_, 'connection') → {success, connection:{…}}.
|
|
367
507
|
// Audit: F-A1-03.
|
|
368
508
|
const r = await request(`/connections/${encodeURIComponent(id)}`, {
|
|
369
509
|
method: "PUT",
|
|
370
|
-
body: JSON.stringify(
|
|
510
|
+
body: JSON.stringify(updateBody),
|
|
371
511
|
});
|
|
372
512
|
if (!r.success) {
|
|
373
|
-
return
|
|
374
|
-
message: r.error ?? "connection update failed",
|
|
375
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
376
|
-
suggestion: hintFor(r.errorCode),
|
|
377
|
-
details: { id },
|
|
378
|
-
});
|
|
513
|
+
return restErrorResult(r, { id }, { message: "connection update failed" });
|
|
379
514
|
}
|
|
380
515
|
const conn = unwrapEntity(r.data, "connection");
|
|
381
516
|
// F-3.2 (Pass-1 consolidation) — see connection_create for the
|
|
@@ -386,7 +521,110 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
386
521
|
id: conn?.id,
|
|
387
522
|
name: conn?.name,
|
|
388
523
|
next_steps: [
|
|
389
|
-
|
|
524
|
+
'Re-test if endpoint/auth changed via apimapper_advanced({ tool: "apimapper_connection_test", arguments: { id } }); flows referencing this connection may need to be republished via apimapper_flow_full_recompile_publish.',
|
|
525
|
+
],
|
|
526
|
+
}, false, { maxChars: 2000 });
|
|
527
|
+
});
|
|
528
|
+
// ── apimapper_connection_recover ───────────────────────────────────
|
|
529
|
+
// Friction-6 fix (Task #46, 2026-05-29) — recover from a connection whose
|
|
530
|
+
// `params` field was persisted as a corrupt JSON-string holding UI metadata
|
|
531
|
+
// blobs (resource_picker, sheets_range, ...) instead of a proper key-value
|
|
532
|
+
// object. Pre-fix: connection_update rejected the patch because the existing
|
|
533
|
+
// shape failed downstream validation; the connection was un-editable.
|
|
534
|
+
//
|
|
535
|
+
// This tool issues a forgiving PUT that resets `params` to {} (and optionally
|
|
536
|
+
// sets a clean replacement via `params_replacement`), letting the customer
|
|
537
|
+
// / AI proceed with a fresh configuration. It also clears any extra cruft
|
|
538
|
+
// keys the AI agent identifies via `clear_fields`.
|
|
539
|
+
server.registerTool("apimapper_connection_recover", {
|
|
540
|
+
title: "Recover Stuck Connection",
|
|
541
|
+
description: "Recover a connection that is un-editable because its `params` field " +
|
|
542
|
+
"is corrupt (e.g. stored as a JSON-string with embedded UI metadata " +
|
|
543
|
+
"instead of an object). Resets `params` to an empty object — or to " +
|
|
544
|
+
"the value of `params_replacement` if supplied. Optionally clears " +
|
|
545
|
+
"additional fields named in `clear_fields`." +
|
|
546
|
+
"\n\nUse when: `apimapper_connection_data` returns 'Please select an " +
|
|
547
|
+
"endpoint' AND `apimapper_connection_update` rejects subsequent " +
|
|
548
|
+
"patches with a validation error about params shape." +
|
|
549
|
+
"\n\nExample:\n apimapper_connection_recover({ id: 'con_abc123' })" +
|
|
550
|
+
"\n apimapper_connection_recover({ id: 'con_abc123', params_replacement: { per_page: '20' }, endpoint: 'values' })",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
id: z
|
|
553
|
+
.string()
|
|
554
|
+
.describe("Connection ID. Use apimapper_connection_list to find."),
|
|
555
|
+
params_replacement: z
|
|
556
|
+
.record(z.string(), z.unknown())
|
|
557
|
+
.optional()
|
|
558
|
+
.describe("Clean replacement for params (key-value object). Default: {} (empty). " +
|
|
559
|
+
// DATA-LOW (Wave-B 2026-06-03): open record by design — the value
|
|
560
|
+
// becomes the connection's `params` column, which the PHP backend
|
|
561
|
+
// column-whitelists on write (Wave A). Free-form key-value pairs
|
|
562
|
+
// are the intended shape here, so no .strict() allow-list applies.
|
|
563
|
+
"Free-form key-value pairs; the server whitelists the persisted columns."),
|
|
564
|
+
endpoint: z
|
|
565
|
+
.string()
|
|
566
|
+
.optional()
|
|
567
|
+
.describe("If the connection has no endpoint configured, set it here in the " +
|
|
568
|
+
"same call (e.g., 'values' for Google Sheets, 'Search' for Pexels). " +
|
|
569
|
+
"Use apimapper_advanced({tool:'apimapper_connection_get'}) to see " +
|
|
570
|
+
"available endpoints[]."),
|
|
571
|
+
clear_fields: z
|
|
572
|
+
.array(z.string().min(1))
|
|
573
|
+
.max(20)
|
|
574
|
+
.optional()
|
|
575
|
+
.describe("Additional fields to clear (set to null). Example: ['template_fields']."),
|
|
576
|
+
confirm: z
|
|
577
|
+
.boolean()
|
|
578
|
+
.default(false)
|
|
579
|
+
.describe("Must be true to execute. On confirm:false, returns a preview of " +
|
|
580
|
+
"the planned reset. Ask user to confirm first."),
|
|
581
|
+
},
|
|
582
|
+
annotations: mutating({ title: "Recover Stuck Connection", openWorld: true }),
|
|
583
|
+
}, async ({ id, params_replacement, endpoint, clear_fields, confirm }) => {
|
|
584
|
+
const patch = {
|
|
585
|
+
params: params_replacement ?? {},
|
|
586
|
+
};
|
|
587
|
+
if (endpoint !== undefined)
|
|
588
|
+
patch.endpoint = endpoint;
|
|
589
|
+
if (Array.isArray(clear_fields)) {
|
|
590
|
+
for (const field of clear_fields) {
|
|
591
|
+
patch[field] = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!confirm) {
|
|
595
|
+
return formatResult({
|
|
596
|
+
preview: true,
|
|
597
|
+
notice: "RECOVER — Connection will be patched with the reset shown below. " +
|
|
598
|
+
"This is a forgiving reset path; existing `params` corrupted shape " +
|
|
599
|
+
"will be overwritten.",
|
|
600
|
+
target: { id },
|
|
601
|
+
patch_preview: patch,
|
|
602
|
+
instruction: "Ask user to confirm, then call again with confirm: true.",
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const r = await request(`/connections/${encodeURIComponent(id)}`, {
|
|
606
|
+
method: "PUT",
|
|
607
|
+
body: JSON.stringify(patch),
|
|
608
|
+
});
|
|
609
|
+
if (!r.success) {
|
|
610
|
+
return restErrorResult(r, { id, patch_keys: Object.keys(patch) }, {
|
|
611
|
+
message: "connection recover failed",
|
|
612
|
+
// Per-site nuance: keep the original `hintFor(...) ?? <deep-corrupt
|
|
613
|
+
// fallback>` suggestion verbatim (hintFor always returns a string,
|
|
614
|
+
// so the `??` arm is a defensive no-op preserved for parity).
|
|
615
|
+
suggestion: hintFor(r.errorCode) ??
|
|
616
|
+
"If recover still fails, the connection row may be deeper-corrupt — " +
|
|
617
|
+
"delete it via apimapper_connection_delete and re-activate the library template.",
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
const conn = unwrapEntity(r.data, "connection");
|
|
621
|
+
return formatResult({
|
|
622
|
+
recovered: true,
|
|
623
|
+
id: conn?.id ?? id,
|
|
624
|
+
name: conn?.name,
|
|
625
|
+
applied_patch_keys: Object.keys(patch),
|
|
626
|
+
next_steps: [
|
|
627
|
+
'Verify via apimapper_advanced({ tool: "apimapper_connection_get", arguments: { id } }); re-test via apimapper_advanced({ tool: "apimapper_connection_test", arguments: { id } }) before re-publishing flows.',
|
|
390
628
|
],
|
|
391
629
|
}, false, { maxChars: 2000 });
|
|
392
630
|
});
|
|
@@ -402,14 +640,29 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
402
640
|
.boolean()
|
|
403
641
|
.default(false)
|
|
404
642
|
.describe("Must be true to execute. On confirm:false, returns a preview."),
|
|
643
|
+
// Phase 2.7 (change protocol): the action is IMPLICIT
|
|
644
|
+
// (`connection.deleted`) — only `summary` is needed; the handler
|
|
645
|
+
// defaults the action. Attached to the DELETE body as
|
|
646
|
+
// `change: { summary }` (the delete otherwise has no body), and only on
|
|
647
|
+
// the real delete — the preview branch never records a change.
|
|
648
|
+
summary: z
|
|
649
|
+
.string()
|
|
650
|
+
.max(280)
|
|
651
|
+
.optional()
|
|
652
|
+
.describe('Optional one-line summary of this deletion (e.g., "Removed the unused Pexels connection").'),
|
|
405
653
|
},
|
|
406
654
|
annotations: destructive({ title: "Delete Connection", openWorld: true }),
|
|
407
|
-
}, async ({ id, confirm }) => {
|
|
655
|
+
}, async ({ id, confirm, summary }) => {
|
|
408
656
|
if (!confirm) {
|
|
409
657
|
// PHP fromControllerResponse(_, 'connection') wraps as
|
|
410
658
|
// {success, connection:{…}}. Unwrap defensively. Audit: F-A1-04.
|
|
411
659
|
const preview = await request(`/connections/${encodeURIComponent(id)}`);
|
|
412
|
-
const
|
|
660
|
+
const rawConn = preview.success ? unwrapEntity(preview.data, "connection") : undefined;
|
|
661
|
+
// F9 (2026-06-09): same wire-shape bridge as connection_get — the
|
|
662
|
+
// preview target read auth_type off the raw camelCase wire shape.
|
|
663
|
+
const conn = rawConn
|
|
664
|
+
? normalizeConnectionFromWire(rawConn)
|
|
665
|
+
: undefined;
|
|
413
666
|
return formatResult({
|
|
414
667
|
preview: true,
|
|
415
668
|
warning: "DESTRUCTIVE — Connection delete cannot be undone. Dependent flows will break.",
|
|
@@ -425,14 +678,17 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
425
678
|
instruction: "Ask user to confirm, then call again with confirm: true.",
|
|
426
679
|
});
|
|
427
680
|
}
|
|
428
|
-
|
|
681
|
+
// Phase 2.7 — implicit action (`connection.deleted`); attach `change:
|
|
682
|
+
// { summary }` only when supplied so the request is unchanged otherwise.
|
|
683
|
+
const r = await request(`/connections/${encodeURIComponent(id)}`, summary !== undefined
|
|
684
|
+
? {
|
|
685
|
+
method: "DELETE",
|
|
686
|
+
body: JSON.stringify({ change: { summary } }),
|
|
687
|
+
headers: { "Content-Type": "application/json" },
|
|
688
|
+
}
|
|
689
|
+
: { method: "DELETE" });
|
|
429
690
|
if (!r.success) {
|
|
430
|
-
return
|
|
431
|
-
message: r.error ?? "connection delete failed",
|
|
432
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
433
|
-
suggestion: hintFor(r.errorCode),
|
|
434
|
-
details: { id },
|
|
435
|
-
});
|
|
691
|
+
return restErrorResult(r, { id }, { message: "connection delete failed" });
|
|
436
692
|
}
|
|
437
693
|
return formatResult({ deleted: true, id }, false, { maxChars: 1500 });
|
|
438
694
|
});
|
|
@@ -453,13 +709,13 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
453
709
|
// probe fields still live under `data.data.*`. Audit: F-A1-05.
|
|
454
710
|
const r = await request("/connections/test", { method: "POST", body: JSON.stringify({ connection_id: id }) }, { unwrapInnerSuccess: true });
|
|
455
711
|
if (!r.success) {
|
|
456
|
-
return
|
|
457
|
-
message:
|
|
458
|
-
|
|
712
|
+
return restErrorResult(r, { id, payloadFailed: r.payloadFailed }, {
|
|
713
|
+
message: "connection test failed",
|
|
714
|
+
// Per-site nuance: a 200 + success:false (payloadFailed) probe gets
|
|
715
|
+
// an upstream-status hint; everything else falls back to hintFor().
|
|
459
716
|
suggestion: r.payloadFailed
|
|
460
717
|
? "Probe ran but reported failure — check the upstream API status, credentials, or connection endpoint."
|
|
461
718
|
: hintFor(r.errorCode),
|
|
462
|
-
details: { id, payloadFailed: r.payloadFailed },
|
|
463
719
|
});
|
|
464
720
|
}
|
|
465
721
|
// Defensive unwrap: prefer `r.data.data` if present, fall back to `r.data` itself
|
|
@@ -494,12 +750,7 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
494
750
|
body: JSON.stringify({ connection_ids }),
|
|
495
751
|
});
|
|
496
752
|
if (!r.success) {
|
|
497
|
-
return
|
|
498
|
-
message: r.error ?? "connection health check failed",
|
|
499
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
500
|
-
suggestion: hintFor(r.errorCode),
|
|
501
|
-
details: { count: connection_ids.length },
|
|
502
|
-
});
|
|
753
|
+
return restErrorResult(r, { count: connection_ids.length }, { message: "connection health check failed" });
|
|
503
754
|
}
|
|
504
755
|
const data = (r.data && typeof r.data === "object") ? r.data : {};
|
|
505
756
|
// PHP returns map keyed by connection id, NOT under a 'results' key —
|
|
@@ -533,6 +784,11 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
533
784
|
"the response items in-memory — this does NOT trigger multi-page fetch upstream (the " +
|
|
534
785
|
"request is single-shot). Use `fields` to project each item down to a whitelist of leaf " +
|
|
535
786
|
"keys, cutting response size on sparse queries." +
|
|
787
|
+
"\n\nF144: if a library/template connection returns a 'Please configure …' / 'Please select an " +
|
|
788
|
+
"endpoint' diagnostic, the missing values are TEMPLATE FIELDS — supply them here via " +
|
|
789
|
+
"`template_fields` (e.g., { spreadsheet_id: '…' }) rather than re-creating the connection or " +
|
|
790
|
+
"starting a new OAuth flow. A connection whose template_fields are already resolved IS " +
|
|
791
|
+
"configured; inspect them with apimapper_connection_get({ id })." +
|
|
536
792
|
"\n\nExample:\n" +
|
|
537
793
|
" apimapper_connection_data({ id: 'con_abc123', limit: 10 }) // first 10 items\n" +
|
|
538
794
|
" apimapper_connection_data({ id: 'con_abc123', limit: 5, offset: 10 }) // skip 10, take 5\n" +
|
|
@@ -545,7 +801,11 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
545
801
|
.trim()
|
|
546
802
|
.min(1)
|
|
547
803
|
.optional()
|
|
548
|
-
.describe('
|
|
804
|
+
.describe('Named endpoint SELECTION — NOT a URL. This is the label of a configured endpoint on the ' +
|
|
805
|
+
'connection (e.g., "Scheduled Events", "Get Values"), the same names connection_create exposes ' +
|
|
806
|
+
'as endpoints[]. List them via apimapper_connection_get({ id }). Empty-string and ' +
|
|
807
|
+
'whitespace-only values are rejected at the schema boundary to prevent "?endpoint=" query ' +
|
|
808
|
+
"garbage; omit the field entirely to use the connection default."),
|
|
549
809
|
template_fields: z
|
|
550
810
|
.record(z.string(), z.union([z.string(), z.number().finite(), z.boolean()]))
|
|
551
811
|
.optional()
|
|
@@ -587,19 +847,24 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
587
847
|
// implicitly, but the explicit guard documents the contract and keeps
|
|
588
848
|
// TypeScript happy under the wider schema.
|
|
589
849
|
if (template_fields) {
|
|
590
|
-
|
|
850
|
+
// A5 (2026-06-10): normalize a pasted Drive/Sheets URL in
|
|
851
|
+
// template_fields.spreadsheet_id to the bare ID before it becomes a
|
|
852
|
+
// query param — the {{spreadsheet_id}} placeholder needs the bare id.
|
|
853
|
+
const normalizedFields = normalizeResourceIdFields(template_fields);
|
|
854
|
+
for (const [k, v] of Object.entries(normalizedFields)) {
|
|
591
855
|
params.set(k, typeof v === "string" ? v : String(v));
|
|
592
856
|
}
|
|
593
857
|
}
|
|
594
858
|
const qs = params.toString();
|
|
595
|
-
|
|
859
|
+
// S-MED-1 (Wave-B 2026-06-03): sanitize the data read. The {…,connection}
|
|
860
|
+
// envelope echoes the connection config (incl. `headers`/`params` arrays
|
|
861
|
+
// that can hold an inline Authorization / X-API-Key value); {sanitize:true}
|
|
862
|
+
// scrubs it — and {name,value} header pairs — before the payload crosses
|
|
863
|
+
// the MCP boundary. The data rows themselves are customer data and are not
|
|
864
|
+
// mutated by the secret-only sanitizer.
|
|
865
|
+
const r = await request(`/connections/${encodeURIComponent(id)}/data${qs ? `?${qs}` : ""}`, {}, { sanitize: true });
|
|
596
866
|
if (!r.success) {
|
|
597
|
-
return
|
|
598
|
-
message: r.error ?? "connection data fetch failed",
|
|
599
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
600
|
-
suggestion: hintFor(r.errorCode),
|
|
601
|
-
details: { id, endpoint, template_fields, limit, offset },
|
|
602
|
-
});
|
|
867
|
+
return restErrorResult(r, { id, endpoint, template_fields, limit, offset }, { message: "connection data fetch failed" });
|
|
603
868
|
}
|
|
604
869
|
// PHP success body: {data:[…items…], connection, body?, content_type?, status?}.
|
|
605
870
|
// Operate on the inner `data` payload, not on the envelope. Surface
|
|
@@ -608,6 +873,22 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
608
873
|
const innerRaw = "data" in envelope ? envelope.data : envelope;
|
|
609
874
|
const body = typeof envelope.body === "string" ? envelope.body : undefined;
|
|
610
875
|
const contentType = typeof envelope.content_type === "string" ? envelope.content_type : undefined;
|
|
876
|
+
// Silent-zero UX gap (2026-06, PR #732 follow-through) — on a 0-row
|
|
877
|
+
// read the PHP /connections/{id}/data handler attaches a structured
|
|
878
|
+
// `diagnostic` ({reason, message}) explaining WHY nothing showed
|
|
879
|
+
// (unresolved endpoint placeholders, items_path mismatch, or a genuine
|
|
880
|
+
// empty result). Forward it additively into the tool output so the AI
|
|
881
|
+
// agent sees the explanation instead of a silent {item_count:0,
|
|
882
|
+
// payload:[]}. Present only when the upstream supplied it (i.e. on
|
|
883
|
+
// empty results), so the non-empty envelope shape is unchanged. We
|
|
884
|
+
// keep the value verbatim — the PHP EmptyResultDiagnostic::classify
|
|
885
|
+
// owns the {reason, message} contract; the TS layer is a pure
|
|
886
|
+
// passthrough and must not reshape it.
|
|
887
|
+
const diagnostic = envelope.diagnostic !== null &&
|
|
888
|
+
typeof envelope.diagnostic === "object" &&
|
|
889
|
+
!Array.isArray(envelope.diagnostic)
|
|
890
|
+
? envelope.diagnostic
|
|
891
|
+
: undefined;
|
|
611
892
|
// rc.10.1 C + rc.13 W1.2 (F-01+F-38) — apply in-memory trim via
|
|
612
893
|
// extracted pure function. See the `applyTrim` JSDoc in
|
|
613
894
|
// connections-trim.ts for the shape-detection contract and
|
|
@@ -633,6 +914,25 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
633
914
|
? pickFields(item, fields)
|
|
634
915
|
: item);
|
|
635
916
|
}
|
|
917
|
+
// F198 (Desktop-E2E #2 forensics, 2026-06-12): a 0-row read used to ship
|
|
918
|
+
// the full {ok, shape, item_count, payload:[], body, content_type}
|
|
919
|
+
// envelope — pure overhead when there is nothing to show. Collapse it to
|
|
920
|
+
// a compact 1-liner that names the likely culprits (template_fields /
|
|
921
|
+
// range) and forwards the upstream diagnostic verbatim when present.
|
|
922
|
+
// Only the array-empty case collapses; an empty object/scalar payload is
|
|
923
|
+
// a different (rare) shape and keeps the full envelope.
|
|
924
|
+
if (Array.isArray(trim.payload) && trim.itemCount === 0) {
|
|
925
|
+
const diagnosticMsg = diagnostic && typeof diagnostic.message === "string"
|
|
926
|
+
? ` ${diagnostic.message}`
|
|
927
|
+
: "";
|
|
928
|
+
return formatResult({
|
|
929
|
+
ok: true,
|
|
930
|
+
item_count: 0,
|
|
931
|
+
note: "0 rows — check template_fields (e.g. spreadsheet_id / range) or the connection " +
|
|
932
|
+
"endpoint; sample the config with apimapper_connection_get({ id })." +
|
|
933
|
+
diagnosticMsg,
|
|
934
|
+
}, false, { maxChars: 600 });
|
|
935
|
+
}
|
|
636
936
|
return formatResult({
|
|
637
937
|
ok: true,
|
|
638
938
|
// F-38 — surface a stable, narrow shape enum instead of typeof.
|
|
@@ -646,6 +946,11 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
646
946
|
// W3.3 — echo the requested whitelist so the AI knows the
|
|
647
947
|
// per-item shape was (or was meant to be) reduced.
|
|
648
948
|
...(fields ? { projected_fields: fields } : {}),
|
|
949
|
+
// Silent-zero UX gap (2026-06) — forward the upstream
|
|
950
|
+
// EmptyResultDiagnostic so a 0-row read explains WHY. Spread
|
|
951
|
+
// additively: absent on the happy path, so existing non-empty
|
|
952
|
+
// wire-shape pins are unchanged.
|
|
953
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
649
954
|
body,
|
|
650
955
|
content_type: contentType,
|
|
651
956
|
payload: projectedPayload,
|
|
@@ -656,12 +961,14 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
656
961
|
title: "List Connection Resources",
|
|
657
962
|
description: "List browseable resources on a connection (drive files, sheets, IG media). " +
|
|
658
963
|
"REST contract: `field` (resource-picker field name) + optional `query` filter." +
|
|
659
|
-
|
|
964
|
+
// F213: gateway-only tool — show the call routed THROUGH the read
|
|
965
|
+
// gateway so the first live invocation doesn't fail invalid_arguments.
|
|
966
|
+
"\n\nExample (gateway-routed):\n apimapper_advanced_read({ tool: \"apimapper_connection_resources\", arguments: { id: \"con_drive_xyz\", field: \"fileId\", query: \"budget\" } })",
|
|
660
967
|
inputSchema: {
|
|
661
968
|
id: z.string().describe("Connection ID. Use apimapper_connection_list to find."),
|
|
662
969
|
field: z
|
|
663
970
|
.string()
|
|
664
|
-
.describe('
|
|
971
|
+
.describe('REQUIRED — resource-picker field name (e.g., "spreadsheet_id", "drive_file_id"). REST wire-key: field.'),
|
|
665
972
|
// rc.10 A4 (2026-05-19): min(3) blocks the single-char "p", "j", "u"
|
|
666
973
|
// probing pattern observed in Maria-walkthrough logs.
|
|
667
974
|
// rc.13 W1.17 (F-29): Master-Doc-Drift — Master requested min(2), kept
|
|
@@ -678,12 +985,7 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
678
985
|
params.set("query", query);
|
|
679
986
|
const r = await request(`/connections/${encodeURIComponent(id)}/resources?${params.toString()}`);
|
|
680
987
|
if (!r.success) {
|
|
681
|
-
return
|
|
682
|
-
message: r.error ?? "connection resources failed",
|
|
683
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
684
|
-
suggestion: hintFor(r.errorCode),
|
|
685
|
-
details: { id, field, query },
|
|
686
|
-
});
|
|
988
|
+
return restErrorResult(r, { id, field, query }, { message: "connection resources failed" });
|
|
687
989
|
}
|
|
688
990
|
// A1-P3-1: pure helper handles the three wire-shapes (bare array,
|
|
689
991
|
// `{ resources }`, `{ items }`) + fallback to `[]` for unknown
|
|
@@ -711,32 +1013,52 @@ export function registerConnectionTools(server, elicitation) {
|
|
|
711
1013
|
// ── apimapper_connection_pipeline_update ───────────────────────────
|
|
712
1014
|
server.registerTool("apimapper_connection_pipeline_update", {
|
|
713
1015
|
title: "Update Connection Pipeline",
|
|
714
|
-
description: "Update the connection's pipeline (endpoints, default_params, headers, items_path, items_shape)." +
|
|
715
|
-
"\n\
|
|
1016
|
+
description: "Update the connection's pipeline (endpoints, default_params, default_headers, headers, items_path, items_shape, body, body_type, graphql_variables, response_type)." +
|
|
1017
|
+
"\n\nWorks on connections WITHOUT a stored pipeline: the protocol keys version/enabled/steps are NOT required — send only the connection-column keys you care about (F184)." +
|
|
1018
|
+
"\n\nPARTIAL UPDATES ARE SAFE (F145) — send only the keys you want to change:" +
|
|
1019
|
+
"\n • Object-map keys (default_params, default_headers, headers, graphql_variables) MERGE into the stored map: changing one entry preserves the others. Pass a value of null to delete a single stored entry (e.g. { default_params: { stale_key: null } })." +
|
|
1020
|
+
"\n • Scalar keys (items_path, items_shape, body, body_type, response_type) replace their single value." +
|
|
1021
|
+
"\n • List keys (endpoints) are REPLACED wholesale (positional merge is ambiguous) — resend every endpoint you want to keep. If a replace drops endpoints, the response carries a `warnings` array naming what was removed." +
|
|
1022
|
+
"\n\nitems_shape values: 'flat' (default, list of objects), 'auto' (best-effort detection), 'headers_rows' (pivot 2D array [[h1,h2],[v1,v2],...] into named-object rows — use this for Google-Sheets-style responses), 'object_keys' (turn a keyed object into a list)." +
|
|
1023
|
+
"\n\nExample (partial — preserves any other stored params):\n apimapper_connection_pipeline_update({ id: 'con_abc123', pipeline: { items_path: 'photos', items_shape: 'auto', default_params: { per_page: '20' } } })" +
|
|
1024
|
+
"\n\nGoogle-Sheets 2D array example:\n apimapper_connection_pipeline_update({ id: 'con_sheet', pipeline: { items_shape: 'headers_rows' } })",
|
|
716
1025
|
inputSchema: {
|
|
717
1026
|
id: z.string().describe("Connection ID. Use apimapper_connection_list to find."),
|
|
718
1027
|
pipeline: z
|
|
719
1028
|
.record(z.string(), z.unknown())
|
|
720
|
-
.describe('Pipeline JSON (
|
|
1029
|
+
.describe('Pipeline JSON (partial allowed). Accepts connection-column keys: endpoints, default_params, default_headers, headers, items_path, items_shape, body, body_type, graphql_variables, response_type. ' +
|
|
1030
|
+
'Object-map keys (default_params/default_headers/headers/graphql_variables) MERGE into stored values (null deletes one entry); scalar keys replace; list keys (endpoints) replace wholesale and report dropped entries in a warnings array. ' +
|
|
1031
|
+
'items_shape options: "flat" | "auto" | "headers_rows" | "object_keys". ' +
|
|
1032
|
+
'Example: {"items_path":"data","items_shape":"headers_rows"}.'),
|
|
721
1033
|
},
|
|
722
1034
|
annotations: mutating({ title: "Update Connection Pipeline", openWorld: true }),
|
|
723
1035
|
}, async ({ id, pipeline }) => {
|
|
724
1036
|
// PHP ConnectionPipelineHandler uses fromControllerResponse(_, 'connection')
|
|
725
|
-
// → {success, connection:{…}}.
|
|
1037
|
+
// → {success, connection:{…}}. F145: a list-key (endpoints) full-replace
|
|
1038
|
+
// also carries a top-level `warnings` array naming dropped entries.
|
|
1039
|
+
// Unwrap defensively. Audit: F-A1-07.
|
|
726
1040
|
const r = await request(`/connections/${encodeURIComponent(id)}/pipeline`, {
|
|
727
1041
|
method: "PUT",
|
|
728
1042
|
body: JSON.stringify(pipeline),
|
|
729
1043
|
});
|
|
730
1044
|
if (!r.success) {
|
|
731
|
-
return
|
|
732
|
-
message: r.error ?? "connection pipeline update failed",
|
|
733
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
734
|
-
suggestion: hintFor(r.errorCode),
|
|
735
|
-
details: { id },
|
|
736
|
-
});
|
|
1045
|
+
return restErrorResult(r, { id }, { message: "connection pipeline update failed" });
|
|
737
1046
|
}
|
|
738
1047
|
const conn = unwrapEntity(r.data, "connection");
|
|
739
|
-
|
|
1048
|
+
// Surface F145 dropped-entry warnings so the agent never silently loses
|
|
1049
|
+
// endpoints on a partial replace.
|
|
1050
|
+
const warnings = r.data && typeof r.data === "object" && Array.isArray(r.data.warnings)
|
|
1051
|
+
? r.data.warnings
|
|
1052
|
+
: undefined;
|
|
1053
|
+
const payload = {
|
|
1054
|
+
updated: true,
|
|
1055
|
+
id: conn?.id,
|
|
1056
|
+
name: conn?.name,
|
|
1057
|
+
};
|
|
1058
|
+
if (warnings && warnings.length > 0) {
|
|
1059
|
+
payload.warnings = warnings;
|
|
1060
|
+
}
|
|
1061
|
+
return formatResult(payload, false, { maxChars: 2000 });
|
|
740
1062
|
});
|
|
741
1063
|
}
|
|
742
1064
|
//# sourceMappingURL=connections.js.map
|