@wootsup/mcp 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CHANGELOG.md +157 -83
  2. package/README.md +31 -27
  3. package/SECURITY.md +15 -6
  4. package/dist/auth/keychain.d.ts +27 -1
  5. package/dist/auth/keychain.js +48 -2
  6. package/dist/auth/keychain.js.map +1 -1
  7. package/dist/catalog/build-catalog.d.ts +31 -0
  8. package/dist/catalog/build-catalog.js +68 -0
  9. package/dist/catalog/build-catalog.js.map +1 -0
  10. package/dist/cli-hint.d.ts +22 -0
  11. package/dist/cli-hint.js +55 -0
  12. package/dist/cli-hint.js.map +1 -0
  13. package/dist/index.js +129 -22
  14. package/dist/index.js.map +1 -1
  15. package/dist/install-skill.js +1 -1
  16. package/dist/modules/apimapper/auto-layout.d.ts +21 -0
  17. package/dist/modules/apimapper/auto-layout.js +54 -0
  18. package/dist/modules/apimapper/auto-layout.js.map +1 -0
  19. package/dist/modules/apimapper/cache.js +25 -17
  20. package/dist/modules/apimapper/cache.js.map +1 -1
  21. package/dist/modules/apimapper/client.d.ts +115 -4
  22. package/dist/modules/apimapper/client.js +699 -304
  23. package/dist/modules/apimapper/client.js.map +1 -1
  24. package/dist/modules/apimapper/connections-format.d.ts +31 -1
  25. package/dist/modules/apimapper/connections-format.js +97 -5
  26. package/dist/modules/apimapper/connections-format.js.map +1 -1
  27. package/dist/modules/apimapper/connections.d.ts +9 -7
  28. package/dist/modules/apimapper/connections.js +449 -127
  29. package/dist/modules/apimapper/connections.js.map +1 -1
  30. package/dist/modules/apimapper/credential-sanitizer.d.ts +5 -0
  31. package/dist/modules/apimapper/credential-sanitizer.js +60 -1
  32. package/dist/modules/apimapper/credential-sanitizer.js.map +1 -1
  33. package/dist/modules/apimapper/credentials.js +105 -61
  34. package/dist/modules/apimapper/credentials.js.map +1 -1
  35. package/dist/modules/apimapper/diagnose.js +21 -2
  36. package/dist/modules/apimapper/diagnose.js.map +1 -1
  37. package/dist/modules/apimapper/elicitation.d.ts +29 -0
  38. package/dist/modules/apimapper/elicitation.js +62 -0
  39. package/dist/modules/apimapper/elicitation.js.map +1 -1
  40. package/dist/modules/apimapper/example-extract.d.ts +13 -0
  41. package/dist/modules/apimapper/example-extract.js +111 -0
  42. package/dist/modules/apimapper/example-extract.js.map +1 -0
  43. package/dist/modules/apimapper/filter-operators.d.ts +24 -0
  44. package/dist/modules/apimapper/filter-operators.js +103 -0
  45. package/dist/modules/apimapper/filter-operators.js.map +1 -0
  46. package/dist/modules/apimapper/flows-format.js +92 -22
  47. package/dist/modules/apimapper/flows-format.js.map +1 -1
  48. package/dist/modules/apimapper/flows.d.ts +8 -7
  49. package/dist/modules/apimapper/flows.js +275 -120
  50. package/dist/modules/apimapper/flows.js.map +1 -1
  51. package/dist/modules/apimapper/gateway/advanced-read-tool.d.ts +9 -0
  52. package/dist/modules/apimapper/gateway/advanced-read-tool.js +172 -0
  53. package/dist/modules/apimapper/gateway/advanced-read-tool.js.map +1 -0
  54. package/dist/modules/apimapper/gateway/advanced-tool.js +66 -106
  55. package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -1
  56. package/dist/modules/apimapper/gateway/collect-module-tools.d.ts +17 -0
  57. package/dist/modules/apimapper/gateway/collect-module-tools.js +44 -0
  58. package/dist/modules/apimapper/gateway/collect-module-tools.js.map +1 -0
  59. package/dist/modules/apimapper/gateway/essentials.d.ts +1 -1
  60. package/dist/modules/apimapper/gateway/essentials.js +21 -2
  61. package/dist/modules/apimapper/gateway/essentials.js.map +1 -1
  62. package/dist/modules/apimapper/gateway/gateway-shared.d.ts +21 -0
  63. package/dist/modules/apimapper/gateway/gateway-shared.js +124 -0
  64. package/dist/modules/apimapper/gateway/gateway-shared.js.map +1 -0
  65. package/dist/modules/apimapper/gateway/test-support.d.ts +1 -17
  66. package/dist/modules/apimapper/gateway/test-support.js +4 -33
  67. package/dist/modules/apimapper/gateway/test-support.js.map +1 -1
  68. package/dist/modules/apimapper/get-skill-cores.d.ts +4 -0
  69. package/dist/modules/apimapper/get-skill-cores.js +220 -0
  70. package/dist/modules/apimapper/get-skill-cores.js.map +1 -0
  71. package/dist/modules/apimapper/get-skill.d.ts +1 -1
  72. package/dist/modules/apimapper/get-skill.js +74 -9
  73. package/dist/modules/apimapper/get-skill.js.map +1 -1
  74. package/dist/modules/apimapper/graph-builder.d.ts +85 -2
  75. package/dist/modules/apimapper/graph-builder.js +151 -15
  76. package/dist/modules/apimapper/graph-builder.js.map +1 -1
  77. package/dist/modules/apimapper/graph.js +152 -48
  78. package/dist/modules/apimapper/graph.js.map +1 -1
  79. package/dist/modules/apimapper/index.js +27 -13
  80. package/dist/modules/apimapper/index.js.map +1 -1
  81. package/dist/modules/apimapper/jmespath-test.d.ts +4 -0
  82. package/dist/modules/apimapper/jmespath-test.js +152 -0
  83. package/dist/modules/apimapper/jmespath-test.js.map +1 -0
  84. package/dist/modules/apimapper/library.js +553 -88
  85. package/dist/modules/apimapper/library.js.map +1 -1
  86. package/dist/modules/apimapper/license.js +12 -36
  87. package/dist/modules/apimapper/license.js.map +1 -1
  88. package/dist/modules/apimapper/list-footer.d.ts +27 -0
  89. package/dist/modules/apimapper/list-footer.js +57 -0
  90. package/dist/modules/apimapper/list-footer.js.map +1 -0
  91. package/dist/modules/apimapper/local-sources.js +100 -57
  92. package/dist/modules/apimapper/local-sources.js.map +1 -1
  93. package/dist/modules/apimapper/mcp-client-identity.d.ts +32 -0
  94. package/dist/modules/apimapper/mcp-client-identity.js +70 -0
  95. package/dist/modules/apimapper/mcp-client-identity.js.map +1 -0
  96. package/dist/modules/apimapper/merge-constants.d.ts +6 -0
  97. package/dist/modules/apimapper/merge-constants.js +26 -0
  98. package/dist/modules/apimapper/merge-constants.js.map +1 -0
  99. package/dist/modules/apimapper/misc.js +13 -27
  100. package/dist/modules/apimapper/misc.js.map +1 -1
  101. package/dist/modules/apimapper/node-schema.d.ts +52 -2
  102. package/dist/modules/apimapper/node-schema.js +95 -4
  103. package/dist/modules/apimapper/node-schema.js.map +1 -1
  104. package/dist/modules/apimapper/onboarding.d.ts +59 -1
  105. package/dist/modules/apimapper/onboarding.js +231 -28
  106. package/dist/modules/apimapper/onboarding.js.map +1 -1
  107. package/dist/modules/apimapper/read-cache.d.ts +16 -3
  108. package/dist/modules/apimapper/read-cache.js +59 -4
  109. package/dist/modules/apimapper/read-cache.js.map +1 -1
  110. package/dist/modules/apimapper/render/index.js +26 -5
  111. package/dist/modules/apimapper/render/index.js.map +1 -1
  112. package/dist/modules/apimapper/resource-id.d.ts +13 -0
  113. package/dist/modules/apimapper/resource-id.js +69 -0
  114. package/dist/modules/apimapper/resource-id.js.map +1 -0
  115. package/dist/modules/apimapper/schema.js +9 -18
  116. package/dist/modules/apimapper/schema.js.map +1 -1
  117. package/dist/modules/apimapper/settings.js +49 -52
  118. package/dist/modules/apimapper/settings.js.map +1 -1
  119. package/dist/modules/apimapper/sites-tools.d.ts +29 -0
  120. package/dist/modules/apimapper/sites-tools.js +165 -0
  121. package/dist/modules/apimapper/sites-tools.js.map +1 -0
  122. package/dist/modules/apimapper/tool-result.d.ts +66 -0
  123. package/dist/modules/apimapper/tool-result.js +125 -0
  124. package/dist/modules/apimapper/tool-result.js.map +1 -0
  125. package/dist/modules/apimapper/toolslist-size.d.ts +12 -11
  126. package/dist/modules/apimapper/toolslist-size.js +34 -21
  127. package/dist/modules/apimapper/toolslist-size.js.map +1 -1
  128. package/dist/modules/apimapper/types.d.ts +34 -0
  129. package/dist/modules/apimapper/types.js +1 -1
  130. package/dist/modules/apimapper/types.js.map +1 -1
  131. package/dist/modules/apimapper/whitelist-drift.d.ts +85 -0
  132. package/dist/modules/apimapper/whitelist-drift.js +375 -0
  133. package/dist/modules/apimapper/whitelist-drift.js.map +1 -0
  134. package/dist/modules/apimapper/workflows.js +302 -58
  135. package/dist/modules/apimapper/workflows.js.map +1 -1
  136. package/dist/modules/apimapper/yootheme-binding.d.ts +35 -0
  137. package/dist/modules/apimapper/yootheme-binding.js +267 -0
  138. package/dist/modules/apimapper/yootheme-binding.js.map +1 -0
  139. package/dist/platform/index.d.ts +56 -0
  140. package/dist/platform/index.js +158 -2
  141. package/dist/platform/index.js.map +1 -1
  142. package/dist/proxy/bridge.d.ts +35 -0
  143. package/dist/proxy/bridge.js +129 -0
  144. package/dist/proxy/bridge.js.map +1 -0
  145. package/dist/proxy/mode.d.ts +9 -0
  146. package/dist/proxy/mode.js +20 -0
  147. package/dist/proxy/mode.js.map +1 -0
  148. package/dist/setup/detect-clients.d.ts +40 -1
  149. package/dist/setup/detect-clients.js +148 -1
  150. package/dist/setup/detect-clients.js.map +1 -1
  151. package/dist/setup/probe-auth.d.ts +51 -0
  152. package/dist/setup/probe-auth.js +141 -0
  153. package/dist/setup/probe-auth.js.map +1 -0
  154. package/dist/setup/probe-handshake.js +40 -7
  155. package/dist/setup/probe-handshake.js.map +1 -1
  156. package/dist/setup/remove-config.d.ts +8 -0
  157. package/dist/setup/remove-config.js +145 -0
  158. package/dist/setup/remove-config.js.map +1 -0
  159. package/dist/setup/uninstall.d.ts +34 -0
  160. package/dist/setup/uninstall.js +147 -0
  161. package/dist/setup/uninstall.js.map +1 -0
  162. package/dist/setup-cli.d.ts +16 -0
  163. package/dist/setup-cli.js +63 -1
  164. package/dist/setup-cli.js.map +1 -1
  165. package/dist/sites/loader.d.ts +48 -0
  166. package/dist/sites/loader.js +134 -0
  167. package/dist/sites/loader.js.map +1 -0
  168. package/dist/sites/schema.d.ts +69 -0
  169. package/dist/sites/schema.js +71 -0
  170. package/dist/sites/schema.js.map +1 -0
  171. package/dist/sites/secret-resolver.d.ts +47 -0
  172. package/dist/sites/secret-resolver.js +150 -0
  173. package/dist/sites/secret-resolver.js.map +1 -0
  174. package/dist/skill-instructions.d.ts +14 -1
  175. package/dist/skill-instructions.js +35 -6
  176. package/dist/skill-instructions.js.map +1 -1
  177. package/dist/transports/stdio.js +4 -4
  178. package/dist/transports/stdio.js.map +1 -1
  179. package/dist/uninstall-skill.d.ts +27 -0
  180. package/dist/uninstall-skill.js +89 -0
  181. package/dist/uninstall-skill.js.map +1 -0
  182. package/docs/architecture.md +21 -21
  183. package/docs/customgraph-internal-migration.md +4 -4
  184. package/docs/security.md +2 -21
  185. package/docs/tools.md +40 -12
  186. package/manifest.json +77 -79
  187. package/package.json +69 -65
  188. package/skills/apimapper/SKILL.md +128 -7
  189. package/skills/apimapper/reference/conditional-style-multi-items.md +114 -0
  190. package/skills/apimapper/reference/dynamize-existing-layout.md +158 -0
  191. package/skills/apimapper/reference/jmespath-cookbook.md +241 -0
  192. package/skills/apimapper/reference/jmespath-pitfalls.md +189 -0
  193. package/skills/apimapper/reference/joomla.md +1 -1
  194. package/skills/apimapper/reference/library-template-discovery.md +65 -0
  195. package/skills/apimapper/reference/merge-two-sources-on-key.md +204 -0
  196. package/skills/apimapper/reference/oauth.md +143 -52
  197. package/skills/apimapper/reference/troubleshooting.md +22 -2
  198. package/skills/apimapper/reference/yootheme-source-to-builder-handoff.md +348 -0
  199. package/skills/apimapper/reference/yootheme.md +75 -44
  200. package/dist/auth/oauth-provider.d.ts +0 -68
  201. package/dist/auth/oauth-provider.js +0 -232
  202. package/dist/auth/oauth-provider.js.map +0 -1
  203. package/dist/server-http.d.ts +0 -22
  204. package/dist/server-http.js +0 -159
  205. package/dist/server-http.js.map +0 -1
  206. package/dist/transports/http.d.ts +0 -29
  207. package/dist/transports/http.js +0 -267
  208. package/dist/transports/http.js.map +0 -1
@@ -1,8 +1,114 @@
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
+ import { withOverflowFooter } from "./list-footer.js";
8
+ import { normalizeResourceIdFields } from "./resource-id.js";
9
+ /**
10
+ * Wave-6 R1 (2026-05-29): Treat any non-"none" / non-empty auth_type as
11
+ * "credential required". The set is intentionally inclusive — the worker
12
+ * library uses both canonical (`oauth2_code`, `api_key`, `bearer`, `basic`)
13
+ * and legacy (`oauth`, `apiKey`) values; we don't try to enumerate them.
14
+ */
15
+ function templateRequiresCredential(authType) {
16
+ if (typeof authType !== "string")
17
+ return false;
18
+ const t = authType.toLowerCase().trim();
19
+ if (t === "" || t === "none")
20
+ return false;
21
+ return true;
22
+ }
23
+ /**
24
+ * Wave-6 R1: read the provider slug from a library template. Tolerates
25
+ * both top-level `provider` and `auth_scheme.provider`.
26
+ */
27
+ function templateProvider(tpl) {
28
+ if (typeof tpl.provider === "string" && tpl.provider !== "")
29
+ return tpl.provider;
30
+ const auth = tpl.auth_scheme;
31
+ if (auth && typeof auth === "object") {
32
+ const p = auth.provider;
33
+ if (typeof p === "string" && p !== "")
34
+ return p;
35
+ }
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Wave-6 R1: read auth_type from a library template, normalising the
40
+ * pre-2.0 alias `oauth → oauth2_code`.
41
+ */
42
+ function templateAuthType(tpl) {
43
+ let t = tpl.auth_type;
44
+ if (typeof t !== "string" || t === "") {
45
+ const auth = tpl.auth_scheme;
46
+ if (auth && typeof auth === "object") {
47
+ const at = auth.type;
48
+ if (typeof at === "string" && at !== "")
49
+ t = at;
50
+ }
51
+ }
52
+ if (typeof t !== "string" || t === "")
53
+ return undefined;
54
+ if (t === "oauth")
55
+ return "oauth2_code";
56
+ if (t === "apiKey")
57
+ return "api_key";
58
+ return t;
59
+ }
60
+ /**
61
+ * Wave-6 R1: credential lookup helpers shared with credentials.ts. Reads
62
+ * `oauth_provider` first, then `provider`.
63
+ */
64
+ function credentialProviderSlug(c) {
65
+ return c.oauth_provider ?? c.provider ?? undefined;
66
+ }
67
+ /**
68
+ * F51 (2026-06-10): reduce a provider slug to its OAuth credential FAMILY.
69
+ *
70
+ * One OAuth login backs many product-specific templates: a single Google
71
+ * credential serves google-sheets / google-drive / google-docs / gmail, a
72
+ * single Meta credential serves instagram / facebook, etc. Library templates
73
+ * ship a product-specific provider slug ("google-sheets"), but the stored
74
+ * credential carries the family slug ("google"). Exact-string matching then
75
+ * yields 0 candidates and the agent gets a false `credential_required` —
76
+ * a weaker agent would launch a redundant OAuth flow (the Run-C benchmark
77
+ * finding). Family matching closes that gap.
78
+ *
79
+ * Strategy is generic + prefix-based: strip a known family PREFIX, plus a
80
+ * small alias table for families whose members don't share a prefix (Meta).
81
+ * Returns the input lower-cased + trimmed when no family rule applies, so a
82
+ * provider with no family still equals itself (a no-op for non-family APIs).
83
+ */
84
+ const PROVIDER_FAMILY_ALIASES = {
85
+ // Meta family members don't share a common prefix.
86
+ instagram: "meta",
87
+ facebook: "meta",
88
+ "facebook-pages": "meta",
89
+ whatsapp: "meta",
90
+ // Microsoft family.
91
+ outlook: "microsoft",
92
+ "microsoft-teams": "microsoft",
93
+ onedrive: "microsoft",
94
+ };
95
+ function providerFamily(slug) {
96
+ if (typeof slug !== "string")
97
+ return undefined;
98
+ const s = slug.toLowerCase().trim();
99
+ if (s === "")
100
+ return undefined;
101
+ if (s in PROVIDER_FAMILY_ALIASES)
102
+ return PROVIDER_FAMILY_ALIASES[s];
103
+ // Prefix families: "google-sheets" → "google", "microsoft-graph" → "microsoft".
104
+ const FAMILY_PREFIXES = ["google", "microsoft", "meta", "amazon", "zoho"];
105
+ for (const prefix of FAMILY_PREFIXES) {
106
+ if (s === prefix || s.startsWith(prefix + "-") || s.startsWith(prefix + "_")) {
107
+ return prefix;
108
+ }
109
+ }
110
+ return s;
111
+ }
6
112
  /** F-14: best-effort JSON.stringify length. Falls back to 0 on cycle. */
7
113
  function computeCatalogSize(catalog) {
8
114
  try {
@@ -117,12 +223,7 @@ export function registerLibraryTools(server) {
117
223
  params.set("limit", String(limit));
118
224
  const r = await request(`/library?${params.toString()}`);
119
225
  if (!r.success) {
120
- return errorResult({
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
- });
226
+ return restErrorResult(r, { category, provider, activated, search, page, limit }, { message: "library list failed" });
126
227
  }
127
228
  // F-SEED-02: PHP returns `connections`; tolerate legacy `items`.
128
229
  let items = Array.isArray(r.data?.connections)
@@ -142,7 +243,9 @@ export function registerLibraryTools(server) {
142
243
  map: mapLibraryRow,
143
244
  compactMap: compactLibraryRow,
144
245
  header: (n) => `${n} library items (page ${page})`,
145
- footer: LIST_NEXT_STEPS,
246
+ // Minor (2026-06-10): overflow-aware footer (see list-footer.ts) — the
247
+ // full catalogue easily exceeds the 21-row compaction threshold.
248
+ footer: withOverflowFooter(LIST_NEXT_STEPS, items.length, "library items"),
146
249
  });
147
250
  });
148
251
  // ── apimapper_library_categories ───────────────────────────────────
@@ -163,11 +266,7 @@ export function registerLibraryTools(server) {
163
266
  }, async () => {
164
267
  const r = await request("/library/categories");
165
268
  if (!r.success) {
166
- return errorResult({
167
- message: r.error ?? "library categories failed",
168
- code: r.errorCode ?? (r.status ? String(r.status) : undefined),
169
- suggestion: hintFor(r.errorCode),
170
- });
269
+ return restErrorResult(r, undefined, { message: "library categories failed" });
171
270
  }
172
271
  const cats = Array.isArray(r.data?.categories) ? r.data.categories : [];
173
272
  const rows = cats
@@ -199,7 +298,14 @@ export function registerLibraryTools(server) {
199
298
  // ── apimapper_library_featured ─────────────────────────────────────
200
299
  server.registerTool("apimapper_library_featured", {
201
300
  title: "List Featured Library Items",
202
- description: "Fetch curated/featured library items shown on the catalog landing." +
301
+ description: "List the curated, featured connection templates the MANDATORY first " +
302
+ "call before building any new API integration. Use this to discover whether " +
303
+ "a ready-made template (with OAuth wizard, auto-header detection, and a " +
304
+ "curated YOOtheme schema) already exists for the API you need. " +
305
+ "Keywords: featured, recommended, starter, popular templates, pre-built, catalog landing. " +
306
+ "When NOT to use: to search the WHOLE catalog by name use apimapper_library_list; " +
307
+ "to read one template's full contract use apimapper_library_connection_detail; " +
308
+ "only fall back to apimapper_connection_create when no template matches." +
203
309
  "\n\nExample:\n apimapper_library_featured({})",
204
310
  inputSchema: {
205
311
  limit: z.number().min(1).max(100).default(12).describe("Max items (1-100)"),
@@ -209,24 +315,30 @@ export function registerLibraryTools(server) {
209
315
  const params = new URLSearchParams({ limit: String(limit) });
210
316
  const r = await request(`/library/featured?${params.toString()}`);
211
317
  if (!r.success) {
212
- return errorResult({
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
- });
318
+ return restErrorResult(r, { limit }, { message: "library featured failed" });
218
319
  }
219
- const items = Array.isArray(r.data?.connections)
320
+ const raw = Array.isArray(r.data?.connections)
220
321
  ? r.data.connections
221
322
  : Array.isArray(r.data?.items)
222
323
  ? r.data.items
223
324
  : [];
325
+ // Minor (2026-06-10) — the endpoint sometimes returns the WHOLE
326
+ // catalogue mislabelled "featured". Curate honestly:
327
+ // - if any row carries a truthy `featured` flag, keep ONLY those;
328
+ // - otherwise fall back to a top-N popularity slice (N ≤ 5) and SAY so
329
+ // in the header instead of claiming the catalogue is the curated set.
330
+ const curated = raw.filter((c) => c.featured === true);
331
+ const usingFallback = curated.length === 0;
332
+ const FALLBACK_N = Math.min(5, limit);
333
+ const items = usingFallback ? raw.slice(0, FALLBACK_N) : curated;
224
334
  return tableResult(toRows(items), {
225
335
  columns: LIBRARY_TABLE_COLUMNS,
226
336
  compactColumns: LIBRARY_COMPACT_COLUMNS,
227
337
  map: mapLibraryRow,
228
338
  compactMap: compactLibraryRow,
229
- header: (n) => `${n} featured library items`,
339
+ header: (n) => usingFallback
340
+ ? `${n} library items (no curated featured set — showing top ${n} by popularity)`
341
+ : `${n} featured library items`,
230
342
  footer: "Subset of the full catalog — NOT the complete library. " +
231
343
  "Use apimapper_library_list({}) for the entire catalog.\n" +
232
344
  LIST_NEXT_STEPS,
@@ -235,7 +347,13 @@ export function registerLibraryTools(server) {
235
347
  // ── apimapper_library_popular ──────────────────────────────────────
236
348
  server.registerTool("apimapper_library_popular", {
237
349
  title: "List Popular Library Items",
238
- description: "Fetch most-activated library items." +
350
+ description: "List the most-activated connection templates across all users — a " +
351
+ "social-proof ranking of what other people actually wire up. Use to " +
352
+ "discover proven, in-demand integrations when you are exploring options. " +
353
+ "Keywords: popular, trending, most-used, most-activated, top templates. " +
354
+ "When NOT to use: for the editorial short-list use apimapper_library_featured; " +
355
+ "to search by API name use apimapper_library_list; to see what THIS user has " +
356
+ "already activated use apimapper_library_activated." +
239
357
  "\n\nExample:\n apimapper_library_popular({})",
240
358
  inputSchema: {
241
359
  limit: z.number().min(1).max(100).default(12).describe("Max items (1-100)"),
@@ -245,12 +363,7 @@ export function registerLibraryTools(server) {
245
363
  const params = new URLSearchParams({ limit: String(limit) });
246
364
  const r = await request(`/library/popular?${params.toString()}`);
247
365
  if (!r.success) {
248
- return errorResult({
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
- });
366
+ return restErrorResult(r, { limit }, { message: "library popular failed" });
254
367
  }
255
368
  const items = Array.isArray(r.data?.connections)
256
369
  ? r.data.connections
@@ -278,27 +391,63 @@ export function registerLibraryTools(server) {
278
391
  server.registerTool("apimapper_library_catalog", {
279
392
  title: "Get Full Library Catalog",
280
393
  description: "Fetch the entire library catalog (all categories + items + featured/popular in one shot). " +
281
- "Heavy — use filtered endpoints for narrower queries." +
282
- "\n\nExample:\n apimapper_library_catalog({})",
283
- inputSchema: {},
394
+ "Heavy — pass `template_id` to narrow the response to a single template's full record " +
395
+ "(skips the 8000-char truncation cap) or call apimapper_library_connection_detail for the " +
396
+ "rendered detail view." +
397
+ "\n\nExamples:\n" +
398
+ " apimapper_library_catalog({}) // full catalog, may truncate at 8000 chars\n" +
399
+ " apimapper_library_catalog({ template_id: 'google-sheets' }) // single template, full record",
400
+ inputSchema: {
401
+ // Wave-5 F12 (2026-05-29): cold-AI #3 hit 8k truncation on the
402
+ // unfiltered catalog and could not retrieve a target template's
403
+ // computed_fields / default_params. Filtering server-side via the
404
+ // same /library/connections/{id} endpoint that library_connection_detail
405
+ // uses gives the AI the FULL record without exceeding maxChars.
406
+ template_id: z
407
+ .string()
408
+ .optional()
409
+ .describe("Filter the catalog response to ONE template's full record (e.g. 'google-sheets'). " +
410
+ "Returns the connection template payload directly under the `connection` key " +
411
+ "(same shape as /library/connections/{id}) — skips the 8000-char truncation cap " +
412
+ "that applies to the unfiltered multi-section response."),
413
+ },
284
414
  annotations: readOnly({ title: "Get Library Catalog", openWorld: true }),
285
- }, async () => {
415
+ }, async ({ template_id }) => {
416
+ // Wave-5 F12: single-template filter routes through the per-connection
417
+ // endpoint to skip the maxChars:8000 truncation that swallows mid-size
418
+ // records in the multi-section catalog response.
419
+ if (typeof template_id === "string" && template_id !== "") {
420
+ const r = await request(`/library/connections/${encodeURIComponent(template_id)}`);
421
+ if (!r.success) {
422
+ return restErrorResult(r, { template_id }, { message: "library catalog (filtered) failed" });
423
+ }
424
+ const connection = unwrapEntity(r.data, "connection") ?? {};
425
+ // DATA-LOW (Wave-B 2026-06-03): the data-level diagnostics key is named
426
+ // `meta` (not `_meta`) to avoid colliding in naming with the
427
+ // protocol-level `_meta` on the result object. This is text content, so
428
+ // the rename is harmless to clients but removes maintainer confusion.
429
+ return formatResult({
430
+ connection,
431
+ meta: {
432
+ filtered_by: { template_id },
433
+ source_endpoint: `/library/connections/${template_id}`,
434
+ },
435
+ }, false, { maxChars: 16000 });
436
+ }
286
437
  const r = await request("/library/catalog");
287
438
  if (!r.success) {
288
- return errorResult({
289
- message: r.error ?? "library catalog failed",
290
- code: r.errorCode ?? (r.status ? String(r.status) : undefined),
291
- suggestion: hintFor(r.errorCode),
292
- });
439
+ return restErrorResult(r, undefined, { message: "library catalog failed" });
293
440
  }
294
441
  const catalog = r.data?.catalog ?? r.data ?? {};
295
442
  // F-14: surface payload size + item count so the AI can decide whether
296
443
  // to call this heavy tool again, or call a narrower endpoint.
297
444
  const sizeBytes = computeCatalogSize(catalog);
298
445
  const itemCount = computeCatalogItemCount(catalog, r.data?.total);
446
+ // DATA-LOW (Wave-B 2026-06-03): `meta` (not `_meta`) — see the filtered
447
+ // branch above. Avoids naming-collision with the protocol-level `_meta`.
299
448
  return formatResult({
300
449
  ...catalog,
301
- _meta: {
450
+ meta: {
302
451
  size_bytes: sizeBytes,
303
452
  item_count: itemCount,
304
453
  },
@@ -316,12 +465,7 @@ export function registerLibraryTools(server) {
316
465
  }, async ({ id }) => {
317
466
  const r = await request(`/library/connections/${encodeURIComponent(id)}`);
318
467
  if (!r.success) {
319
- return errorResult({
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
- });
468
+ return restErrorResult(r, { id }, { message: "library connection detail failed" });
325
469
  }
326
470
  if (!r.data || (typeof r.data === "object" && Object.keys(r.data).length === 0)) {
327
471
  return errorResult({
@@ -339,18 +483,21 @@ export function registerLibraryTools(server) {
339
483
  // ── apimapper_library_activated ────────────────────────────────────
340
484
  server.registerTool("apimapper_library_activated", {
341
485
  title: "List Activated Library Items",
342
- description: "List library items the user has activated (= has a connection for)." +
486
+ description: "List the library templates THIS site has already activated (each one " +
487
+ "has a backing connection). Use to check what is already wired up before " +
488
+ "activating a duplicate, or to find the connection that came from a template. " +
489
+ "Keywords: activated, installed, my templates, already connected, in use. " +
490
+ "When NOT to use: to browse templates you could activate use " +
491
+ "apimapper_library_featured / apimapper_library_list; to activate one use " +
492
+ "apimapper_library_activate; to deactivate (deletes the connection) use " +
493
+ "apimapper_library_deactivate." +
343
494
  "\n\nExample:\n apimapper_library_activated({})",
344
495
  inputSchema: {},
345
496
  annotations: readOnly({ title: "List Activated Library Connections", openWorld: true }),
346
497
  }, async () => {
347
498
  const r = await request("/library/activated");
348
499
  if (!r.success) {
349
- return errorResult({
350
- message: r.error ?? "library activated failed",
351
- code: r.errorCode ?? (r.status ? String(r.status) : undefined),
352
- suggestion: hintFor(r.errorCode),
353
- });
500
+ return restErrorResult(r, undefined, { message: "library activated failed" });
354
501
  }
355
502
  // F-A4-01: PHP returns `connections`; tolerate legacy `items`.
356
503
  const items = (Array.isArray(r.data?.connections)
@@ -371,52 +518,260 @@ export function registerLibraryTools(server) {
371
518
  });
372
519
  });
373
520
  // ── apimapper_library_activate ─────────────────────────────────────
521
+ // Wave-6 R1 (2026-05-29): For auth-protected templates (most APIs), the
522
+ // tool now auto-resolves `credential_id` when the user has exactly ONE
523
+ // OAuth credential matching the template's provider — and fails loudly
524
+ // (structured `credential_required` / `credential_ambiguous`) instead of
525
+ // landing a broken connection (empty endpoint, auth_type:none) when no
526
+ // unique match exists. Cold-AI #4 root cause: agents called
527
+ // `library_activate({id, extra_fields})` without a `credential_id`, the
528
+ // server-side defensive hydration silently fell through, and the resulting
529
+ // connection produced empty data the AI couldn't debug from the response.
374
530
  server.registerTool("apimapper_library_activate", {
375
531
  title: "Activate Library Template",
376
- description: "Activate a library template — creates a new connection. REST contract: " +
377
- "`extra_fields` (template placeholder values) + optional `library_connection` (template metadata override)." +
378
- "\n\nExample:\n apimapper_library_activate({ id: 'pexels', extra_fields: { api_key: 'a1b2c3d4...' } })",
532
+ description: "Activate a library template — creates a new connection. " +
533
+ "\n\n**FOR AUTH-PROTECTED TEMPLATES (most APIs incl. Google Sheets, Notion, " +
534
+ "Airtable, Pexels, OpenWeatherMap, Calendly, GitHub):** you MUST link a " +
535
+ "credential. The tool auto-links when exactly ONE matching credential exists " +
536
+ "for the template's provider; otherwise it fails with `credential_required` " +
537
+ "or `credential_ambiguous`. Pre-flight: call " +
538
+ "`apimapper_credential_list({})` and confirm a credential exists for the " +
539
+ "provider — if not, create one via `apimapper_oauth_authorize_begin` (OAuth) " +
540
+ "or `apimapper_advanced({tool:'apimapper_credential_create',arguments:{…}})` " +
541
+ "(api_key/bearer). " +
542
+ "\n\nREST contract: `extra_fields` (template placeholder values) + optional " +
543
+ "`library_connection` (template metadata override) + optional `credential_id`." +
544
+ "\n\n⚠️ Template fields passed at the TOP LEVEL of the call are silently " +
545
+ "ignored (the schema strips unknown keys) — they MUST be nested under " +
546
+ "`extra_fields` (F186)." +
547
+ "\n\n**REQUIRED extra_fields per template (the install 422s with " +
548
+ "`incomplete_setup` if any are missing — pass them up front):**\n" +
549
+ " - google-sheets: `spreadsheet_id` (bare ID or Drive/Sheets URL) AND " +
550
+ "`range` (A1 notation, e.g. 'A1:Z1000'; tab-qualified 'Sheet-Tab!A:Z' or " +
551
+ "open-ended 'A:Z' forms are valid; the tab name is OPTIONAL).\n" +
552
+ " - google-docs: `document_id`.\n" +
553
+ " - airtable: `base_id` AND `table_name`.\n" +
554
+ " - strapi: `site_url` AND `content_type`.\n" +
555
+ " (Call apimapper_library_connection_detail({ id }) to read the exact " +
556
+ "required/optional extra_fields + examples for ANY template.)\n" +
557
+ "\nExamples:\n" +
558
+ " apimapper_library_activate({ id: 'pexels', credential_id: 'cred_pexels', extra_fields: { api_key: '…' } })\n" +
559
+ " apimapper_library_activate({ id: 'google-sheets', extra_fields: { spreadsheet_id: '1AbC…', range: 'A1:Z1000' } }) // auto-links the unique Google OAuth credential",
379
560
  inputSchema: {
380
561
  id: z.string().describe("Library item ID. Use apimapper_library_list to find."),
381
- credential_id: z.string().optional().describe("Credential ID to link (required for auth-protected templates)"),
562
+ credential_id: z
563
+ .string()
564
+ .optional()
565
+ .describe("Credential ID. REQUIRED for auth-protected templates unless exactly " +
566
+ "one matching credential exists (then auto-linked). Use " +
567
+ "apimapper_credential_list({}) to find one, or " +
568
+ "apimapper_oauth_authorize_begin({provider}) to create one."),
382
569
  extra_fields: z
383
570
  .record(z.string(), z.unknown())
384
571
  .optional()
385
- .describe('Template placeholder values (e.g., {"spreadsheet_id":"...","user_uri":"..."}). REST key: extra_fields.'),
572
+ .describe('Template placeholder values. REST key: extra_fields. Each library ' +
573
+ 'template declares its own required/optional keys — read them via ' +
574
+ 'apimapper_library_connection_detail({ id }). Common required sets:\n' +
575
+ ' • google-sheets: { spreadsheet_id, range }. spreadsheet_id may be the ' +
576
+ 'bare ID or a full Drive/Sheets URL ' +
577
+ '(https://docs.google.com/spreadsheets/d/<ID>/edit) — a pasted URL is ' +
578
+ 'auto-extracted to the bare ID (A5). `range` is A1 notation, ' +
579
+ 'e.g. "A1:Z1000"; a tab-qualified "Sheet-Tab!A:Z" or open-ended "A:Z" ' +
580
+ 'form is also valid and the tab name is OPTIONAL.\n' +
581
+ ' • airtable: { base_id, table_name }. • strapi: { site_url, content_type }. ' +
582
+ '• google-docs: { document_id }.'),
386
583
  library_connection: z
387
584
  .record(z.string(), z.unknown())
388
585
  .optional()
389
586
  .describe('Optional template metadata override (rename connection, override base_url, etc.). ' +
390
587
  'REST key: library_connection.'),
588
+ // F1 (2026-06-08): one credential can back MULTIPLE connections (one per
589
+ // resource — e.g. several Airtable bases). The backend reuses an
590
+ // existing connection only when the SAME resource (extra_fields) is
591
+ // requested AND that connection is healthy. Pass `force_new: true` to
592
+ // skip reuse entirely and always create a fresh connection (a distinct
593
+ // resource on the same credential). REST key: force_new.
594
+ force_new: z
595
+ .boolean()
596
+ .optional()
597
+ .describe("Force creation of a NEW connection even if a matching one exists " +
598
+ "(default: false → the backend reuses a healthy connection for the " +
599
+ "same resource). Set true when wiring a second/different resource " +
600
+ "on the same credential. REST key: force_new."),
391
601
  },
392
602
  // W1.26 (IA-1): activate is idempotent — re-activating the same template
393
- // yields the same connection (PHP returns the existing row instead of
394
- // duplicating). mutating() is the correct semantic class.
603
+ // for the same resource yields the same connection (the backend returns
604
+ // the existing row instead of duplicating). mutating() is the correct
605
+ // semantic class.
395
606
  annotations: mutating({ title: "Activate Library Connection", openWorld: true }),
396
- }, async ({ id, credential_id, extra_fields, library_connection }) => {
607
+ }, async ({ id, credential_id, extra_fields, library_connection, force_new }) => {
608
+ // Wave-6 R1 (2026-05-29): Credential auto-resolution gate.
609
+ // When `credential_id` is missing AND the template has a non-none auth_type,
610
+ // fetch the template + credential list, try to auto-link a unique match,
611
+ // and fail-loud with a structured error when 0 or >1 candidates exist.
612
+ // The auto-link logic mirrors `apimapper_oauth_authorize_begin` (which
613
+ // resolves an OAuth credential by provider) but applies to ANY auth_type
614
+ // (api_key, bearer, basic_auth, oauth2_*).
615
+ let resolvedCredentialId = credential_id;
616
+ let autoLinkedFrom;
617
+ if (!resolvedCredentialId) {
618
+ // Fetch template metadata to discover its auth scheme.
619
+ const tr = await request(`/library/connections/${encodeURIComponent(id)}`);
620
+ if (tr.success && tr.data) {
621
+ const tplRaw = unwrapEntity(tr.data, "connection") ?? {};
622
+ const tplAuthType = templateAuthType(tplRaw);
623
+ const tplProvider = templateProvider(tplRaw);
624
+ if (templateRequiresCredential(tplAuthType)) {
625
+ // Fetch credentials sanitised — we only need id/name/provider/auth_type.
626
+ const cr = await request("/credentials", {}, { sanitize: true });
627
+ if (!cr.success) {
628
+ return restErrorResult(cr, { id, template_provider: tplProvider, template_auth_type: tplAuthType }, {
629
+ message: "credential lookup failed during activation",
630
+ // Per-site nuance: a domain code fallback + a fixed
631
+ // activation-recovery suggestion override hintFor().
632
+ code: "credential_lookup_failed",
633
+ suggestion: "Retry, or pass an explicit `credential_id`. " +
634
+ "Use apimapper_credential_list({}) to find one.",
635
+ });
636
+ }
637
+ const allCreds = Array.isArray(cr.data?.credentials) ? cr.data.credentials : [];
638
+ // Match by provider first; if template has no provider, fall back to auth_type.
639
+ let candidates = tplProvider
640
+ ? allCreds.filter((c) => credentialProviderSlug(c)?.toLowerCase() === tplProvider.toLowerCase())
641
+ : allCreds.filter((c) => c.auth_type === tplAuthType);
642
+ // F51 (2026-06-10): when an EXACT provider match yields nothing,
643
+ // fall back to provider-FAMILY matching (google-sheets → google).
644
+ // Exact match always wins (we only reach here on 0 exact hits), so
645
+ // a Sheets-specific credential is preferred over a generic Google
646
+ // one when both exist. familyMatched flags the response so the
647
+ // agent can confirm the auto-link reused an existing credential
648
+ // instead of demanding a fresh OAuth flow.
649
+ let familyMatched = false;
650
+ if (candidates.length === 0 && tplProvider) {
651
+ const tplFamily = providerFamily(tplProvider);
652
+ if (tplFamily) {
653
+ candidates = allCreds.filter((c) => providerFamily(credentialProviderSlug(c)) === tplFamily);
654
+ familyMatched = candidates.length > 0;
655
+ }
656
+ }
657
+ if (candidates.length === 0) {
658
+ const providerHint = tplProvider ?? tplAuthType ?? "the template's provider";
659
+ return errorResult({
660
+ message: `Template "${id}" requires authentication (${tplAuthType}) but no matching ` +
661
+ `credential exists for "${providerHint}".`,
662
+ code: "credential_required",
663
+ suggestion: `Create a credential first:\n` +
664
+ ` • OAuth: apimapper_oauth_authorize_begin({ provider: "${tplProvider ?? "<provider>"}" })\n` +
665
+ ` • api_key/bearer/basic: apimapper_advanced({ tool: "apimapper_credential_create", arguments: { auth_type: "${tplAuthType}", ... } })\n` +
666
+ `Then retry apimapper_library_activate with the new credential_id.`,
667
+ details: {
668
+ id,
669
+ template_provider: tplProvider,
670
+ template_auth_type: tplAuthType,
671
+ available_credentials: allCreds.map((c) => ({
672
+ id: c.id,
673
+ name: c.name,
674
+ provider: credentialProviderSlug(c),
675
+ auth_type: c.auth_type,
676
+ })),
677
+ },
678
+ });
679
+ }
680
+ if (candidates.length > 1) {
681
+ return errorResult({
682
+ message: `Template "${id}" requires authentication and ${candidates.length} candidate ` +
683
+ `credentials exist for "${tplProvider ?? tplAuthType}". Pass an explicit credential_id.`,
684
+ code: "credential_ambiguous",
685
+ suggestion: "Re-call apimapper_library_activate with one of the credential_ids below " +
686
+ "(see `details.candidates`).",
687
+ details: {
688
+ id,
689
+ template_provider: tplProvider,
690
+ template_auth_type: tplAuthType,
691
+ candidates: candidates.map((c) => ({
692
+ id: c.id,
693
+ name: c.name,
694
+ provider: credentialProviderSlug(c),
695
+ auth_type: c.auth_type,
696
+ })),
697
+ },
698
+ });
699
+ }
700
+ // Exactly one match → auto-link.
701
+ const picked = candidates[0];
702
+ resolvedCredentialId = picked.id;
703
+ autoLinkedFrom = {
704
+ provider: credentialProviderSlug(picked),
705
+ credentialName: picked.name,
706
+ familyMatched,
707
+ };
708
+ }
709
+ }
710
+ // Note: if template fetch fails or auth_type is "none"/missing, we
711
+ // fall through to the original (no credential) activation path. The
712
+ // server-side validator will still reject missing required extra_fields.
713
+ }
397
714
  const body = {};
398
- if (credential_id)
399
- body.credential_id = credential_id;
715
+ if (resolvedCredentialId)
716
+ body.credential_id = resolvedCredentialId;
717
+ // A5 (2026-06-10): normalize a pasted Drive/Sheets URL in
718
+ // extra_fields.spreadsheet_id to the bare ID before it hits the activate
719
+ // endpoint — the {{spreadsheet_id}} placeholder needs the bare id, and
720
+ // the cold-agent customer supplies only a URL.
400
721
  if (extra_fields)
401
- body.extra_fields = extra_fields;
722
+ body.extra_fields = normalizeResourceIdFields(extra_fields);
402
723
  if (library_connection)
403
724
  body.library_connection = library_connection;
725
+ // F1 (2026-06-08): only send force_new when explicitly true — keep the
726
+ // request body minimal so a default activate stays byte-identical to the
727
+ // pre-F1 wire shape (the backend defaults to reuse).
728
+ if (force_new === true)
729
+ body.force_new = true;
404
730
  const r = await request(`/library/${encodeURIComponent(id)}/activate`, {
405
731
  method: "POST",
406
732
  body: JSON.stringify(body),
407
733
  });
408
734
  if (!r.success) {
409
- return errorResult({
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
- });
735
+ // F186 (2026-06-12): the most common cause of a missing-fields 422
736
+ // WITHOUT extra_fields in the call is that the caller passed the
737
+ // template fields at the TOP LEVEL — the schema layer silently
738
+ // stripped them. Name the fix explicitly instead of letting the
739
+ // agent re-guess.
740
+ const missingFields = /missing required field/i.test(String(r.error ?? ""));
741
+ if (missingFields && !extra_fields) {
742
+ return restErrorResult(r, { id }, {
743
+ message: "library activate failed",
744
+ suggestion: "If you passed the template fields (e.g. spreadsheet_id, range) as " +
745
+ "top-level arguments they were ignored — nest them under " +
746
+ "extra_fields: apimapper_library_activate({ id, extra_fields: { … } }).",
747
+ });
748
+ }
749
+ return restErrorResult(r, { id }, { message: "library activate failed" });
415
750
  }
416
- // F-A4-03: PHP returns `{success, connection: {id, ...}, reused?, flow?}`.
417
- // Read the nested `connection` directly (we DO NOT use unwrapEntity here
418
- // because its fall-back to `data` would surface the wrong `id` if the
419
- // envelope key is missing).
751
+ // F1 (2026-06-08): the backend is now the SOLE authority on reuse vs.
752
+ // create vs. heal. One credential can back MANY connections (one per
753
+ // resource); the backend reuses a connection only when the SAME resource
754
+ // is requested AND that connection is healthy, and it NEVER mutates a
755
+ // healthy row. It reports its decision via three booleans on the response:
756
+ //
757
+ // reused:true, mutated:false → existing healthy connection
758
+ // returned unchanged.
759
+ // reused:true, healed:true, mutated:true → existing connection was
760
+ // corrupt; the backend repaired
761
+ // it in place.
762
+ // (no reused / reused:false) → a brand-new connection was
763
+ // created (new resource or
764
+ // force_new:true).
765
+ //
766
+ // The MCP layer no longer re-derives a mismatch and rejects after the
767
+ // fact (the old `connection_reused_with_stale_state` path assumed a
768
+ // mutation that the backend no longer performs). We simply INTERPRET the
769
+ // backend's booleans into a clear, human-readable status string.
770
+ //
771
+ // F-A4-03: PHP returns `{success, connection: {id, ...}, reused?, healed?,
772
+ // mutated?, flow?}`. Read the nested `connection` directly (we DO NOT use
773
+ // unwrapEntity here because its fall-back to `data` would surface the
774
+ // wrong `id` if the envelope key is missing).
420
775
  const dataObj = r.data && typeof r.data === "object" ? r.data : {};
421
776
  const conn = (dataObj.connection && typeof dataObj.connection === "object")
422
777
  ? dataObj.connection
@@ -426,16 +781,61 @@ export function registerLibraryTools(server) {
426
781
  (typeof dataObj.connectionId === "string" ? dataObj.connectionId : undefined) ||
427
782
  null;
428
783
  const reused = typeof dataObj.reused === "boolean" ? dataObj.reused : undefined;
784
+ const healed = typeof dataObj.healed === "boolean" ? dataObj.healed : undefined;
785
+ const mutated = typeof dataObj.mutated === "boolean" ? dataObj.mutated : undefined;
429
786
  const flowSummary = dataObj.flow && typeof dataObj.flow === "object"
430
787
  ? dataObj.flow
431
788
  : undefined;
789
+ // F1: derive a single status + note from the backend's decision booleans.
790
+ let status;
791
+ let note;
792
+ if (reused === true && healed === true) {
793
+ status = "reused_healed";
794
+ note =
795
+ "Reused (healed): an existing connection for this resource was corrupt " +
796
+ "and the backend repaired it in place. No duplicate was created.";
797
+ }
798
+ else if (reused === true) {
799
+ status = "reused";
800
+ note =
801
+ "Reused (unchanged): a healthy connection already existed for this " +
802
+ "resource and was returned as-is — no changes were made. Pass " +
803
+ "force_new:true to create a separate connection for a different resource.";
804
+ }
805
+ else {
806
+ status = "activated";
807
+ note = "Connection created; use apimapper_connection_list to find the new id.";
808
+ }
432
809
  return formatResult({
433
810
  activated: true,
811
+ status,
434
812
  id,
435
813
  connection_id: connectionId,
436
- reused,
814
+ // Wave-6 R1 — surface auto-link decision when the tool resolved
815
+ // the credential for the agent. Helps the AI confirm which
816
+ // credential ended up wired in (vs. a previously stored one).
817
+ ...(autoLinkedFrom
818
+ ? {
819
+ auto_linked_credential: {
820
+ credential_id: resolvedCredentialId,
821
+ provider: autoLinkedFrom.provider,
822
+ credential_name: autoLinkedFrom.credentialName,
823
+ // F51 — when true, the credential was matched by OAuth
824
+ // FAMILY (e.g. a "google" credential for a "google-sheets"
825
+ // template) rather than an exact provider string. Surfaced
826
+ // so the agent confirms an existing credential was reused
827
+ // instead of needing a fresh OAuth flow.
828
+ ...(autoLinkedFrom.familyMatched ? { family_matched: true } : {}),
829
+ },
830
+ }
831
+ : {}),
832
+ // F1 — echo the backend's decision booleans verbatim so the agent
833
+ // can branch on them too (only when the backend supplied them).
834
+ ...(reused !== undefined ? { reused } : {}),
835
+ ...(healed !== undefined ? { healed } : {}),
836
+ ...(mutated !== undefined ? { mutated } : {}),
437
837
  flow: flowSummary,
438
- note: "Connection created; use apimapper_connection_list to find the new id.",
838
+ note,
439
839
  }, false, { maxChars: 3000 });
440
840
  });
441
841
  // ── apimapper_library_deactivate ───────────────────────────────────
@@ -465,12 +865,7 @@ export function registerLibraryTools(server) {
465
865
  method: "DELETE",
466
866
  });
467
867
  if (!r.success) {
468
- return errorResult({
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
- });
868
+ return restErrorResult(r, { id }, { message: "library deactivate failed" });
474
869
  }
475
870
  return formatResult({ deactivated: true, id }, false, { maxChars: 1500 });
476
871
  });
@@ -490,21 +885,88 @@ function buildConnectionDetail(id, connection) {
490
885
  { key: "description", label: "Description", value: str(connection.description) },
491
886
  ];
492
887
  const overview = overviewAll.filter((e) => e.value !== null);
493
- // Endpoints arrive as an array of objects surface the count plus each
494
- // endpoint's label so the detail view stays scalar-only.
888
+ // Endpoints arrive as an array of objects. Friction-3 fix (Task #46,
889
+ // 2026-05-29): expand each endpoint to surface its NAME (not "Endpoint 1")
890
+ // and the list of required/optional extra_fields the template declares —
891
+ // those are the values an AI agent must pass into `library_activate({extra_fields})`.
495
892
  const endpoints = Array.isArray(connection.endpoints) ? connection.endpoints : [];
893
+ const templateExtraFields = Array.isArray(connection.extra_fields)
894
+ ? connection.extra_fields
895
+ : [];
496
896
  const endpointEntries = [
497
897
  { key: "endpoint_count", label: "Endpoints", value: endpoints.length },
498
- ...endpoints.map((ep, idx) => {
898
+ ...endpoints.flatMap((ep, idx) => {
499
899
  const epObj = ep && typeof ep === "object" ? ep : {};
500
- const label = str(epObj.label) ?? str(epObj.id) ?? `endpoint ${idx + 1}`;
501
- return {
502
- key: `endpoint_${idx}`,
503
- label: ` ${str(epObj.id) ?? idx + 1}`,
504
- value: label,
505
- };
900
+ const name = str(epObj.name) ?? str(epObj.id) ?? `endpoint ${idx + 1}`;
901
+ const description = str(epObj.description) ?? str(epObj.label);
902
+ const path = str(epObj.path);
903
+ const method = str(epObj.method);
904
+ const entries = [
905
+ {
906
+ key: `endpoint_${idx}_name`,
907
+ label: ` ${idx + 1}. Name`,
908
+ value: name,
909
+ format: "code",
910
+ copyable: true,
911
+ },
912
+ ];
913
+ if (description) {
914
+ entries.push({
915
+ key: `endpoint_${idx}_description`,
916
+ label: " Description",
917
+ value: description,
918
+ });
919
+ }
920
+ if (path || method) {
921
+ entries.push({
922
+ key: `endpoint_${idx}_route`,
923
+ label: " Route",
924
+ value: `${method ?? "GET"} ${path ?? "—"}`,
925
+ });
926
+ }
927
+ return entries;
506
928
  }),
507
929
  ];
930
+ // Friction-3 (Task #46): list required + optional extra_fields. AI agents
931
+ // need to know which keys MUST appear in the `extra_fields` argument of
932
+ // library_activate.
933
+ const extraFieldEntries = [];
934
+ if (templateExtraFields.length > 0) {
935
+ for (const ef of templateExtraFields) {
936
+ const efName = str(ef.name);
937
+ if (!efName)
938
+ continue;
939
+ const required = ef.required === true;
940
+ const efType = str(ef.type) ?? "string";
941
+ // Wave-8 C1 (2026-05-29): featured-connections templates ship the
942
+ // human-readable description under `help_text` (not `description`).
943
+ // The label-fallback was kept but now ranks below help_text so
944
+ // google-sheets' "Choose from your recent spreadsheets or paste a URL"
945
+ // surfaces instead of just "Spreadsheet".
946
+ const efDesc = str(ef.help_text) ??
947
+ str(ef.description) ??
948
+ str(ef.label) ??
949
+ "";
950
+ const efExample = str(ef.example) ?? str(ef.placeholder) ?? "";
951
+ const valueParts = [];
952
+ valueParts.push(`type=${efType}`);
953
+ if (required)
954
+ valueParts.push("REQUIRED");
955
+ else
956
+ valueParts.push("optional");
957
+ if (efExample)
958
+ valueParts.push(`e.g. "${efExample}"`);
959
+ if (efDesc)
960
+ valueParts.push(efDesc);
961
+ extraFieldEntries.push({
962
+ key: `extra_${efName}`,
963
+ label: ` ${efName}`,
964
+ value: valueParts.join(" · "),
965
+ format: "code",
966
+ copyable: true,
967
+ });
968
+ }
969
+ }
508
970
  const auth = connection.auth_scheme && typeof connection.auth_scheme === "object"
509
971
  ? connection.auth_scheme
510
972
  : undefined;
@@ -520,6 +982,9 @@ function buildConnectionDetail(id, connection) {
520
982
  groups: [
521
983
  { label: "Template", entries: overview },
522
984
  ...(endpointEntries.length > 0 ? [{ label: "Endpoints", entries: endpointEntries }] : []),
985
+ ...(extraFieldEntries.length > 0
986
+ ? [{ label: "Required/Optional extra_fields", entries: extraFieldEntries }]
987
+ : []),
523
988
  ...(authEntries.length > 0 ? [{ label: "Authentication", entries: authEntries }] : []),
524
989
  {
525
990
  // IA-10 (W3.9): actionable next steps in a dedicated group — visible