@wootsup/mcp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -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/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 +97 -22
- package/dist/index.js.map +1 -1
- package/dist/install-skill.js +1 -1
- package/dist/modules/apimapper/cache.js +25 -17
- package/dist/modules/apimapper/cache.js.map +1 -1
- package/dist/modules/apimapper/client.d.ts +62 -1
- package/dist/modules/apimapper/client.js +555 -291
- package/dist/modules/apimapper/client.js.map +1 -1
- package/dist/modules/apimapper/connections.js +230 -75
- 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 +19 -47
- 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/flows.js +60 -77
- package/dist/modules/apimapper/flows.js.map +1 -1
- package/dist/modules/apimapper/gateway/advanced-tool.js +56 -5
- package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -1
- package/dist/modules/apimapper/gateway/essentials.d.ts +1 -1
- package/dist/modules/apimapper/gateway/essentials.js +8 -1
- package/dist/modules/apimapper/gateway/essentials.js.map +1 -1
- package/dist/modules/apimapper/get-skill.d.ts +1 -1
- package/dist/modules/apimapper/get-skill.js +44 -6
- package/dist/modules/apimapper/get-skill.js.map +1 -1
- package/dist/modules/apimapper/graph.js +40 -36
- package/dist/modules/apimapper/graph.js.map +1 -1
- package/dist/modules/apimapper/index.js +2 -0
- package/dist/modules/apimapper/index.js.map +1 -1
- package/dist/modules/apimapper/library.js +425 -83
- 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/local-sources.js +20 -34
- package/dist/modules/apimapper/local-sources.js.map +1 -1
- package/dist/modules/apimapper/misc.js +13 -27
- package/dist/modules/apimapper/misc.js.map +1 -1
- package/dist/modules/apimapper/onboarding.d.ts +30 -1
- package/dist/modules/apimapper/onboarding.js +114 -19
- package/dist/modules/apimapper/onboarding.js.map +1 -1
- 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 +46 -0
- package/dist/modules/apimapper/tool-result.js +63 -0
- package/dist/modules/apimapper/tool-result.js.map +1 -0
- package/dist/modules/apimapper/toolslist-size.d.ts +11 -10
- package/dist/modules/apimapper/toolslist-size.js +16 -14
- package/dist/modules/apimapper/toolslist-size.js.map +1 -1
- package/dist/modules/apimapper/types.d.ts +21 -0
- 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 +360 -0
- package/dist/modules/apimapper/whitelist-drift.js.map +1 -0
- package/dist/modules/apimapper/workflows.js +82 -27
- 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 +186 -0
- package/dist/modules/apimapper/yootheme-binding.js.map +1 -0
- package/dist/platform/index.d.ts +56 -0
- package/dist/platform/index.js +151 -2
- package/dist/platform/index.js.map +1 -1
- 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-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 +7 -0
- package/dist/setup-cli.js +29 -1
- package/dist/setup-cli.js.map +1 -1
- package/dist/sites/loader.d.ts +41 -0
- package/dist/sites/loader.js +119 -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 +1 -1
- package/dist/skill-instructions.js +5 -0
- 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 +68 -65
- package/skills/apimapper/SKILL.md +53 -7
- package/skills/apimapper/reference/conditional-style-multi-items.md +114 -0
- package/skills/apimapper/reference/jmespath-pitfalls.md +108 -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 +99 -0
- package/skills/apimapper/reference/troubleshooting.md +20 -0
- package/skills/apimapper/reference/yootheme.md +1 -1
- package/dist/auth/oauth-provider.d.ts +0 -68
- package/dist/auth/oauth-provider.js +0 -232
- package/dist/auth/oauth-provider.js.map +0 -1
- package/dist/server-http.d.ts +0 -22
- package/dist/server-http.js +0 -159
- package/dist/server-http.js.map +0 -1
- package/dist/transports/http.d.ts +0 -29
- package/dist/transports/http.js +0 -267
- package/dist/transports/http.js.map +0 -1
|
@@ -1,8 +1,67 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatResult, tableResult, detailResult, errorResult, readOnly, mutating, destructive, } from "@getimo/mcp-toolkit";
|
|
3
3
|
import { request, hintFor } from "./client.js";
|
|
4
|
+
import { restErrorResult } from "./tool-result.js";
|
|
4
5
|
import { unwrapEntity } from "./envelope.js";
|
|
5
6
|
import { toRows } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Wave-6 R1 (2026-05-29): Treat any non-"none" / non-empty auth_type as
|
|
9
|
+
* "credential required". The set is intentionally inclusive — the worker
|
|
10
|
+
* library uses both canonical (`oauth2_code`, `api_key`, `bearer`, `basic`)
|
|
11
|
+
* and legacy (`oauth`, `apiKey`) values; we don't try to enumerate them.
|
|
12
|
+
*/
|
|
13
|
+
function templateRequiresCredential(authType) {
|
|
14
|
+
if (typeof authType !== "string")
|
|
15
|
+
return false;
|
|
16
|
+
const t = authType.toLowerCase().trim();
|
|
17
|
+
if (t === "" || t === "none")
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wave-6 R1: read the provider slug from a library template. Tolerates
|
|
23
|
+
* both top-level `provider` and `auth_scheme.provider`.
|
|
24
|
+
*/
|
|
25
|
+
function templateProvider(tpl) {
|
|
26
|
+
if (typeof tpl.provider === "string" && tpl.provider !== "")
|
|
27
|
+
return tpl.provider;
|
|
28
|
+
const auth = tpl.auth_scheme;
|
|
29
|
+
if (auth && typeof auth === "object") {
|
|
30
|
+
const p = auth.provider;
|
|
31
|
+
if (typeof p === "string" && p !== "")
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wave-6 R1: read auth_type from a library template, normalising the
|
|
38
|
+
* pre-2.0 alias `oauth → oauth2_code`.
|
|
39
|
+
*/
|
|
40
|
+
function templateAuthType(tpl) {
|
|
41
|
+
let t = tpl.auth_type;
|
|
42
|
+
if (typeof t !== "string" || t === "") {
|
|
43
|
+
const auth = tpl.auth_scheme;
|
|
44
|
+
if (auth && typeof auth === "object") {
|
|
45
|
+
const at = auth.type;
|
|
46
|
+
if (typeof at === "string" && at !== "")
|
|
47
|
+
t = at;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (typeof t !== "string" || t === "")
|
|
51
|
+
return undefined;
|
|
52
|
+
if (t === "oauth")
|
|
53
|
+
return "oauth2_code";
|
|
54
|
+
if (t === "apiKey")
|
|
55
|
+
return "api_key";
|
|
56
|
+
return t;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Wave-6 R1: credential lookup helpers shared with credentials.ts. Reads
|
|
60
|
+
* `oauth_provider` first, then `provider`.
|
|
61
|
+
*/
|
|
62
|
+
function credentialProviderSlug(c) {
|
|
63
|
+
return c.oauth_provider ?? c.provider ?? undefined;
|
|
64
|
+
}
|
|
6
65
|
/** F-14: best-effort JSON.stringify length. Falls back to 0 on cycle. */
|
|
7
66
|
function computeCatalogSize(catalog) {
|
|
8
67
|
try {
|
|
@@ -117,12 +176,7 @@ export function registerLibraryTools(server) {
|
|
|
117
176
|
params.set("limit", String(limit));
|
|
118
177
|
const r = await request(`/library?${params.toString()}`);
|
|
119
178
|
if (!r.success) {
|
|
120
|
-
return
|
|
121
|
-
message: r.error ?? "library list failed",
|
|
122
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
123
|
-
suggestion: hintFor(r.errorCode),
|
|
124
|
-
details: { category, provider, activated, search, page, limit },
|
|
125
|
-
});
|
|
179
|
+
return restErrorResult(r, { category, provider, activated, search, page, limit }, { message: "library list failed" });
|
|
126
180
|
}
|
|
127
181
|
// F-SEED-02: PHP returns `connections`; tolerate legacy `items`.
|
|
128
182
|
let items = Array.isArray(r.data?.connections)
|
|
@@ -163,11 +217,7 @@ export function registerLibraryTools(server) {
|
|
|
163
217
|
}, async () => {
|
|
164
218
|
const r = await request("/library/categories");
|
|
165
219
|
if (!r.success) {
|
|
166
|
-
return
|
|
167
|
-
message: r.error ?? "library categories failed",
|
|
168
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
169
|
-
suggestion: hintFor(r.errorCode),
|
|
170
|
-
});
|
|
220
|
+
return restErrorResult(r, undefined, { message: "library categories failed" });
|
|
171
221
|
}
|
|
172
222
|
const cats = Array.isArray(r.data?.categories) ? r.data.categories : [];
|
|
173
223
|
const rows = cats
|
|
@@ -199,7 +249,14 @@ export function registerLibraryTools(server) {
|
|
|
199
249
|
// ── apimapper_library_featured ─────────────────────────────────────
|
|
200
250
|
server.registerTool("apimapper_library_featured", {
|
|
201
251
|
title: "List Featured Library Items",
|
|
202
|
-
description: "
|
|
252
|
+
description: "List the curated, featured connection templates — the MANDATORY first " +
|
|
253
|
+
"call before building any new API integration. Use this to discover whether " +
|
|
254
|
+
"a ready-made template (with OAuth wizard, auto-header detection, and a " +
|
|
255
|
+
"curated YOOtheme schema) already exists for the API you need. " +
|
|
256
|
+
"Keywords: featured, recommended, starter, popular templates, pre-built, catalog landing. " +
|
|
257
|
+
"When NOT to use: to search the WHOLE catalog by name use apimapper_library_list; " +
|
|
258
|
+
"to read one template's full contract use apimapper_library_connection_detail; " +
|
|
259
|
+
"only fall back to apimapper_connection_create when no template matches." +
|
|
203
260
|
"\n\nExample:\n apimapper_library_featured({})",
|
|
204
261
|
inputSchema: {
|
|
205
262
|
limit: z.number().min(1).max(100).default(12).describe("Max items (1-100)"),
|
|
@@ -209,12 +266,7 @@ export function registerLibraryTools(server) {
|
|
|
209
266
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
210
267
|
const r = await request(`/library/featured?${params.toString()}`);
|
|
211
268
|
if (!r.success) {
|
|
212
|
-
return
|
|
213
|
-
message: r.error ?? "library featured failed",
|
|
214
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
215
|
-
suggestion: hintFor(r.errorCode),
|
|
216
|
-
details: { limit },
|
|
217
|
-
});
|
|
269
|
+
return restErrorResult(r, { limit }, { message: "library featured failed" });
|
|
218
270
|
}
|
|
219
271
|
const items = Array.isArray(r.data?.connections)
|
|
220
272
|
? r.data.connections
|
|
@@ -235,7 +287,13 @@ export function registerLibraryTools(server) {
|
|
|
235
287
|
// ── apimapper_library_popular ──────────────────────────────────────
|
|
236
288
|
server.registerTool("apimapper_library_popular", {
|
|
237
289
|
title: "List Popular Library Items",
|
|
238
|
-
description: "
|
|
290
|
+
description: "List the most-activated connection templates across all users — a " +
|
|
291
|
+
"social-proof ranking of what other people actually wire up. Use to " +
|
|
292
|
+
"discover proven, in-demand integrations when you are exploring options. " +
|
|
293
|
+
"Keywords: popular, trending, most-used, most-activated, top templates. " +
|
|
294
|
+
"When NOT to use: for the editorial short-list use apimapper_library_featured; " +
|
|
295
|
+
"to search by API name use apimapper_library_list; to see what THIS user has " +
|
|
296
|
+
"already activated use apimapper_library_activated." +
|
|
239
297
|
"\n\nExample:\n apimapper_library_popular({})",
|
|
240
298
|
inputSchema: {
|
|
241
299
|
limit: z.number().min(1).max(100).default(12).describe("Max items (1-100)"),
|
|
@@ -245,12 +303,7 @@ export function registerLibraryTools(server) {
|
|
|
245
303
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
246
304
|
const r = await request(`/library/popular?${params.toString()}`);
|
|
247
305
|
if (!r.success) {
|
|
248
|
-
return
|
|
249
|
-
message: r.error ?? "library popular failed",
|
|
250
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
251
|
-
suggestion: hintFor(r.errorCode),
|
|
252
|
-
details: { limit },
|
|
253
|
-
});
|
|
306
|
+
return restErrorResult(r, { limit }, { message: "library popular failed" });
|
|
254
307
|
}
|
|
255
308
|
const items = Array.isArray(r.data?.connections)
|
|
256
309
|
? r.data.connections
|
|
@@ -278,27 +331,63 @@ export function registerLibraryTools(server) {
|
|
|
278
331
|
server.registerTool("apimapper_library_catalog", {
|
|
279
332
|
title: "Get Full Library Catalog",
|
|
280
333
|
description: "Fetch the entire library catalog (all categories + items + featured/popular in one shot). " +
|
|
281
|
-
"Heavy —
|
|
282
|
-
"
|
|
283
|
-
|
|
334
|
+
"Heavy — pass `template_id` to narrow the response to a single template's full record " +
|
|
335
|
+
"(skips the 8000-char truncation cap) or call apimapper_library_connection_detail for the " +
|
|
336
|
+
"rendered detail view." +
|
|
337
|
+
"\n\nExamples:\n" +
|
|
338
|
+
" apimapper_library_catalog({}) // full catalog, may truncate at 8000 chars\n" +
|
|
339
|
+
" apimapper_library_catalog({ template_id: 'google-sheets' }) // single template, full record",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
// Wave-5 F12 (2026-05-29): cold-AI #3 hit 8k truncation on the
|
|
342
|
+
// unfiltered catalog and could not retrieve a target template's
|
|
343
|
+
// computed_fields / default_params. Filtering server-side via the
|
|
344
|
+
// same /library/connections/{id} endpoint that library_connection_detail
|
|
345
|
+
// uses gives the AI the FULL record without exceeding maxChars.
|
|
346
|
+
template_id: z
|
|
347
|
+
.string()
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Filter the catalog response to ONE template's full record (e.g. 'google-sheets'). " +
|
|
350
|
+
"Returns the connection template payload directly under the `connection` key " +
|
|
351
|
+
"(same shape as /library/connections/{id}) — skips the 8000-char truncation cap " +
|
|
352
|
+
"that applies to the unfiltered multi-section response."),
|
|
353
|
+
},
|
|
284
354
|
annotations: readOnly({ title: "Get Library Catalog", openWorld: true }),
|
|
285
|
-
}, async () => {
|
|
355
|
+
}, async ({ template_id }) => {
|
|
356
|
+
// Wave-5 F12: single-template filter routes through the per-connection
|
|
357
|
+
// endpoint to skip the maxChars:8000 truncation that swallows mid-size
|
|
358
|
+
// records in the multi-section catalog response.
|
|
359
|
+
if (typeof template_id === "string" && template_id !== "") {
|
|
360
|
+
const r = await request(`/library/connections/${encodeURIComponent(template_id)}`);
|
|
361
|
+
if (!r.success) {
|
|
362
|
+
return restErrorResult(r, { template_id }, { message: "library catalog (filtered) failed" });
|
|
363
|
+
}
|
|
364
|
+
const connection = unwrapEntity(r.data, "connection") ?? {};
|
|
365
|
+
// DATA-LOW (Wave-B 2026-06-03): the data-level diagnostics key is named
|
|
366
|
+
// `meta` (not `_meta`) to avoid colliding in naming with the
|
|
367
|
+
// protocol-level `_meta` on the result object. This is text content, so
|
|
368
|
+
// the rename is harmless to clients but removes maintainer confusion.
|
|
369
|
+
return formatResult({
|
|
370
|
+
connection,
|
|
371
|
+
meta: {
|
|
372
|
+
filtered_by: { template_id },
|
|
373
|
+
source_endpoint: `/library/connections/${template_id}`,
|
|
374
|
+
},
|
|
375
|
+
}, false, { maxChars: 16000 });
|
|
376
|
+
}
|
|
286
377
|
const r = await request("/library/catalog");
|
|
287
378
|
if (!r.success) {
|
|
288
|
-
return
|
|
289
|
-
message: r.error ?? "library catalog failed",
|
|
290
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
291
|
-
suggestion: hintFor(r.errorCode),
|
|
292
|
-
});
|
|
379
|
+
return restErrorResult(r, undefined, { message: "library catalog failed" });
|
|
293
380
|
}
|
|
294
381
|
const catalog = r.data?.catalog ?? r.data ?? {};
|
|
295
382
|
// F-14: surface payload size + item count so the AI can decide whether
|
|
296
383
|
// to call this heavy tool again, or call a narrower endpoint.
|
|
297
384
|
const sizeBytes = computeCatalogSize(catalog);
|
|
298
385
|
const itemCount = computeCatalogItemCount(catalog, r.data?.total);
|
|
386
|
+
// DATA-LOW (Wave-B 2026-06-03): `meta` (not `_meta`) — see the filtered
|
|
387
|
+
// branch above. Avoids naming-collision with the protocol-level `_meta`.
|
|
299
388
|
return formatResult({
|
|
300
389
|
...catalog,
|
|
301
|
-
|
|
390
|
+
meta: {
|
|
302
391
|
size_bytes: sizeBytes,
|
|
303
392
|
item_count: itemCount,
|
|
304
393
|
},
|
|
@@ -316,12 +405,7 @@ export function registerLibraryTools(server) {
|
|
|
316
405
|
}, async ({ id }) => {
|
|
317
406
|
const r = await request(`/library/connections/${encodeURIComponent(id)}`);
|
|
318
407
|
if (!r.success) {
|
|
319
|
-
return
|
|
320
|
-
message: r.error ?? "library connection detail failed",
|
|
321
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
322
|
-
suggestion: hintFor(r.errorCode),
|
|
323
|
-
details: { id },
|
|
324
|
-
});
|
|
408
|
+
return restErrorResult(r, { id }, { message: "library connection detail failed" });
|
|
325
409
|
}
|
|
326
410
|
if (!r.data || (typeof r.data === "object" && Object.keys(r.data).length === 0)) {
|
|
327
411
|
return errorResult({
|
|
@@ -339,18 +423,21 @@ export function registerLibraryTools(server) {
|
|
|
339
423
|
// ── apimapper_library_activated ────────────────────────────────────
|
|
340
424
|
server.registerTool("apimapper_library_activated", {
|
|
341
425
|
title: "List Activated Library Items",
|
|
342
|
-
description: "List library
|
|
426
|
+
description: "List the library templates THIS site has already activated (each one " +
|
|
427
|
+
"has a backing connection). Use to check what is already wired up before " +
|
|
428
|
+
"activating a duplicate, or to find the connection that came from a template. " +
|
|
429
|
+
"Keywords: activated, installed, my templates, already connected, in use. " +
|
|
430
|
+
"When NOT to use: to browse templates you could activate use " +
|
|
431
|
+
"apimapper_library_featured / apimapper_library_list; to activate one use " +
|
|
432
|
+
"apimapper_library_activate; to deactivate (deletes the connection) use " +
|
|
433
|
+
"apimapper_library_deactivate." +
|
|
343
434
|
"\n\nExample:\n apimapper_library_activated({})",
|
|
344
435
|
inputSchema: {},
|
|
345
436
|
annotations: readOnly({ title: "List Activated Library Connections", openWorld: true }),
|
|
346
437
|
}, async () => {
|
|
347
438
|
const r = await request("/library/activated");
|
|
348
439
|
if (!r.success) {
|
|
349
|
-
return
|
|
350
|
-
message: r.error ?? "library activated failed",
|
|
351
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
352
|
-
suggestion: hintFor(r.errorCode),
|
|
353
|
-
});
|
|
440
|
+
return restErrorResult(r, undefined, { message: "library activated failed" });
|
|
354
441
|
}
|
|
355
442
|
// F-A4-01: PHP returns `connections`; tolerate legacy `items`.
|
|
356
443
|
const items = (Array.isArray(r.data?.connections)
|
|
@@ -371,14 +458,41 @@ export function registerLibraryTools(server) {
|
|
|
371
458
|
});
|
|
372
459
|
});
|
|
373
460
|
// ── apimapper_library_activate ─────────────────────────────────────
|
|
461
|
+
// Wave-6 R1 (2026-05-29): For auth-protected templates (most APIs), the
|
|
462
|
+
// tool now auto-resolves `credential_id` when the user has exactly ONE
|
|
463
|
+
// OAuth credential matching the template's provider — and fails loudly
|
|
464
|
+
// (structured `credential_required` / `credential_ambiguous`) instead of
|
|
465
|
+
// landing a broken connection (empty endpoint, auth_type:none) when no
|
|
466
|
+
// unique match exists. Cold-AI #4 root cause: agents called
|
|
467
|
+
// `library_activate({id, extra_fields})` without a `credential_id`, the
|
|
468
|
+
// server-side defensive hydration silently fell through, and the resulting
|
|
469
|
+
// connection produced empty data the AI couldn't debug from the response.
|
|
374
470
|
server.registerTool("apimapper_library_activate", {
|
|
375
471
|
title: "Activate Library Template",
|
|
376
|
-
description: "Activate a library template — creates a new connection.
|
|
377
|
-
"
|
|
378
|
-
"
|
|
472
|
+
description: "Activate a library template — creates a new connection. " +
|
|
473
|
+
"\n\n**FOR AUTH-PROTECTED TEMPLATES (most APIs incl. Google Sheets, Notion, " +
|
|
474
|
+
"Airtable, Pexels, OpenWeatherMap, Calendly, GitHub):** you MUST link a " +
|
|
475
|
+
"credential. The tool auto-links when exactly ONE matching credential exists " +
|
|
476
|
+
"for the template's provider; otherwise it fails with `credential_required` " +
|
|
477
|
+
"or `credential_ambiguous`. Pre-flight: call " +
|
|
478
|
+
"`apimapper_credential_list({})` and confirm a credential exists for the " +
|
|
479
|
+
"provider — if not, create one via `apimapper_oauth_authorize_begin` (OAuth) " +
|
|
480
|
+
"or `apimapper_advanced({tool:'apimapper_credential_create',arguments:{…}})` " +
|
|
481
|
+
"(api_key/bearer). " +
|
|
482
|
+
"\n\nREST contract: `extra_fields` (template placeholder values) + optional " +
|
|
483
|
+
"`library_connection` (template metadata override) + optional `credential_id`." +
|
|
484
|
+
"\n\nExamples:\n" +
|
|
485
|
+
" apimapper_library_activate({ id: 'pexels', credential_id: 'cred_pexels', extra_fields: { api_key: '…' } })\n" +
|
|
486
|
+
" apimapper_library_activate({ id: 'google-sheets', extra_fields: { spreadsheet_id: '1AbC…' } }) // auto-links the unique Google OAuth credential",
|
|
379
487
|
inputSchema: {
|
|
380
488
|
id: z.string().describe("Library item ID. Use apimapper_library_list to find."),
|
|
381
|
-
credential_id: z
|
|
489
|
+
credential_id: z
|
|
490
|
+
.string()
|
|
491
|
+
.optional()
|
|
492
|
+
.describe("Credential ID. REQUIRED for auth-protected templates unless exactly " +
|
|
493
|
+
"one matching credential exists (then auto-linked). Use " +
|
|
494
|
+
"apimapper_credential_list({}) to find one, or " +
|
|
495
|
+
"apimapper_oauth_authorize_begin({provider}) to create one."),
|
|
382
496
|
extra_fields: z
|
|
383
497
|
.record(z.string(), z.unknown())
|
|
384
498
|
.optional()
|
|
@@ -388,35 +502,159 @@ export function registerLibraryTools(server) {
|
|
|
388
502
|
.optional()
|
|
389
503
|
.describe('Optional template metadata override (rename connection, override base_url, etc.). ' +
|
|
390
504
|
'REST key: library_connection.'),
|
|
505
|
+
// F1 (2026-06-08): one credential can back MULTIPLE connections (one per
|
|
506
|
+
// resource — e.g. several Airtable bases). The backend reuses an
|
|
507
|
+
// existing connection only when the SAME resource (extra_fields) is
|
|
508
|
+
// requested AND that connection is healthy. Pass `force_new: true` to
|
|
509
|
+
// skip reuse entirely and always create a fresh connection (a distinct
|
|
510
|
+
// resource on the same credential). REST key: force_new.
|
|
511
|
+
force_new: z
|
|
512
|
+
.boolean()
|
|
513
|
+
.optional()
|
|
514
|
+
.describe("Force creation of a NEW connection even if a matching one exists " +
|
|
515
|
+
"(default: false → the backend reuses a healthy connection for the " +
|
|
516
|
+
"same resource). Set true when wiring a second/different resource " +
|
|
517
|
+
"on the same credential. REST key: force_new."),
|
|
391
518
|
},
|
|
392
519
|
// W1.26 (IA-1): activate is idempotent — re-activating the same template
|
|
393
|
-
// yields the same connection (
|
|
394
|
-
// duplicating). mutating() is the correct
|
|
520
|
+
// for the same resource yields the same connection (the backend returns
|
|
521
|
+
// the existing row instead of duplicating). mutating() is the correct
|
|
522
|
+
// semantic class.
|
|
395
523
|
annotations: mutating({ title: "Activate Library Connection", openWorld: true }),
|
|
396
|
-
}, async ({ id, credential_id, extra_fields, library_connection }) => {
|
|
524
|
+
}, async ({ id, credential_id, extra_fields, library_connection, force_new }) => {
|
|
525
|
+
// Wave-6 R1 (2026-05-29): Credential auto-resolution gate.
|
|
526
|
+
// When `credential_id` is missing AND the template has a non-none auth_type,
|
|
527
|
+
// fetch the template + credential list, try to auto-link a unique match,
|
|
528
|
+
// and fail-loud with a structured error when 0 or >1 candidates exist.
|
|
529
|
+
// The auto-link logic mirrors `apimapper_oauth_authorize_begin` (which
|
|
530
|
+
// resolves an OAuth credential by provider) but applies to ANY auth_type
|
|
531
|
+
// (api_key, bearer, basic_auth, oauth2_*).
|
|
532
|
+
let resolvedCredentialId = credential_id;
|
|
533
|
+
let autoLinkedFrom;
|
|
534
|
+
if (!resolvedCredentialId) {
|
|
535
|
+
// Fetch template metadata to discover its auth scheme.
|
|
536
|
+
const tr = await request(`/library/connections/${encodeURIComponent(id)}`);
|
|
537
|
+
if (tr.success && tr.data) {
|
|
538
|
+
const tplRaw = unwrapEntity(tr.data, "connection") ?? {};
|
|
539
|
+
const tplAuthType = templateAuthType(tplRaw);
|
|
540
|
+
const tplProvider = templateProvider(tplRaw);
|
|
541
|
+
if (templateRequiresCredential(tplAuthType)) {
|
|
542
|
+
// Fetch credentials sanitised — we only need id/name/provider/auth_type.
|
|
543
|
+
const cr = await request("/credentials", {}, { sanitize: true });
|
|
544
|
+
if (!cr.success) {
|
|
545
|
+
return restErrorResult(cr, { id, template_provider: tplProvider, template_auth_type: tplAuthType }, {
|
|
546
|
+
message: "credential lookup failed during activation",
|
|
547
|
+
// Per-site nuance: a domain code fallback + a fixed
|
|
548
|
+
// activation-recovery suggestion override hintFor().
|
|
549
|
+
code: "credential_lookup_failed",
|
|
550
|
+
suggestion: "Retry, or pass an explicit `credential_id`. " +
|
|
551
|
+
"Use apimapper_credential_list({}) to find one.",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
const allCreds = Array.isArray(cr.data?.credentials) ? cr.data.credentials : [];
|
|
555
|
+
// Match by provider first; if template has no provider, fall back to auth_type.
|
|
556
|
+
const candidates = tplProvider
|
|
557
|
+
? allCreds.filter((c) => credentialProviderSlug(c)?.toLowerCase() === tplProvider.toLowerCase())
|
|
558
|
+
: allCreds.filter((c) => c.auth_type === tplAuthType);
|
|
559
|
+
if (candidates.length === 0) {
|
|
560
|
+
const providerHint = tplProvider ?? tplAuthType ?? "the template's provider";
|
|
561
|
+
return errorResult({
|
|
562
|
+
message: `Template "${id}" requires authentication (${tplAuthType}) but no matching ` +
|
|
563
|
+
`credential exists for "${providerHint}".`,
|
|
564
|
+
code: "credential_required",
|
|
565
|
+
suggestion: `Create a credential first:\n` +
|
|
566
|
+
` • OAuth: apimapper_oauth_authorize_begin({ provider: "${tplProvider ?? "<provider>"}" })\n` +
|
|
567
|
+
` • api_key/bearer/basic: apimapper_advanced({ tool: "apimapper_credential_create", arguments: { auth_type: "${tplAuthType}", ... } })\n` +
|
|
568
|
+
`Then retry apimapper_library_activate with the new credential_id.`,
|
|
569
|
+
details: {
|
|
570
|
+
id,
|
|
571
|
+
template_provider: tplProvider,
|
|
572
|
+
template_auth_type: tplAuthType,
|
|
573
|
+
available_credentials: allCreds.map((c) => ({
|
|
574
|
+
id: c.id,
|
|
575
|
+
name: c.name,
|
|
576
|
+
provider: credentialProviderSlug(c),
|
|
577
|
+
auth_type: c.auth_type,
|
|
578
|
+
})),
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (candidates.length > 1) {
|
|
583
|
+
return errorResult({
|
|
584
|
+
message: `Template "${id}" requires authentication and ${candidates.length} candidate ` +
|
|
585
|
+
`credentials exist for "${tplProvider ?? tplAuthType}". Pass an explicit credential_id.`,
|
|
586
|
+
code: "credential_ambiguous",
|
|
587
|
+
suggestion: "Re-call apimapper_library_activate with one of the credential_ids below " +
|
|
588
|
+
"(see `details.candidates`).",
|
|
589
|
+
details: {
|
|
590
|
+
id,
|
|
591
|
+
template_provider: tplProvider,
|
|
592
|
+
template_auth_type: tplAuthType,
|
|
593
|
+
candidates: candidates.map((c) => ({
|
|
594
|
+
id: c.id,
|
|
595
|
+
name: c.name,
|
|
596
|
+
provider: credentialProviderSlug(c),
|
|
597
|
+
auth_type: c.auth_type,
|
|
598
|
+
})),
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
// Exactly one match → auto-link.
|
|
603
|
+
const picked = candidates[0];
|
|
604
|
+
resolvedCredentialId = picked.id;
|
|
605
|
+
autoLinkedFrom = {
|
|
606
|
+
provider: credentialProviderSlug(picked),
|
|
607
|
+
credentialName: picked.name,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Note: if template fetch fails or auth_type is "none"/missing, we
|
|
612
|
+
// fall through to the original (no credential) activation path. The
|
|
613
|
+
// server-side validator will still reject missing required extra_fields.
|
|
614
|
+
}
|
|
397
615
|
const body = {};
|
|
398
|
-
if (
|
|
399
|
-
body.credential_id =
|
|
616
|
+
if (resolvedCredentialId)
|
|
617
|
+
body.credential_id = resolvedCredentialId;
|
|
400
618
|
if (extra_fields)
|
|
401
619
|
body.extra_fields = extra_fields;
|
|
402
620
|
if (library_connection)
|
|
403
621
|
body.library_connection = library_connection;
|
|
622
|
+
// F1 (2026-06-08): only send force_new when explicitly true — keep the
|
|
623
|
+
// request body minimal so a default activate stays byte-identical to the
|
|
624
|
+
// pre-F1 wire shape (the backend defaults to reuse).
|
|
625
|
+
if (force_new === true)
|
|
626
|
+
body.force_new = true;
|
|
404
627
|
const r = await request(`/library/${encodeURIComponent(id)}/activate`, {
|
|
405
628
|
method: "POST",
|
|
406
629
|
body: JSON.stringify(body),
|
|
407
630
|
});
|
|
408
631
|
if (!r.success) {
|
|
409
|
-
return
|
|
410
|
-
message: r.error ?? "library activate failed",
|
|
411
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
412
|
-
suggestion: hintFor(r.errorCode),
|
|
413
|
-
details: { id },
|
|
414
|
-
});
|
|
632
|
+
return restErrorResult(r, { id }, { message: "library activate failed" });
|
|
415
633
|
}
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
634
|
+
// F1 (2026-06-08): the backend is now the SOLE authority on reuse vs.
|
|
635
|
+
// create vs. heal. One credential can back MANY connections (one per
|
|
636
|
+
// resource); the backend reuses a connection only when the SAME resource
|
|
637
|
+
// is requested AND that connection is healthy, and it NEVER mutates a
|
|
638
|
+
// healthy row. It reports its decision via three booleans on the response:
|
|
639
|
+
//
|
|
640
|
+
// reused:true, mutated:false → existing healthy connection
|
|
641
|
+
// returned unchanged.
|
|
642
|
+
// reused:true, healed:true, mutated:true → existing connection was
|
|
643
|
+
// corrupt; the backend repaired
|
|
644
|
+
// it in place.
|
|
645
|
+
// (no reused / reused:false) → a brand-new connection was
|
|
646
|
+
// created (new resource or
|
|
647
|
+
// force_new:true).
|
|
648
|
+
//
|
|
649
|
+
// The MCP layer no longer re-derives a mismatch and rejects after the
|
|
650
|
+
// fact (the old `connection_reused_with_stale_state` path assumed a
|
|
651
|
+
// mutation that the backend no longer performs). We simply INTERPRET the
|
|
652
|
+
// backend's booleans into a clear, human-readable status string.
|
|
653
|
+
//
|
|
654
|
+
// F-A4-03: PHP returns `{success, connection: {id, ...}, reused?, healed?,
|
|
655
|
+
// mutated?, flow?}`. Read the nested `connection` directly (we DO NOT use
|
|
656
|
+
// unwrapEntity here because its fall-back to `data` would surface the
|
|
657
|
+
// wrong `id` if the envelope key is missing).
|
|
420
658
|
const dataObj = r.data && typeof r.data === "object" ? r.data : {};
|
|
421
659
|
const conn = (dataObj.connection && typeof dataObj.connection === "object")
|
|
422
660
|
? dataObj.connection
|
|
@@ -426,16 +664,55 @@ export function registerLibraryTools(server) {
|
|
|
426
664
|
(typeof dataObj.connectionId === "string" ? dataObj.connectionId : undefined) ||
|
|
427
665
|
null;
|
|
428
666
|
const reused = typeof dataObj.reused === "boolean" ? dataObj.reused : undefined;
|
|
667
|
+
const healed = typeof dataObj.healed === "boolean" ? dataObj.healed : undefined;
|
|
668
|
+
const mutated = typeof dataObj.mutated === "boolean" ? dataObj.mutated : undefined;
|
|
429
669
|
const flowSummary = dataObj.flow && typeof dataObj.flow === "object"
|
|
430
670
|
? dataObj.flow
|
|
431
671
|
: undefined;
|
|
672
|
+
// F1: derive a single status + note from the backend's decision booleans.
|
|
673
|
+
let status;
|
|
674
|
+
let note;
|
|
675
|
+
if (reused === true && healed === true) {
|
|
676
|
+
status = "reused_healed";
|
|
677
|
+
note =
|
|
678
|
+
"Reused (healed): an existing connection for this resource was corrupt " +
|
|
679
|
+
"and the backend repaired it in place. No duplicate was created.";
|
|
680
|
+
}
|
|
681
|
+
else if (reused === true) {
|
|
682
|
+
status = "reused";
|
|
683
|
+
note =
|
|
684
|
+
"Reused (unchanged): a healthy connection already existed for this " +
|
|
685
|
+
"resource and was returned as-is — no changes were made. Pass " +
|
|
686
|
+
"force_new:true to create a separate connection for a different resource.";
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
status = "activated";
|
|
690
|
+
note = "Connection created; use apimapper_connection_list to find the new id.";
|
|
691
|
+
}
|
|
432
692
|
return formatResult({
|
|
433
693
|
activated: true,
|
|
694
|
+
status,
|
|
434
695
|
id,
|
|
435
696
|
connection_id: connectionId,
|
|
436
|
-
|
|
697
|
+
// Wave-6 R1 — surface auto-link decision when the tool resolved
|
|
698
|
+
// the credential for the agent. Helps the AI confirm which
|
|
699
|
+
// credential ended up wired in (vs. a previously stored one).
|
|
700
|
+
...(autoLinkedFrom
|
|
701
|
+
? {
|
|
702
|
+
auto_linked_credential: {
|
|
703
|
+
credential_id: resolvedCredentialId,
|
|
704
|
+
provider: autoLinkedFrom.provider,
|
|
705
|
+
credential_name: autoLinkedFrom.credentialName,
|
|
706
|
+
},
|
|
707
|
+
}
|
|
708
|
+
: {}),
|
|
709
|
+
// F1 — echo the backend's decision booleans verbatim so the agent
|
|
710
|
+
// can branch on them too (only when the backend supplied them).
|
|
711
|
+
...(reused !== undefined ? { reused } : {}),
|
|
712
|
+
...(healed !== undefined ? { healed } : {}),
|
|
713
|
+
...(mutated !== undefined ? { mutated } : {}),
|
|
437
714
|
flow: flowSummary,
|
|
438
|
-
note
|
|
715
|
+
note,
|
|
439
716
|
}, false, { maxChars: 3000 });
|
|
440
717
|
});
|
|
441
718
|
// ── apimapper_library_deactivate ───────────────────────────────────
|
|
@@ -465,12 +742,7 @@ export function registerLibraryTools(server) {
|
|
|
465
742
|
method: "DELETE",
|
|
466
743
|
});
|
|
467
744
|
if (!r.success) {
|
|
468
|
-
return
|
|
469
|
-
message: r.error ?? "library deactivate failed",
|
|
470
|
-
code: r.errorCode ?? (r.status ? String(r.status) : undefined),
|
|
471
|
-
suggestion: hintFor(r.errorCode),
|
|
472
|
-
details: { id },
|
|
473
|
-
});
|
|
745
|
+
return restErrorResult(r, { id }, { message: "library deactivate failed" });
|
|
474
746
|
}
|
|
475
747
|
return formatResult({ deactivated: true, id }, false, { maxChars: 1500 });
|
|
476
748
|
});
|
|
@@ -490,21 +762,88 @@ function buildConnectionDetail(id, connection) {
|
|
|
490
762
|
{ key: "description", label: "Description", value: str(connection.description) },
|
|
491
763
|
];
|
|
492
764
|
const overview = overviewAll.filter((e) => e.value !== null);
|
|
493
|
-
// Endpoints arrive as an array of objects
|
|
494
|
-
// endpoint
|
|
765
|
+
// Endpoints arrive as an array of objects. Friction-3 fix (Task #46,
|
|
766
|
+
// 2026-05-29): expand each endpoint to surface its NAME (not "Endpoint 1")
|
|
767
|
+
// and the list of required/optional extra_fields the template declares —
|
|
768
|
+
// those are the values an AI agent must pass into `library_activate({extra_fields})`.
|
|
495
769
|
const endpoints = Array.isArray(connection.endpoints) ? connection.endpoints : [];
|
|
770
|
+
const templateExtraFields = Array.isArray(connection.extra_fields)
|
|
771
|
+
? connection.extra_fields
|
|
772
|
+
: [];
|
|
496
773
|
const endpointEntries = [
|
|
497
774
|
{ key: "endpoint_count", label: "Endpoints", value: endpoints.length },
|
|
498
|
-
...endpoints.
|
|
775
|
+
...endpoints.flatMap((ep, idx) => {
|
|
499
776
|
const epObj = ep && typeof ep === "object" ? ep : {};
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
777
|
+
const name = str(epObj.name) ?? str(epObj.id) ?? `endpoint ${idx + 1}`;
|
|
778
|
+
const description = str(epObj.description) ?? str(epObj.label);
|
|
779
|
+
const path = str(epObj.path);
|
|
780
|
+
const method = str(epObj.method);
|
|
781
|
+
const entries = [
|
|
782
|
+
{
|
|
783
|
+
key: `endpoint_${idx}_name`,
|
|
784
|
+
label: ` ${idx + 1}. Name`,
|
|
785
|
+
value: name,
|
|
786
|
+
format: "code",
|
|
787
|
+
copyable: true,
|
|
788
|
+
},
|
|
789
|
+
];
|
|
790
|
+
if (description) {
|
|
791
|
+
entries.push({
|
|
792
|
+
key: `endpoint_${idx}_description`,
|
|
793
|
+
label: " Description",
|
|
794
|
+
value: description,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
if (path || method) {
|
|
798
|
+
entries.push({
|
|
799
|
+
key: `endpoint_${idx}_route`,
|
|
800
|
+
label: " Route",
|
|
801
|
+
value: `${method ?? "GET"} ${path ?? "—"}`,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return entries;
|
|
506
805
|
}),
|
|
507
806
|
];
|
|
807
|
+
// Friction-3 (Task #46): list required + optional extra_fields. AI agents
|
|
808
|
+
// need to know which keys MUST appear in the `extra_fields` argument of
|
|
809
|
+
// library_activate.
|
|
810
|
+
const extraFieldEntries = [];
|
|
811
|
+
if (templateExtraFields.length > 0) {
|
|
812
|
+
for (const ef of templateExtraFields) {
|
|
813
|
+
const efName = str(ef.name);
|
|
814
|
+
if (!efName)
|
|
815
|
+
continue;
|
|
816
|
+
const required = ef.required === true;
|
|
817
|
+
const efType = str(ef.type) ?? "string";
|
|
818
|
+
// Wave-8 C1 (2026-05-29): featured-connections templates ship the
|
|
819
|
+
// human-readable description under `help_text` (not `description`).
|
|
820
|
+
// The label-fallback was kept but now ranks below help_text so
|
|
821
|
+
// google-sheets' "Choose from your recent spreadsheets or paste a URL"
|
|
822
|
+
// surfaces instead of just "Spreadsheet".
|
|
823
|
+
const efDesc = str(ef.help_text) ??
|
|
824
|
+
str(ef.description) ??
|
|
825
|
+
str(ef.label) ??
|
|
826
|
+
"";
|
|
827
|
+
const efExample = str(ef.example) ?? str(ef.placeholder) ?? "";
|
|
828
|
+
const valueParts = [];
|
|
829
|
+
valueParts.push(`type=${efType}`);
|
|
830
|
+
if (required)
|
|
831
|
+
valueParts.push("REQUIRED");
|
|
832
|
+
else
|
|
833
|
+
valueParts.push("optional");
|
|
834
|
+
if (efExample)
|
|
835
|
+
valueParts.push(`e.g. "${efExample}"`);
|
|
836
|
+
if (efDesc)
|
|
837
|
+
valueParts.push(efDesc);
|
|
838
|
+
extraFieldEntries.push({
|
|
839
|
+
key: `extra_${efName}`,
|
|
840
|
+
label: ` ${efName}`,
|
|
841
|
+
value: valueParts.join(" · "),
|
|
842
|
+
format: "code",
|
|
843
|
+
copyable: true,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
508
847
|
const auth = connection.auth_scheme && typeof connection.auth_scheme === "object"
|
|
509
848
|
? connection.auth_scheme
|
|
510
849
|
: undefined;
|
|
@@ -520,6 +859,9 @@ function buildConnectionDetail(id, connection) {
|
|
|
520
859
|
groups: [
|
|
521
860
|
{ label: "Template", entries: overview },
|
|
522
861
|
...(endpointEntries.length > 0 ? [{ label: "Endpoints", entries: endpointEntries }] : []),
|
|
862
|
+
...(extraFieldEntries.length > 0
|
|
863
|
+
? [{ label: "Required/Optional extra_fields", entries: extraFieldEntries }]
|
|
864
|
+
: []),
|
|
523
865
|
...(authEntries.length > 0 ? [{ label: "Authentication", entries: authEntries }] : []),
|
|
524
866
|
{
|
|
525
867
|
// IA-10 (W3.9): actionable next steps in a dedicated group — visible
|