@wootsup/mcp 0.1.0-rc.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/index.d.ts +19 -0
- package/dist/index.js +72 -6
- package/dist/index.js.map +1 -1
- package/dist/modules/apimapper/cache.d.ts +2 -2
- package/dist/modules/apimapper/cache.js +107 -25
- package/dist/modules/apimapper/cache.js.map +1 -1
- package/dist/modules/apimapper/client.d.ts +40 -0
- package/dist/modules/apimapper/client.js +82 -12
- package/dist/modules/apimapper/client.js.map +1 -1
- package/dist/modules/apimapper/connections-format.d.ts +51 -0
- package/dist/modules/apimapper/connections-format.js +261 -0
- package/dist/modules/apimapper/connections-format.js.map +1 -0
- package/dist/modules/apimapper/connections-trim.d.ts +82 -0
- package/dist/modules/apimapper/connections-trim.js +224 -0
- package/dist/modules/apimapper/connections-trim.js.map +1 -0
- package/dist/modules/apimapper/connections.d.ts +14 -2
- package/dist/modules/apimapper/connections.js +447 -143
- package/dist/modules/apimapper/connections.js.map +1 -1
- package/dist/modules/apimapper/credentials-format.d.ts +21 -0
- package/dist/modules/apimapper/credentials-format.js +145 -0
- package/dist/modules/apimapper/credentials-format.js.map +1 -0
- package/dist/modules/apimapper/credentials.d.ts +12 -2
- package/dist/modules/apimapper/credentials.js +253 -72
- package/dist/modules/apimapper/credentials.js.map +1 -1
- package/dist/modules/apimapper/diagnose.d.ts +54 -2
- package/dist/modules/apimapper/diagnose.js +193 -11
- package/dist/modules/apimapper/diagnose.js.map +1 -1
- package/dist/modules/apimapper/elicitation.d.ts +54 -0
- package/dist/modules/apimapper/elicitation.js +90 -0
- package/dist/modules/apimapper/elicitation.js.map +1 -0
- package/dist/modules/apimapper/flows-format.d.ts +50 -0
- package/dist/modules/apimapper/flows-format.js +318 -0
- package/dist/modules/apimapper/flows-format.js.map +1 -0
- package/dist/modules/apimapper/flows.d.ts +13 -2
- package/dist/modules/apimapper/flows.js +325 -118
- package/dist/modules/apimapper/flows.js.map +1 -1
- package/dist/modules/apimapper/gateway/advanced-tool.d.ts +9 -0
- package/dist/modules/apimapper/gateway/advanced-tool.js +214 -0
- package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -0
- package/dist/modules/apimapper/gateway/capturing-server.d.ts +81 -0
- package/dist/modules/apimapper/gateway/capturing-server.js +87 -0
- package/dist/modules/apimapper/gateway/capturing-server.js.map +1 -0
- package/dist/modules/apimapper/gateway/essentials.d.ts +4 -0
- package/dist/modules/apimapper/gateway/essentials.js +28 -0
- package/dist/modules/apimapper/gateway/essentials.js.map +1 -0
- package/dist/modules/apimapper/gateway/test-support.d.ts +17 -0
- package/dist/modules/apimapper/gateway/test-support.js +43 -0
- package/dist/modules/apimapper/gateway/test-support.js.map +1 -0
- package/dist/modules/apimapper/get-skill.d.ts +3 -3
- package/dist/modules/apimapper/get-skill.js +4 -2
- package/dist/modules/apimapper/get-skill.js.map +1 -1
- package/dist/modules/apimapper/graph-builder.js +1 -1
- package/dist/modules/apimapper/graph-builder.js.map +1 -1
- package/dist/modules/apimapper/graph.d.ts +2 -2
- package/dist/modules/apimapper/graph.js +165 -34
- package/dist/modules/apimapper/graph.js.map +1 -1
- package/dist/modules/apimapper/index.d.ts +17 -1
- package/dist/modules/apimapper/index.js +66 -17
- package/dist/modules/apimapper/index.js.map +1 -1
- package/dist/modules/apimapper/inspect.d.ts +3 -2
- package/dist/modules/apimapper/inspect.js +97 -13
- package/dist/modules/apimapper/inspect.js.map +1 -1
- package/dist/modules/apimapper/library.d.ts +2 -2
- package/dist/modules/apimapper/library.js +303 -60
- package/dist/modules/apimapper/library.js.map +1 -1
- package/dist/modules/apimapper/license-format.d.ts +22 -0
- package/dist/modules/apimapper/license-format.js +149 -0
- package/dist/modules/apimapper/license-format.js.map +1 -0
- package/dist/modules/apimapper/license.d.ts +16 -2
- package/dist/modules/apimapper/license.js +85 -37
- package/dist/modules/apimapper/license.js.map +1 -1
- package/dist/modules/apimapper/local-sources.d.ts +2 -2
- package/dist/modules/apimapper/local-sources.js +58 -30
- package/dist/modules/apimapper/local-sources.js.map +1 -1
- package/dist/modules/apimapper/misc.d.ts +30 -2
- package/dist/modules/apimapper/misc.js +129 -50
- package/dist/modules/apimapper/misc.js.map +1 -1
- package/dist/modules/apimapper/node-schema.d.ts +52 -0
- package/dist/modules/apimapper/node-schema.js +70 -2
- package/dist/modules/apimapper/node-schema.js.map +1 -1
- package/dist/modules/apimapper/normalizers.d.ts +1 -0
- package/dist/modules/apimapper/normalizers.js +51 -0
- package/dist/modules/apimapper/normalizers.js.map +1 -1
- package/dist/modules/apimapper/onboarding.d.ts +48 -2
- package/dist/modules/apimapper/onboarding.js +324 -17
- package/dist/modules/apimapper/onboarding.js.map +1 -1
- package/dist/modules/apimapper/read-cache.d.ts +31 -2
- package/dist/modules/apimapper/read-cache.js +20 -6
- package/dist/modules/apimapper/read-cache.js.map +1 -1
- package/dist/modules/apimapper/render/_shared.d.ts +24 -0
- package/dist/modules/apimapper/render/_shared.js +84 -0
- package/dist/modules/apimapper/render/_shared.js.map +1 -0
- package/dist/modules/apimapper/render/dag.d.ts +18 -0
- package/dist/modules/apimapper/render/dag.js +70 -0
- package/dist/modules/apimapper/render/dag.js.map +1 -0
- package/dist/modules/apimapper/render/index.d.ts +2 -0
- package/dist/modules/apimapper/render/index.js +112 -0
- package/dist/modules/apimapper/render/index.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/chart-bar.d.ts +2 -0
- package/dist/modules/apimapper/render/renderers/chart-bar.js +70 -0
- package/dist/modules/apimapper/render/renderers/chart-bar.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/chart-line.d.ts +2 -0
- package/dist/modules/apimapper/render/renderers/chart-line.js +71 -0
- package/dist/modules/apimapper/render/renderers/chart-line.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/diff.d.ts +2 -0
- package/dist/modules/apimapper/render/renderers/diff.js +154 -0
- package/dist/modules/apimapper/render/renderers/diff.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/flow-diagram.d.ts +1 -0
- package/dist/modules/apimapper/render/renderers/flow-diagram.js +180 -0
- package/dist/modules/apimapper/render/renderers/flow-diagram.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/json-tree.d.ts +2 -0
- package/dist/modules/apimapper/render/renderers/json-tree.js +87 -0
- package/dist/modules/apimapper/render/renderers/json-tree.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/schema-diagram.d.ts +2 -0
- package/dist/modules/apimapper/render/renderers/schema-diagram.js +83 -0
- package/dist/modules/apimapper/render/renderers/schema-diagram.js.map +1 -0
- package/dist/modules/apimapper/render/renderers/table.d.ts +2 -0
- package/dist/modules/apimapper/render/renderers/table.js +75 -0
- package/dist/modules/apimapper/render/renderers/table.js.map +1 -0
- package/dist/modules/apimapper/render/schemas.d.ts +23 -0
- package/dist/modules/apimapper/render/schemas.js +56 -0
- package/dist/modules/apimapper/render/schemas.js.map +1 -0
- package/dist/modules/apimapper/render/secret-masking.d.ts +5 -0
- package/dist/modules/apimapper/render/secret-masking.js +51 -0
- package/dist/modules/apimapper/render/secret-masking.js.map +1 -0
- package/dist/modules/apimapper/render/sidecar.d.ts +21 -0
- package/dist/modules/apimapper/render/sidecar.js +66 -0
- package/dist/modules/apimapper/render/sidecar.js.map +1 -0
- package/dist/modules/apimapper/render/token-cap.d.ts +21 -0
- package/dist/modules/apimapper/render/token-cap.js +57 -0
- package/dist/modules/apimapper/render/token-cap.js.map +1 -0
- package/dist/modules/apimapper/schema.d.ts +2 -2
- package/dist/modules/apimapper/schema.js +100 -32
- package/dist/modules/apimapper/schema.js.map +1 -1
- package/dist/modules/apimapper/settings-format.d.ts +23 -0
- package/dist/modules/apimapper/settings-format.js +135 -0
- package/dist/modules/apimapper/settings-format.js.map +1 -0
- package/dist/modules/apimapper/settings.d.ts +2 -2
- package/dist/modules/apimapper/settings.js +101 -40
- package/dist/modules/apimapper/settings.js.map +1 -1
- package/dist/modules/apimapper/skill-resources.d.ts +2 -2
- package/dist/modules/apimapper/skill-resources.js.map +1 -1
- package/dist/modules/apimapper/token-baseline.harness.d.ts +91 -0
- package/dist/modules/apimapper/token-baseline.harness.js +291 -0
- package/dist/modules/apimapper/token-baseline.harness.js.map +1 -0
- package/dist/modules/apimapper/toolslist-size.d.ts +55 -0
- package/dist/modules/apimapper/toolslist-size.js +190 -0
- package/dist/modules/apimapper/toolslist-size.js.map +1 -0
- package/dist/modules/apimapper/types.d.ts +23 -8
- package/dist/modules/apimapper/types.js +26 -1
- package/dist/modules/apimapper/types.js.map +1 -1
- package/dist/modules/apimapper/use-profile.d.ts +21 -0
- package/dist/modules/apimapper/use-profile.js +56 -2
- package/dist/modules/apimapper/use-profile.js.map +1 -1
- package/dist/modules/apimapper/workflows.d.ts +2 -2
- package/dist/modules/apimapper/workflows.js +143 -16
- package/dist/modules/apimapper/workflows.js.map +1 -1
- package/dist/platform/index.js +44 -5
- package/dist/platform/index.js.map +1 -1
- package/dist/setup-cli.d.ts +53 -0
- package/dist/setup-cli.js +135 -6
- package/dist/setup-cli.js.map +1 -1
- package/docs/architecture.md +1 -1
- package/docs/tools.md +1 -1
- package/manifest.json +12 -3
- package/package.json +9 -4
- package/skills/apimapper/SKILL.md +1 -1
- package/skills/apimapper/reference/render.md +132 -0
- package/skills/apimapper/reference/troubleshooting.md +1 -1
- package/skills/apimapper/reference/yootheme.md +1 -1
|
@@ -1,23 +1,127 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatResult,
|
|
2
|
+
import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, errorWithSuggestion, pickFields, createProgressReporter, elicitChoice, } from "@getimo/mcp-toolkit";
|
|
3
3
|
import { request, hintFor, WP_BASE, WP_USER, authConfigured } from "./client.js";
|
|
4
|
+
import { toRows } from "./types.js";
|
|
4
5
|
import { unwrapEntity } from "./envelope.js";
|
|
5
|
-
|
|
6
|
+
import { ambiguityFallbackError, } from "./elicitation.js";
|
|
7
|
+
import { fetchWithTimeout } from "./diagnose.js";
|
|
8
|
+
import { filterByNameQuery } from "./node-schema.js";
|
|
9
|
+
import { CONNECTION_TABLE_COLUMNS, CONNECTION_COMPACT_COLUMNS, CONNECTION_LIST_NEXT_STEPS, RESOURCE_TABLE_COLUMNS, mapConnectionRow, compactConnectionRow, mapResourceRow, buildConnectionDetail, buildConnectionTestStats, buildHealthCheckStats, buildHealthStats, } from "./connections-format.js";
|
|
10
|
+
// W3 Stage-2 hardening — applyTrim + extractResourceList live in the
|
|
11
|
+
// sibling connections-trim.ts (pure functions, ~200 lines). Imported for
|
|
12
|
+
// internal use by the connection_data + connection_resources handlers,
|
|
13
|
+
// and re-exported so the public surface
|
|
14
|
+
// `import { ... } from "./connections.js"` stays unchanged for existing
|
|
15
|
+
// callers and the test suite.
|
|
16
|
+
import { applyTrim, extractResourceList, } from "./connections-trim.js";
|
|
17
|
+
export { applyTrim, extractResourceList };
|
|
18
|
+
// F-LS-04 (live-smoke 2026-05-20) — the PHP /connections endpoint emits
|
|
19
|
+
// camelCase keys (authType, credentialId, ...). Every formatter + consumer
|
|
20
|
+
// in this module reads snake_case. The wire-shape normaliser bridges
|
|
21
|
+
// the two so the AI no longer sees auth=none / credential=— on real
|
|
22
|
+
// upstream connections. Applied at the connection_list handler — that is
|
|
23
|
+
// the only tool that fans out raw connection items to a downstream
|
|
24
|
+
// formatter; connection_get / _update / _delete pass through unwrapEntity
|
|
25
|
+
// + the rich-card builder which already reads from the same row map.
|
|
26
|
+
import { normalizeConnectionFromWire } from "./normalizers.js";
|
|
27
|
+
/**
|
|
28
|
+
* F-15 (W1.9) — downstream probe of api.wootsup.com from `apimapper_health`.
|
|
29
|
+
*
|
|
30
|
+
* The customer's WP-REST probe alone cannot distinguish "your server is fine
|
|
31
|
+
* but getimo's license/release infra is degraded" from a generic failure.
|
|
32
|
+
* Hitting the upstream `/health` endpoint surfaces that signal so an AI
|
|
33
|
+
* client can route the user to the right troubleshooting branch.
|
|
34
|
+
*/
|
|
35
|
+
const APIMAPPER_API_HEALTH_URL = "https://api.wootsup.com/health";
|
|
36
|
+
/**
|
|
37
|
+
* 3-second budget — health is meant to be a fast snapshot. Shorter than
|
|
38
|
+
* diagnose's 5s because diagnose probes a customer-controlled server (which
|
|
39
|
+
* may legitimately be slow under load), while api.wootsup.com is
|
|
40
|
+
* getimo-controlled CDN-backed infra that should respond in <500ms.
|
|
41
|
+
*/
|
|
42
|
+
const APIMAPPER_API_HEALTH_TIMEOUT_MS = 3_000;
|
|
43
|
+
/**
|
|
44
|
+
* F-15: probe api.wootsup.com/health and map the outcome to a stable
|
|
45
|
+
* status-string. NEVER throws — the health tool's contract is to surface
|
|
46
|
+
* downstream failure as data, not to fail itself when the downstream is
|
|
47
|
+
* unreachable. The four return values form a closed enum:
|
|
48
|
+
*
|
|
49
|
+
* - "OK" — 2xx response from api.wootsup.com
|
|
50
|
+
* - "HTTP N" — non-2xx response (e.g. "HTTP 503")
|
|
51
|
+
* - "timeout" — AbortError/TimeoutError after 3s
|
|
52
|
+
* - "unreachable" — any other fetch error (DNS, refused, TLS, etc.)
|
|
53
|
+
*
|
|
54
|
+
* Reuses fetchWithTimeout from diagnose.ts so the AbortSignal.timeout
|
|
55
|
+
* wiring lives in exactly one place (DRY across probe call sites).
|
|
56
|
+
*/
|
|
57
|
+
async function probeApimapperApi() {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetchWithTimeout(APIMAPPER_API_HEALTH_URL, { method: "GET" }, APIMAPPER_API_HEALTH_TIMEOUT_MS);
|
|
60
|
+
if (res.ok)
|
|
61
|
+
return "OK";
|
|
62
|
+
return `HTTP ${res.status}`;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError")) {
|
|
66
|
+
return "timeout";
|
|
67
|
+
}
|
|
68
|
+
return "unreachable";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Register the connection CRUD + probe + sample + pipeline tools.
|
|
73
|
+
*
|
|
74
|
+
* @param server the tool registrar (essentials forward, rest captured).
|
|
75
|
+
* @param elicitation optional elicitation capability (the real McpServer, or
|
|
76
|
+
* any `{ server: { elicitInput } }`). Supplied only by the host wiring in
|
|
77
|
+
* index.ts. When omitted, `connection_create` falls back to a structured
|
|
78
|
+
* error on genuine credential ambiguity instead of prompting.
|
|
79
|
+
*/
|
|
80
|
+
export function registerConnectionTools(server, elicitation) {
|
|
6
81
|
// ── apimapper_health ───────────────────────────────────────────────
|
|
7
82
|
server.registerTool("apimapper_health", {
|
|
8
83
|
title: "API Mapper REST Health",
|
|
9
84
|
description: "Check connectivity + auth against the API Mapper REST namespace. " +
|
|
10
|
-
"Returns {wp_base, wp_user, auth, connectivity, connection_count, http_status}. " +
|
|
85
|
+
"Returns {wp_base, wp_user, auth, connectivity, connection_count, http_status, apimapper_api_status}. " +
|
|
86
|
+
"`apimapper_api_status` reports the downstream api.wootsup.com health (OK/HTTP N/timeout/unreachable) " +
|
|
87
|
+
"so a degradation of getimo infra is distinguishable from a customer-server issue. " +
|
|
11
88
|
"Run this first if any other tool fails." +
|
|
12
89
|
"\n\nExample:\n apimapper_health({})",
|
|
13
90
|
inputSchema: {},
|
|
14
|
-
annotations: readOnly(),
|
|
91
|
+
annotations: readOnly({ title: "Health Check", openWorld: true }),
|
|
15
92
|
}, async () => {
|
|
16
93
|
const checks = {
|
|
17
94
|
wp_base: WP_BASE,
|
|
18
95
|
wp_user: WP_USER,
|
|
19
96
|
auth: authConfigured() ? "configured" : "MISSING (set APIMAPPER_WP_APP_PASS)",
|
|
20
97
|
};
|
|
98
|
+
// L-5 (W3F-5): when the customer uses bearer-only auth (the modern
|
|
99
|
+
// `amk_live_…` flow), APIMAPPER_WP_USER is intentionally unset, so
|
|
100
|
+
// the previous render emitted `WP user: ""`. Probe `/identity` and
|
|
101
|
+
// fall back to its `.username` field — it's anon-readable on both
|
|
102
|
+
// platforms and never throws. The probe is cheap (single GET) and
|
|
103
|
+
// runs alongside the existing api.wootsup.com probe.
|
|
104
|
+
if (checks.wp_user === "") {
|
|
105
|
+
try {
|
|
106
|
+
const idRes = await request("/identity");
|
|
107
|
+
const fromIdentity = idRes.success && typeof idRes.data?.username === "string"
|
|
108
|
+
? idRes.data.username
|
|
109
|
+
: "";
|
|
110
|
+
if (fromIdentity !== "") {
|
|
111
|
+
checks.wp_user = fromIdentity;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Identity probe failure is non-fatal — the rest of the
|
|
116
|
+
// health snapshot still surfaces.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// F-15 (W1.9) — downstream api.wootsup.com probe. Runs before the
|
|
120
|
+
// WP-REST check; both are independent, but sequential is cheap (3s
|
|
121
|
+
// worst-case for the api probe, then the WP probe) and keeps the
|
|
122
|
+
// result ordering deterministic. Probe never throws — surfaces as
|
|
123
|
+
// status string per probeApimapperApi contract.
|
|
124
|
+
checks.apimapper_api_status = await probeApimapperApi();
|
|
21
125
|
try {
|
|
22
126
|
const r = await request("/connections");
|
|
23
127
|
checks.http_status = r.status ?? 0;
|
|
@@ -35,60 +139,74 @@ export function registerConnectionTools(server) {
|
|
|
35
139
|
const err = errorWithSuggestion(e, { healthTool: "apimapper_health" });
|
|
36
140
|
checks.connectivity = `ERROR: ${err.message}`;
|
|
37
141
|
}
|
|
38
|
-
|
|
142
|
+
// W3.1 — health snapshot is a flat scalar set: statsResult is the
|
|
143
|
+
// goldstandard fit. Every check becomes a stat so an AI client reads
|
|
144
|
+
// connectivity/auth/downstream state without parsing free text.
|
|
145
|
+
return buildHealthStats(checks);
|
|
39
146
|
});
|
|
40
147
|
// ── apimapper_connection_list ──────────────────────────────────────
|
|
41
148
|
server.registerTool("apimapper_connection_list", {
|
|
42
149
|
title: "List Connections",
|
|
43
|
-
|
|
44
|
-
|
|
150
|
+
// rc.10 A5 (2026-05-19) — description states the default limit
|
|
151
|
+
// already covers a typical install. The Maria-walkthrough log
|
|
152
|
+
// showed AI defaulting to limit:200/500 reflexively.
|
|
153
|
+
description: "List all API Mapper connections. The default limit of 50 already covers a typical install — " +
|
|
154
|
+
"most customers run with 5-25 connections, increase the limit only if you have a specific " +
|
|
155
|
+
"reason to expect more. Use apimapper_connection_get for full details of a single connection." +
|
|
156
|
+
"\n\nExample:\n apimapper_connection_list({}) // returns up to 50 connections in one call\n" +
|
|
157
|
+
" apimapper_connection_list({ source: 'user' }) // narrow to user-created only",
|
|
45
158
|
inputSchema: {
|
|
46
159
|
source: z
|
|
47
160
|
.enum(["library", "demo", "user", "all"])
|
|
48
161
|
.default("all")
|
|
49
162
|
.describe("Filter by origin"),
|
|
50
|
-
limit: z.number().min(1).max(500).default(50).describe("Max items (1-500)"),
|
|
163
|
+
limit: z.number().min(1).max(500).default(50).describe("Max items (1-500). Default 50 already covers a typical install — do not raise unless you have evidence the list is larger."),
|
|
164
|
+
// W1.18 (F-32) — case-insensitive substring filter applied
|
|
165
|
+
// in-memory AFTER the upstream fetch (no per-name REST call).
|
|
166
|
+
// Min 2 chars at the schema boundary blocks the single-char
|
|
167
|
+
// probe-thrash pattern observed in the Maria-walkthrough logs.
|
|
168
|
+
name_query: z
|
|
169
|
+
.string()
|
|
170
|
+
.min(2)
|
|
171
|
+
.optional()
|
|
172
|
+
.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."),
|
|
51
173
|
},
|
|
52
|
-
annotations: readOnly(),
|
|
53
|
-
}, async ({ source, limit }) => {
|
|
174
|
+
annotations: readOnly({ title: "List Connections", openWorld: true }),
|
|
175
|
+
}, async ({ source, limit, name_query }) => {
|
|
54
176
|
const r = await request("/connections");
|
|
55
177
|
if (!r.success) {
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}, true);
|
|
178
|
+
return errorResult({
|
|
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
|
+
});
|
|
63
184
|
}
|
|
64
|
-
|
|
185
|
+
// F-LS-04 — normalise the camelCase wire shape (authType, credentialId,
|
|
186
|
+
// …) to snake_case BEFORE anything downstream reads it. The
|
|
187
|
+
// `Connection` domain type uses snake_case keys, so the result is
|
|
188
|
+
// safely cast back: `normalizeConnectionFromWire` only adds keys, it
|
|
189
|
+
// never drops them, and existing snake_case values win on collision.
|
|
190
|
+
const rawItems = Array.isArray(r.data?.connections) ? r.data.connections : [];
|
|
191
|
+
let items = rawItems.map((c) => normalizeConnectionFromWire(c));
|
|
65
192
|
if (source !== "all")
|
|
66
193
|
items = items.filter((c) => c.source === source);
|
|
194
|
+
// W1.18 (F-32) — filter BEFORE slice so limit applies to the matched
|
|
195
|
+
// subset, not the haystack. See `filterByNameQuery` JSDoc for the
|
|
196
|
+
// case-folding + undefined-skip contract shared with flow_list.
|
|
197
|
+
items = filterByNameQuery(items, name_query);
|
|
67
198
|
items = items.slice(0, limit);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
columns: [
|
|
78
|
-
// Width 36 accommodates 30+ char IDs (e.g. legacy template
|
|
79
|
-
// connections like conn_calendly_jla_229fcbce95b7). Truncation
|
|
80
|
-
// produces orphan IDs that downstream tools fail to resolve
|
|
81
|
-
// via *_get; +6 headroom matches the longest historical IDs.
|
|
82
|
-
{ key: "id", label: "ID", width: 36 },
|
|
83
|
-
{ key: "name", label: "NAME", width: 28 },
|
|
84
|
-
{ key: "source", label: "SRC", width: 8 },
|
|
85
|
-
{ key: "endpoint", label: "ENDPOINT", width: 30 },
|
|
86
|
-
{ key: "method", label: "M", width: 5 },
|
|
87
|
-
{ key: "auth_type", label: "AUTH", width: 12 },
|
|
88
|
-
{ key: "credential_id", label: "CRED", width: 26 },
|
|
89
|
-
],
|
|
199
|
+
// W3.1 — tableResult: ASCII table for the LLM + typed DataTable payload
|
|
200
|
+
// for the Rich Card. T1 (W3.2): explicit compactColumns/compactMap drop
|
|
201
|
+
// ENDPOINT/METHOD/CRED at 21+ rows. IA-7: id stays llmOnly. IA-10: the
|
|
202
|
+
// footer carries the next-step guidance.
|
|
203
|
+
return tableResult(toRows(items), {
|
|
204
|
+
columns: CONNECTION_TABLE_COLUMNS,
|
|
205
|
+
compactColumns: CONNECTION_COMPACT_COLUMNS,
|
|
206
|
+
map: mapConnectionRow,
|
|
207
|
+
compactMap: compactConnectionRow,
|
|
90
208
|
header: (n) => `${n} connections`,
|
|
91
|
-
footer:
|
|
209
|
+
footer: CONNECTION_LIST_NEXT_STEPS,
|
|
92
210
|
});
|
|
93
211
|
});
|
|
94
212
|
// ── apimapper_connection_get ───────────────────────────────────────
|
|
@@ -99,20 +217,33 @@ export function registerConnectionTools(server) {
|
|
|
99
217
|
inputSchema: {
|
|
100
218
|
id: z.string().describe('Connection ID (e.g., "conn_Mz33OVPF1z3ap8fbbQtpx"). Use apimapper_connection_list.'),
|
|
101
219
|
},
|
|
102
|
-
annotations: readOnly(),
|
|
220
|
+
annotations: readOnly({ title: "Get Connection", openWorld: true }),
|
|
103
221
|
}, async ({ id }) => {
|
|
104
222
|
// PHP wraps via fromControllerResponse($response, 'connection') →
|
|
105
223
|
// `{success:true, connection:{…}}`. Unwrap defensively so a future
|
|
106
224
|
// flatten on the PHP side doesn't break us. Audit: F-A1-01.
|
|
107
225
|
const r = await request(`/connections/${encodeURIComponent(id)}`);
|
|
108
226
|
if (!r.success) {
|
|
109
|
-
return
|
|
227
|
+
return errorResult({
|
|
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
|
+
});
|
|
110
233
|
}
|
|
111
234
|
const conn = unwrapEntity(r.data, "connection");
|
|
112
235
|
if (!conn || Object.keys(conn).length === 0) {
|
|
113
|
-
return
|
|
236
|
+
return errorResult({
|
|
237
|
+
message: "connection not found",
|
|
238
|
+
code: r.status ? String(r.status) : "not_found",
|
|
239
|
+
suggestion: hintFor("not_found"),
|
|
240
|
+
details: { id },
|
|
241
|
+
});
|
|
114
242
|
}
|
|
115
|
-
|
|
243
|
+
// W3.1 — detailResult: grouped key-value detail for the Rich Card.
|
|
244
|
+
// IA-7: opaque IDs are copyable code entries. IA-10: a dedicated
|
|
245
|
+
// "Next steps" group carries the follow-up calls.
|
|
246
|
+
return buildConnectionDetail(id, conn);
|
|
116
247
|
});
|
|
117
248
|
// ── apimapper_connection_create ────────────────────────────────────
|
|
118
249
|
server.registerTool("apimapper_connection_create", {
|
|
@@ -129,29 +260,92 @@ export function registerConnectionTools(server) {
|
|
|
129
260
|
cache_ttl: z.number().int().min(0).default(3600).describe("Cache TTL in seconds (snake_case wire key)"),
|
|
130
261
|
description: z.string().optional().describe("Free-text description"),
|
|
131
262
|
},
|
|
132
|
-
annotations: creating(),
|
|
263
|
+
annotations: creating({ title: "Create Connection", openWorld: true }),
|
|
133
264
|
}, async (input) => {
|
|
265
|
+
// W3.6 — when the connection's auth_type needs a credential and none is
|
|
266
|
+
// given: exactly 1 stored credential → auto-pick; >1 → elicitChoice;
|
|
267
|
+
// elicitChoice null (unsupported client / declined) → structured
|
|
268
|
+
// candidate-list error. auth_type "none" never needs a credential.
|
|
269
|
+
let resolvedInput = { ...input };
|
|
270
|
+
if (input.auth_type !== "none" && !input.credential_id) {
|
|
271
|
+
const lr = await request("/credentials", {}, { sanitize: true });
|
|
272
|
+
if (!lr.success) {
|
|
273
|
+
return errorResult({
|
|
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
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const creds = Array.isArray(lr.data?.credentials) ? lr.data.credentials : [];
|
|
281
|
+
if (creds.length === 0) {
|
|
282
|
+
return errorResult({
|
|
283
|
+
message: `auth_type "${input.auth_type}" needs a credential, but none are stored.`,
|
|
284
|
+
code: "credential_not_found",
|
|
285
|
+
suggestion: "Create one with apimapper_credential_create, then retry with its credential_id.",
|
|
286
|
+
details: { name: input.name, auth_type: input.auth_type },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (creds.length === 1) {
|
|
290
|
+
resolvedInput = { ...input, credential_id: creds[0].id };
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
const candidates = creds.map((c) => ({
|
|
294
|
+
id: c.id,
|
|
295
|
+
label: c.name,
|
|
296
|
+
}));
|
|
297
|
+
const picked = elicitation
|
|
298
|
+
? await elicitChoice(elicitation, `The "${input.name}" connection (auth_type: ${input.auth_type}) needs a ` +
|
|
299
|
+
"credential. Pick which stored credential to use.", candidates.map((c) => c.id))
|
|
300
|
+
: null;
|
|
301
|
+
if (picked === null) {
|
|
302
|
+
return ambiguityFallbackError({
|
|
303
|
+
code: "credential_ambiguous",
|
|
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 };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
134
313
|
// PHP fromControllerResponse(_, 'connection', 201) → {success, connection:{…}}.
|
|
135
314
|
// Audit: F-A1-02.
|
|
136
315
|
const r = await request("/connections", {
|
|
137
316
|
method: "POST",
|
|
138
|
-
body: JSON.stringify(
|
|
317
|
+
body: JSON.stringify(resolvedInput),
|
|
139
318
|
});
|
|
140
319
|
if (!r.success) {
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}, true);
|
|
320
|
+
return errorResult({
|
|
321
|
+
message: r.error ?? "connection create failed",
|
|
322
|
+
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
323
|
+
suggestion: hintFor(r.errorCode),
|
|
324
|
+
details: { name: input.name, endpoint: input.endpoint },
|
|
325
|
+
});
|
|
148
326
|
}
|
|
149
327
|
const conn = unwrapEntity(r.data, "connection");
|
|
328
|
+
// F-3.2 (Pass-1 consolidation) — mutating-tool payload-shape rationale.
|
|
329
|
+
//
|
|
330
|
+
// connection_create + connection_update stay on the flat
|
|
331
|
+
// `formatResult({ ... })` shape rather than migrating to the
|
|
332
|
+
// structured detailResult / actionResult builders. The mutating-tool
|
|
333
|
+
// payload here is intentionally minimal (the wire echo confirms the
|
|
334
|
+
// op + identity), and the detail builders are tuned for fully
|
|
335
|
+
// populated entities (badge/group/entry layouts) — wrapping a 3-field
|
|
336
|
+
// confirmation in a Rich Card would add noise without surfacing more
|
|
337
|
+
// information. This mirrors the connection_data passthrough rationale:
|
|
338
|
+
// the LLM benefits from the bare structured echo; UI hosts that want
|
|
339
|
+
// a richer view re-fetch via connection_get. IA-10 uniformity is
|
|
340
|
+
// restored via `next_steps[]` (array, single-element) below.
|
|
150
341
|
return formatResult({
|
|
151
342
|
created: true,
|
|
152
343
|
id: conn?.id,
|
|
153
344
|
name: conn?.name,
|
|
154
345
|
source: conn?.source,
|
|
346
|
+
next_steps: [
|
|
347
|
+
"Use apimapper_connection_test to verify reachability, then apimapper_flow_create to wire it into a flow.",
|
|
348
|
+
],
|
|
155
349
|
}, false, { maxChars: 2000 });
|
|
156
350
|
});
|
|
157
351
|
// ── apimapper_connection_update ────────────────────────────────────
|
|
@@ -167,7 +361,7 @@ export function registerConnectionTools(server) {
|
|
|
167
361
|
'Examples: {"name":"Renamed"}, {"cache_ttl":7200}, {"endpoint":"/v2/posts"}, ' +
|
|
168
362
|
'{"credential_id":"cred_xxx"}'),
|
|
169
363
|
},
|
|
170
|
-
annotations: mutating(),
|
|
364
|
+
annotations: mutating({ title: "Update Connection", openWorld: true }),
|
|
171
365
|
}, async ({ id, patch }) => {
|
|
172
366
|
// PHP fromControllerResponse(_, 'connection') → {success, connection:{…}}.
|
|
173
367
|
// Audit: F-A1-03.
|
|
@@ -176,10 +370,25 @@ export function registerConnectionTools(server) {
|
|
|
176
370
|
body: JSON.stringify(patch),
|
|
177
371
|
});
|
|
178
372
|
if (!r.success) {
|
|
179
|
-
return
|
|
373
|
+
return errorResult({
|
|
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
|
+
});
|
|
180
379
|
}
|
|
181
380
|
const conn = unwrapEntity(r.data, "connection");
|
|
182
|
-
|
|
381
|
+
// F-3.2 (Pass-1 consolidation) — see connection_create for the
|
|
382
|
+
// mutating-tool payload-shape rationale. next_steps[] restored for
|
|
383
|
+
// IA-10 uniformity (single-element array, not flat `next:`).
|
|
384
|
+
return formatResult({
|
|
385
|
+
updated: true,
|
|
386
|
+
id: conn?.id,
|
|
387
|
+
name: conn?.name,
|
|
388
|
+
next_steps: [
|
|
389
|
+
"Re-run apimapper_connection_test if endpoint/auth changed; flows referencing this connection may need to be republished.",
|
|
390
|
+
],
|
|
391
|
+
}, false, { maxChars: 2000 });
|
|
183
392
|
});
|
|
184
393
|
// ── apimapper_connection_delete ────────────────────────────────────
|
|
185
394
|
server.registerTool("apimapper_connection_delete", {
|
|
@@ -194,7 +403,7 @@ export function registerConnectionTools(server) {
|
|
|
194
403
|
.default(false)
|
|
195
404
|
.describe("Must be true to execute. On confirm:false, returns a preview."),
|
|
196
405
|
},
|
|
197
|
-
annotations: destructive(),
|
|
406
|
+
annotations: destructive({ title: "Delete Connection", openWorld: true }),
|
|
198
407
|
}, async ({ id, confirm }) => {
|
|
199
408
|
if (!confirm) {
|
|
200
409
|
// PHP fromControllerResponse(_, 'connection') wraps as
|
|
@@ -218,7 +427,12 @@ export function registerConnectionTools(server) {
|
|
|
218
427
|
}
|
|
219
428
|
const r = await request(`/connections/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
220
429
|
if (!r.success) {
|
|
221
|
-
return
|
|
430
|
+
return errorResult({
|
|
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
|
+
});
|
|
222
436
|
}
|
|
223
437
|
return formatResult({ deleted: true, id }, false, { maxChars: 1500 });
|
|
224
438
|
});
|
|
@@ -231,33 +445,30 @@ export function registerConnectionTools(server) {
|
|
|
231
445
|
inputSchema: {
|
|
232
446
|
id: z.string().describe("Connection ID. Use apimapper_connection_list to find."),
|
|
233
447
|
},
|
|
234
|
-
annotations: readOnly(),
|
|
448
|
+
annotations: readOnly({ title: "Test Connection", openWorld: true }),
|
|
235
449
|
}, async ({ id }) => {
|
|
450
|
+
// PHP wraps probe fields one level deeper:
|
|
451
|
+
// {success:true, data:{actionType, http_code, duration_ms, body_preview, …}, items_count}
|
|
452
|
+
// unwrapInnerSuccess sees outer success:true and lets it through, but
|
|
453
|
+
// probe fields still live under `data.data.*`. Audit: F-A1-05.
|
|
236
454
|
const r = await request("/connections/test", { method: "POST", body: JSON.stringify({ connection_id: id }) }, { unwrapInnerSuccess: true });
|
|
237
455
|
if (!r.success) {
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
payloadFailed: r.payloadFailed,
|
|
243
|
-
context: { id },
|
|
244
|
-
hint: r.payloadFailed
|
|
456
|
+
return errorResult({
|
|
457
|
+
message: r.error ?? "connection test failed",
|
|
458
|
+
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
459
|
+
suggestion: r.payloadFailed
|
|
245
460
|
? "Probe ran but reported failure — check the upstream API status, credentials, or connection endpoint."
|
|
246
461
|
: hintFor(r.errorCode),
|
|
247
|
-
|
|
462
|
+
details: { id, payloadFailed: r.payloadFailed },
|
|
463
|
+
});
|
|
248
464
|
}
|
|
249
465
|
// Defensive unwrap: prefer `r.data.data` if present, fall back to `r.data` itself
|
|
250
466
|
// so a future PHP flatten doesn't break this tool.
|
|
251
467
|
const outer = (r.data && typeof r.data === "object" ? r.data : {});
|
|
252
468
|
const probe = (outer.data && typeof outer.data === "object" ? outer.data : outer);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
actionType: probe.actionType ?? "ok",
|
|
257
|
-
http_code: probe.http_code,
|
|
258
|
-
duration_ms: probe.duration_ms,
|
|
259
|
-
body_preview: probe.body_preview?.slice(0, 500),
|
|
260
|
-
}, false, { maxChars: 2500 });
|
|
469
|
+
// W3.1 — statsResult: the probe outcome is a small flat scalar set.
|
|
470
|
+
// IA-10: the follow-up guidance rides in the stats description.
|
|
471
|
+
return buildConnectionTestStats(id, probe);
|
|
261
472
|
});
|
|
262
473
|
// ── apimapper_connection_health_check ──────────────────────────────
|
|
263
474
|
server.registerTool("apimapper_connection_health_check", {
|
|
@@ -271,20 +482,24 @@ export function registerConnectionTools(server) {
|
|
|
271
482
|
.default([])
|
|
272
483
|
.describe('Connection IDs to probe (empty = all). REST wire-key: connection_ids.'),
|
|
273
484
|
},
|
|
274
|
-
annotations: readOnly(),
|
|
275
|
-
}, async ({ connection_ids }) => {
|
|
485
|
+
annotations: readOnly({ title: "Connection Health Check", openWorld: true }),
|
|
486
|
+
}, async ({ connection_ids }, extra) => {
|
|
487
|
+
// W3.5 — coarse progress side-channel. `null` when the caller sent no
|
|
488
|
+
// progressToken; `progress?.report(...)` then no-ops. The batch probe
|
|
489
|
+
// can take 10-30s, so a start/done signal keeps the caller informed.
|
|
490
|
+
const progress = extra ? createProgressReporter(extra) : null;
|
|
491
|
+
await progress?.report(0, 1, "Probing connections…");
|
|
276
492
|
const r = await request("/connections/health-check", {
|
|
277
493
|
method: "POST",
|
|
278
494
|
body: JSON.stringify({ connection_ids }),
|
|
279
495
|
});
|
|
280
496
|
if (!r.success) {
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}, true);
|
|
497
|
+
return errorResult({
|
|
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
|
+
});
|
|
288
503
|
}
|
|
289
504
|
const data = (r.data && typeof r.data === "object") ? r.data : {};
|
|
290
505
|
// PHP returns map keyed by connection id, NOT under a 'results' key —
|
|
@@ -297,71 +512,143 @@ export function registerConnectionTools(server) {
|
|
|
297
512
|
const o = x;
|
|
298
513
|
return o.success === true || o.actionType === "ok";
|
|
299
514
|
}).length;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
515
|
+
await progress?.report(1, 1, `Probed ${results.length} connection(s)`);
|
|
516
|
+
// W3.1 — statsResult: the batch outcome is a counts dashboard.
|
|
517
|
+
// IA-10: the description routes the AI to connection_test for the
|
|
518
|
+
// per-connection probe detail (the full result map is too large for
|
|
519
|
+
// a stat grid).
|
|
520
|
+
return buildHealthCheckStats(okCount, results.length - okCount, results.length);
|
|
306
521
|
});
|
|
307
522
|
// ── apimapper_connection_data ──────────────────────────────────────
|
|
523
|
+
// rc.10.1 C (2026-05-19) — exposes `limit` + `offset` to AI agents so
|
|
524
|
+
// they can constrain large API responses without injecting template
|
|
525
|
+
// hacks like `template_fields.per_page`. Trim happens IN-MEMORY on
|
|
526
|
+
// the TS side after the response is unwrapped — this is single-shot
|
|
527
|
+
// upstream, no multi-page fetch is triggered. Real cursor / page-loop
|
|
528
|
+
// pagination is a separate roadmap item (see plan Section E1).
|
|
308
529
|
server.registerTool("apimapper_connection_data", {
|
|
309
530
|
title: "Fetch Connection Sample Data",
|
|
310
531
|
description: "Fetch sample data from a connection's configured endpoint. The PHP handler reads ALL " +
|
|
311
|
-
"query params as source-context for template substitution." +
|
|
312
|
-
"
|
|
532
|
+
"query params as source-context for template substitution. Use `limit`/`offset` to trim " +
|
|
533
|
+
"the response items in-memory — this does NOT trigger multi-page fetch upstream (the " +
|
|
534
|
+
"request is single-shot). Use `fields` to project each item down to a whitelist of leaf " +
|
|
535
|
+
"keys, cutting response size on sparse queries." +
|
|
536
|
+
"\n\nExample:\n" +
|
|
537
|
+
" apimapper_connection_data({ id: 'con_abc123', limit: 10 }) // first 10 items\n" +
|
|
538
|
+
" apimapper_connection_data({ id: 'con_abc123', limit: 5, offset: 10 }) // skip 10, take 5\n" +
|
|
539
|
+
" apimapper_connection_data({ id: 'con_abc123', fields: ['title', 'image.url'] }) // 2 leaf keys per item\n" +
|
|
540
|
+
" apimapper_connection_data({ id: 'con_abc123', template_fields: { query: 'nature' } })",
|
|
313
541
|
inputSchema: {
|
|
314
542
|
id: z.string().describe("Connection ID. Use apimapper_connection_list to find."),
|
|
315
|
-
endpoint: z
|
|
543
|
+
endpoint: z
|
|
544
|
+
.string()
|
|
545
|
+
.trim()
|
|
546
|
+
.min(1)
|
|
547
|
+
.optional()
|
|
548
|
+
.describe('Override endpoint (e.g., "Scheduled Events"). Empty-string and whitespace-only values are rejected at the schema boundary to prevent "?endpoint=" query garbage; omit the field entirely to use the connection default.'),
|
|
316
549
|
template_fields: z
|
|
317
|
-
.record(z.string(), z.string())
|
|
550
|
+
.record(z.string(), z.union([z.string(), z.number().finite(), z.boolean()]))
|
|
318
551
|
.optional()
|
|
319
|
-
.describe('Template field values flattened into query string (
|
|
552
|
+
.describe('Template field values flattened into the query string. Accepts string/number/boolean values; non-string values are stringified via JS String() (number 25 → "25", true → "true", false → "false"). NaN/Infinity numbers are rejected by schema (defense-in-depth). Nested objects/arrays are rejected. Example: { spreadsheet_id: "abc", per_page: 25, active: true } → ?spreadsheet_id=abc&per_page=25&active=true'),
|
|
553
|
+
limit: z
|
|
554
|
+
.number()
|
|
555
|
+
.int()
|
|
556
|
+
.min(1)
|
|
557
|
+
.max(500)
|
|
558
|
+
.optional()
|
|
559
|
+
.describe("Trim the returned items array to at most N (1-500). Applied in-memory after the upstream fetch — does NOT request fewer items from the source API. Omit to return the full response."),
|
|
560
|
+
offset: z
|
|
561
|
+
.number()
|
|
562
|
+
.int()
|
|
563
|
+
.min(0)
|
|
564
|
+
.optional()
|
|
565
|
+
.describe("Skip the first N items before applying `limit`. Applied in-memory. Useful for inspecting later items in a large response when combined with `limit`."),
|
|
566
|
+
// rc.13 W3.3 (2026-05-20) — optional per-item field whitelist.
|
|
567
|
+
// When the data payload is an array of objects each item is mapped
|
|
568
|
+
// through pickFields() so only the named leaf keys survive.
|
|
569
|
+
fields: z
|
|
570
|
+
.array(z.string().min(1))
|
|
571
|
+
.max(40)
|
|
572
|
+
.optional()
|
|
573
|
+
.describe("Optional whitelist of fields to keep per item (supports nested paths like 'image.url'). Default: all fields. Cuts response size on sparse queries — e.g. fields:['title','image.url'] keeps only those two leaf keys per record."),
|
|
320
574
|
},
|
|
321
|
-
annotations: readOnly(),
|
|
322
|
-
}, async ({ id, endpoint, template_fields }) => {
|
|
575
|
+
annotations: readOnly({ title: "Get Connection Data", openWorld: true }),
|
|
576
|
+
}, async ({ id, endpoint, template_fields, limit, offset, fields }) => {
|
|
323
577
|
const params = new URLSearchParams();
|
|
324
578
|
if (endpoint)
|
|
325
579
|
params.set("endpoint", endpoint);
|
|
326
580
|
// Flatten template_fields directly as query params — PHP handler does
|
|
327
581
|
// array_filter() across get_query_params() and uses each key as a
|
|
328
|
-
// template variable.
|
|
582
|
+
// template variable. Non-string primitives (number/boolean — F-10/W1.5)
|
|
583
|
+
// are explicitly stringified via String(): the schema admits
|
|
584
|
+
// string|number|boolean unions, so the handler must canonicalise
|
|
585
|
+
// numbers to their decimal repr ("25") and booleans to "true"/"false"
|
|
586
|
+
// before they hit URLSearchParams. URLSearchParams.set would coerce
|
|
587
|
+
// implicitly, but the explicit guard documents the contract and keeps
|
|
588
|
+
// TypeScript happy under the wider schema.
|
|
329
589
|
if (template_fields) {
|
|
330
590
|
for (const [k, v] of Object.entries(template_fields)) {
|
|
331
|
-
params.set(k, v);
|
|
591
|
+
params.set(k, typeof v === "string" ? v : String(v));
|
|
332
592
|
}
|
|
333
593
|
}
|
|
334
594
|
const qs = params.toString();
|
|
335
595
|
const r = await request(`/connections/${encodeURIComponent(id)}/data${qs ? `?${qs}` : ""}`);
|
|
336
596
|
if (!r.success) {
|
|
337
|
-
return
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, true);
|
|
597
|
+
return errorResult({
|
|
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
|
+
});
|
|
344
603
|
}
|
|
345
604
|
// PHP success body: {data:[…items…], connection, body?, content_type?, status?}.
|
|
346
605
|
// Operate on the inner `data` payload, not on the envelope. Surface
|
|
347
606
|
// `body`/`content_type` for the non-JSON path. Audit: F-A1-06.
|
|
348
607
|
const envelope = (r.data && typeof r.data === "object" ? r.data : {});
|
|
349
|
-
const
|
|
608
|
+
const innerRaw = "data" in envelope ? envelope.data : envelope;
|
|
350
609
|
const body = typeof envelope.body === "string" ? envelope.body : undefined;
|
|
351
610
|
const contentType = typeof envelope.content_type === "string" ? envelope.content_type : undefined;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
611
|
+
// rc.10.1 C + rc.13 W1.2 (F-01+F-38) — apply in-memory trim via
|
|
612
|
+
// extracted pure function. See the `applyTrim` JSDoc in
|
|
613
|
+
// connections-trim.ts for the shape-detection contract and
|
|
614
|
+
// shallow-clone guarantees.
|
|
615
|
+
const trim = applyTrim(innerRaw, limit, offset);
|
|
616
|
+
// rc.13 W3.3 — optional field-projection. Runs AFTER trim: the
|
|
617
|
+
// trimmed slice is the smallest correct surface to project, and
|
|
618
|
+
// projection composes cleanly on top of it (project-of-trimmed ≡
|
|
619
|
+
// narrowing a smaller list). Projection is applied ONLY when the
|
|
620
|
+
// (trimmed) payload is an array of plain objects — each item is
|
|
621
|
+
// mapped through pickFields(), which flattens nested paths to leaf
|
|
622
|
+
// keys and silently drops missing fields. For every other shape
|
|
623
|
+
// (single object, scalar, nested-items envelope, array of
|
|
624
|
+
// primitives) projection is skipped and the payload passes through
|
|
625
|
+
// unchanged — there is no per-record surface to whitelist there, so
|
|
626
|
+
// narrowing would be lossy or meaningless. `projected_fields` is
|
|
627
|
+
// still echoed back in the envelope whenever `fields` was supplied,
|
|
628
|
+
// so the AI client always knows projection was requested even when
|
|
629
|
+
// the shape made it a no-op.
|
|
630
|
+
let projectedPayload = trim.payload;
|
|
631
|
+
if (fields && Array.isArray(trim.payload)) {
|
|
632
|
+
projectedPayload = trim.payload.map((item) => item !== null && typeof item === "object" && !Array.isArray(item)
|
|
633
|
+
? pickFields(item, fields)
|
|
634
|
+
: item);
|
|
635
|
+
}
|
|
358
636
|
return formatResult({
|
|
359
637
|
ok: true,
|
|
360
|
-
shape
|
|
361
|
-
|
|
638
|
+
// F-38 — surface a stable, narrow shape enum instead of typeof.
|
|
639
|
+
// "object" is preserved for the legacy non-iterable case so
|
|
640
|
+
// existing wire-shape pins keep working.
|
|
641
|
+
shape: trim.detectedShape === "other" ? typeof trim.payload : trim.detectedShape,
|
|
642
|
+
item_count: trim.itemCount,
|
|
643
|
+
...(trim.trimmed && trim.totalBeforeTrim !== undefined
|
|
644
|
+
? { total_before_trim: trim.totalBeforeTrim, trimmed: true }
|
|
645
|
+
: {}),
|
|
646
|
+
// W3.3 — echo the requested whitelist so the AI knows the
|
|
647
|
+
// per-item shape was (or was meant to be) reduced.
|
|
648
|
+
...(fields ? { projected_fields: fields } : {}),
|
|
362
649
|
body,
|
|
363
650
|
content_type: contentType,
|
|
364
|
-
payload:
|
|
651
|
+
payload: projectedPayload,
|
|
365
652
|
}, false, { maxChars: 6000 });
|
|
366
653
|
});
|
|
367
654
|
// ── apimapper_connection_resources ─────────────────────────────────
|
|
@@ -375,9 +662,15 @@ export function registerConnectionTools(server) {
|
|
|
375
662
|
field: z
|
|
376
663
|
.string()
|
|
377
664
|
.describe('Resource-picker field name (e.g., "spreadsheet_id", "drive_file_id"). REST wire-key: field.'),
|
|
378
|
-
|
|
665
|
+
// rc.10 A4 (2026-05-19): min(3) blocks the single-char "p", "j", "u"
|
|
666
|
+
// probing pattern observed in Maria-walkthrough logs.
|
|
667
|
+
// rc.13 W1.17 (F-29): Master-Doc-Drift — Master requested min(2), kept
|
|
668
|
+
// min(3) (stricter is OK, less regression-risk). Pin-test in
|
|
669
|
+
// connections.test.ts "F-29 connection_resources query min length pin"
|
|
670
|
+
// guards against loosening.
|
|
671
|
+
query: z.string().min(3).optional().describe("Free-text search query. Must be 3+ characters — single-char and 2-char probes are blocked because the resource lists are small enough to scan without typeahead."),
|
|
379
672
|
},
|
|
380
|
-
annotations: readOnly(),
|
|
673
|
+
annotations: readOnly({ title: "List Connection Resources", openWorld: true }),
|
|
381
674
|
}, async ({ id, field, query }) => {
|
|
382
675
|
const params = new URLSearchParams();
|
|
383
676
|
params.set("field", field);
|
|
@@ -385,29 +678,35 @@ export function registerConnectionTools(server) {
|
|
|
385
678
|
params.set("query", query);
|
|
386
679
|
const r = await request(`/connections/${encodeURIComponent(id)}/resources?${params.toString()}`);
|
|
387
680
|
if (!r.success) {
|
|
388
|
-
return
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}, true);
|
|
681
|
+
return errorResult({
|
|
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
|
+
});
|
|
395
687
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
field
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
688
|
+
// A1-P3-1: pure helper handles the three wire-shapes (bare array,
|
|
689
|
+
// `{ resources }`, `{ items }`) + fallback to `[]` for unknown
|
|
690
|
+
// shapes. Pinned per-branch in connections.test.ts.
|
|
691
|
+
const resources = extractResourceList(r.data);
|
|
692
|
+
// W3.1 — tableResult: resources are a flat row list. IA-7: opaque id
|
|
693
|
+
// stays llmOnly. IA-10: the footer carries the next-step guidance and
|
|
694
|
+
// the truncation note when more than 100 resources are returned.
|
|
695
|
+
const truncated = resources.length > 100;
|
|
696
|
+
const footerLines = [
|
|
697
|
+
`Picker field: ${field}${query ? ` · filter: "${query}"` : ""}.`,
|
|
698
|
+
];
|
|
699
|
+
if (truncated) {
|
|
700
|
+
footerLines.push(`Showing first 100 of ${resources.length} resources — narrow with the query filter.`);
|
|
701
|
+
}
|
|
702
|
+
footerLines.push("Next: pass a chosen resource ID into apimapper_connection_update " +
|
|
703
|
+
"({ id, patch }) or the connection's pipeline default_params.");
|
|
704
|
+
return tableResult(resources.slice(0, 100), {
|
|
705
|
+
columns: RESOURCE_TABLE_COLUMNS,
|
|
706
|
+
map: mapResourceRow,
|
|
707
|
+
header: (n) => `${n} resources on ${id}`,
|
|
708
|
+
footer: footerLines.join("\n"),
|
|
709
|
+
});
|
|
411
710
|
});
|
|
412
711
|
// ── apimapper_connection_pipeline_update ───────────────────────────
|
|
413
712
|
server.registerTool("apimapper_connection_pipeline_update", {
|
|
@@ -420,7 +719,7 @@ export function registerConnectionTools(server) {
|
|
|
420
719
|
.record(z.string(), z.unknown())
|
|
421
720
|
.describe('Pipeline JSON (e.g., {"endpoints":[...], "items_path":"data", "items_shape":"flat"})'),
|
|
422
721
|
},
|
|
423
|
-
annotations: mutating(),
|
|
722
|
+
annotations: mutating({ title: "Update Connection Pipeline", openWorld: true }),
|
|
424
723
|
}, async ({ id, pipeline }) => {
|
|
425
724
|
// PHP ConnectionPipelineHandler uses fromControllerResponse(_, 'connection')
|
|
426
725
|
// → {success, connection:{…}}. Unwrap defensively. Audit: F-A1-07.
|
|
@@ -429,7 +728,12 @@ export function registerConnectionTools(server) {
|
|
|
429
728
|
body: JSON.stringify(pipeline),
|
|
430
729
|
});
|
|
431
730
|
if (!r.success) {
|
|
432
|
-
return
|
|
731
|
+
return errorResult({
|
|
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
|
+
});
|
|
433
737
|
}
|
|
434
738
|
const conn = unwrapEntity(r.data, "connection");
|
|
435
739
|
return formatResult({ updated: true, id: conn?.id, name: conn?.name }, false, { maxChars: 2000 });
|