@wootsup/mcp 0.3.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 (141) hide show
  1. package/CHANGELOG.md +14 -5
  2. package/dist/catalog/build-catalog.d.ts +31 -0
  3. package/dist/catalog/build-catalog.js +68 -0
  4. package/dist/catalog/build-catalog.js.map +1 -0
  5. package/dist/index.js +37 -5
  6. package/dist/index.js.map +1 -1
  7. package/dist/modules/apimapper/auto-layout.d.ts +21 -0
  8. package/dist/modules/apimapper/auto-layout.js +54 -0
  9. package/dist/modules/apimapper/auto-layout.js.map +1 -0
  10. package/dist/modules/apimapper/client.d.ts +54 -4
  11. package/dist/modules/apimapper/client.js +145 -14
  12. package/dist/modules/apimapper/client.js.map +1 -1
  13. package/dist/modules/apimapper/connections-format.d.ts +31 -1
  14. package/dist/modules/apimapper/connections-format.js +97 -5
  15. package/dist/modules/apimapper/connections-format.js.map +1 -1
  16. package/dist/modules/apimapper/connections.d.ts +9 -7
  17. package/dist/modules/apimapper/connections.js +225 -58
  18. package/dist/modules/apimapper/connections.js.map +1 -1
  19. package/dist/modules/apimapper/credentials.js +86 -14
  20. package/dist/modules/apimapper/credentials.js.map +1 -1
  21. package/dist/modules/apimapper/elicitation.d.ts +29 -0
  22. package/dist/modules/apimapper/elicitation.js +62 -0
  23. package/dist/modules/apimapper/elicitation.js.map +1 -1
  24. package/dist/modules/apimapper/example-extract.d.ts +13 -0
  25. package/dist/modules/apimapper/example-extract.js +111 -0
  26. package/dist/modules/apimapper/example-extract.js.map +1 -0
  27. package/dist/modules/apimapper/filter-operators.d.ts +24 -0
  28. package/dist/modules/apimapper/filter-operators.js +103 -0
  29. package/dist/modules/apimapper/filter-operators.js.map +1 -0
  30. package/dist/modules/apimapper/flows-format.js +92 -22
  31. package/dist/modules/apimapper/flows-format.js.map +1 -1
  32. package/dist/modules/apimapper/flows.d.ts +8 -7
  33. package/dist/modules/apimapper/flows.js +216 -44
  34. package/dist/modules/apimapper/flows.js.map +1 -1
  35. package/dist/modules/apimapper/gateway/advanced-read-tool.d.ts +9 -0
  36. package/dist/modules/apimapper/gateway/advanced-read-tool.js +172 -0
  37. package/dist/modules/apimapper/gateway/advanced-read-tool.js.map +1 -0
  38. package/dist/modules/apimapper/gateway/advanced-tool.js +39 -130
  39. package/dist/modules/apimapper/gateway/advanced-tool.js.map +1 -1
  40. package/dist/modules/apimapper/gateway/collect-module-tools.d.ts +17 -0
  41. package/dist/modules/apimapper/gateway/collect-module-tools.js +44 -0
  42. package/dist/modules/apimapper/gateway/collect-module-tools.js.map +1 -0
  43. package/dist/modules/apimapper/gateway/essentials.d.ts +1 -1
  44. package/dist/modules/apimapper/gateway/essentials.js +19 -7
  45. package/dist/modules/apimapper/gateway/essentials.js.map +1 -1
  46. package/dist/modules/apimapper/gateway/gateway-shared.d.ts +21 -0
  47. package/dist/modules/apimapper/gateway/gateway-shared.js +124 -0
  48. package/dist/modules/apimapper/gateway/gateway-shared.js.map +1 -0
  49. package/dist/modules/apimapper/gateway/test-support.d.ts +1 -17
  50. package/dist/modules/apimapper/gateway/test-support.js +4 -33
  51. package/dist/modules/apimapper/gateway/test-support.js.map +1 -1
  52. package/dist/modules/apimapper/get-skill-cores.d.ts +4 -0
  53. package/dist/modules/apimapper/get-skill-cores.js +220 -0
  54. package/dist/modules/apimapper/get-skill-cores.js.map +1 -0
  55. package/dist/modules/apimapper/get-skill.d.ts +1 -1
  56. package/dist/modules/apimapper/get-skill.js +30 -3
  57. package/dist/modules/apimapper/get-skill.js.map +1 -1
  58. package/dist/modules/apimapper/graph-builder.d.ts +85 -2
  59. package/dist/modules/apimapper/graph-builder.js +151 -15
  60. package/dist/modules/apimapper/graph-builder.js.map +1 -1
  61. package/dist/modules/apimapper/graph.js +115 -15
  62. package/dist/modules/apimapper/graph.js.map +1 -1
  63. package/dist/modules/apimapper/index.js +25 -13
  64. package/dist/modules/apimapper/index.js.map +1 -1
  65. package/dist/modules/apimapper/jmespath-test.d.ts +4 -0
  66. package/dist/modules/apimapper/jmespath-test.js +152 -0
  67. package/dist/modules/apimapper/jmespath-test.js.map +1 -0
  68. package/dist/modules/apimapper/library.js +131 -8
  69. package/dist/modules/apimapper/library.js.map +1 -1
  70. package/dist/modules/apimapper/list-footer.d.ts +27 -0
  71. package/dist/modules/apimapper/list-footer.js +57 -0
  72. package/dist/modules/apimapper/list-footer.js.map +1 -0
  73. package/dist/modules/apimapper/local-sources.js +88 -31
  74. package/dist/modules/apimapper/local-sources.js.map +1 -1
  75. package/dist/modules/apimapper/mcp-client-identity.d.ts +32 -0
  76. package/dist/modules/apimapper/mcp-client-identity.js +70 -0
  77. package/dist/modules/apimapper/mcp-client-identity.js.map +1 -0
  78. package/dist/modules/apimapper/merge-constants.d.ts +6 -0
  79. package/dist/modules/apimapper/merge-constants.js +26 -0
  80. package/dist/modules/apimapper/merge-constants.js.map +1 -0
  81. package/dist/modules/apimapper/node-schema.d.ts +52 -2
  82. package/dist/modules/apimapper/node-schema.js +95 -4
  83. package/dist/modules/apimapper/node-schema.js.map +1 -1
  84. package/dist/modules/apimapper/onboarding.d.ts +29 -0
  85. package/dist/modules/apimapper/onboarding.js +117 -9
  86. package/dist/modules/apimapper/onboarding.js.map +1 -1
  87. package/dist/modules/apimapper/read-cache.d.ts +16 -3
  88. package/dist/modules/apimapper/read-cache.js +59 -4
  89. package/dist/modules/apimapper/read-cache.js.map +1 -1
  90. package/dist/modules/apimapper/render/index.js +26 -5
  91. package/dist/modules/apimapper/render/index.js.map +1 -1
  92. package/dist/modules/apimapper/resource-id.d.ts +13 -0
  93. package/dist/modules/apimapper/resource-id.js +69 -0
  94. package/dist/modules/apimapper/resource-id.js.map +1 -0
  95. package/dist/modules/apimapper/tool-result.d.ts +20 -0
  96. package/dist/modules/apimapper/tool-result.js +67 -5
  97. package/dist/modules/apimapper/tool-result.js.map +1 -1
  98. package/dist/modules/apimapper/toolslist-size.d.ts +10 -10
  99. package/dist/modules/apimapper/toolslist-size.js +29 -18
  100. package/dist/modules/apimapper/toolslist-size.js.map +1 -1
  101. package/dist/modules/apimapper/types.d.ts +13 -0
  102. package/dist/modules/apimapper/types.js +1 -1
  103. package/dist/modules/apimapper/types.js.map +1 -1
  104. package/dist/modules/apimapper/whitelist-drift.js +16 -1
  105. package/dist/modules/apimapper/whitelist-drift.js.map +1 -1
  106. package/dist/modules/apimapper/workflows.js +221 -32
  107. package/dist/modules/apimapper/workflows.js.map +1 -1
  108. package/dist/modules/apimapper/yootheme-binding.js +103 -22
  109. package/dist/modules/apimapper/yootheme-binding.js.map +1 -1
  110. package/dist/platform/index.js +7 -0
  111. package/dist/platform/index.js.map +1 -1
  112. package/dist/proxy/bridge.d.ts +35 -0
  113. package/dist/proxy/bridge.js +129 -0
  114. package/dist/proxy/bridge.js.map +1 -0
  115. package/dist/proxy/mode.d.ts +9 -0
  116. package/dist/proxy/mode.js +20 -0
  117. package/dist/proxy/mode.js.map +1 -0
  118. package/dist/setup/probe-auth.d.ts +51 -0
  119. package/dist/setup/probe-auth.js +141 -0
  120. package/dist/setup/probe-auth.js.map +1 -0
  121. package/dist/setup-cli.d.ts +9 -0
  122. package/dist/setup-cli.js +34 -0
  123. package/dist/setup-cli.js.map +1 -1
  124. package/dist/sites/loader.d.ts +7 -0
  125. package/dist/sites/loader.js +16 -1
  126. package/dist/sites/loader.js.map +1 -1
  127. package/dist/skill-instructions.d.ts +14 -1
  128. package/dist/skill-instructions.js +30 -6
  129. package/dist/skill-instructions.js.map +1 -1
  130. package/manifest.json +2 -2
  131. package/package.json +3 -2
  132. package/skills/apimapper/SKILL.md +78 -3
  133. package/skills/apimapper/reference/dynamize-existing-layout.md +158 -0
  134. package/skills/apimapper/reference/jmespath-cookbook.md +241 -0
  135. package/skills/apimapper/reference/jmespath-pitfalls.md +81 -0
  136. package/skills/apimapper/reference/library-template-discovery.md +1 -1
  137. package/skills/apimapper/reference/merge-two-sources-on-key.md +117 -12
  138. package/skills/apimapper/reference/oauth.md +143 -52
  139. package/skills/apimapper/reference/troubleshooting.md +2 -2
  140. package/skills/apimapper/reference/yootheme-source-to-builder-handoff.md +348 -0
  141. package/skills/apimapper/reference/yootheme.md +75 -44
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, pickFields, createProgressReporter, elicitChoice, } from "@getimo/mcp-toolkit";
2
+ import { formatResult, tableResult, errorResult, readOnly, creating, mutating, destructive, pickFields, createProgressReporter, } from "@getimo/mcp-toolkit";
3
3
  import { request, hintFor, WP_BASE, WP_USER, authConfigured } from "./client.js";
4
4
  import { restErrorResult } from "./tool-result.js";
5
5
  import { toRows } from "./types.js";
@@ -7,7 +7,8 @@ import { unwrapEntity } from "./envelope.js";
7
7
  import { ambiguityFallbackError, } from "./elicitation.js";
8
8
  import { fetchWithTimeout } from "./diagnose.js";
9
9
  import { filterByNameQuery } from "./node-schema.js";
10
- import { CONNECTION_TABLE_COLUMNS, CONNECTION_COMPACT_COLUMNS, CONNECTION_LIST_NEXT_STEPS, RESOURCE_TABLE_COLUMNS, mapConnectionRow, compactConnectionRow, mapResourceRow, buildConnectionDetail, buildConnectionTestStats, buildHealthCheckStats, buildHealthStats, } from "./connections-format.js";
10
+ import { withOverflowFooter, truncatedByLimitNote } from "./list-footer.js";
11
+ import { CONNECTION_TABLE_COLUMNS, CONNECTION_FULL_COLUMNS, CONNECTION_COMPACT_COLUMNS, CONNECTION_LIST_NEXT_STEPS, RESOURCE_TABLE_COLUMNS, mapConnectionRow, mapConnectionRowFull, compactConnectionRow, mapResourceRow, buildConnectionDetail, buildConnectionTestStats, buildHealthCheckStats, buildHealthStats, } from "./connections-format.js";
11
12
  // W3 Stage-2 hardening — applyTrim + extractResourceList live in the
12
13
  // sibling connections-trim.ts (pure functions, ~200 lines). Imported for
13
14
  // internal use by the connection_data + connection_resources handlers,
@@ -25,6 +26,7 @@ export { applyTrim, extractResourceList };
25
26
  // formatter; connection_get / _update / _delete pass through unwrapEntity
26
27
  // + the rich-card builder which already reads from the same row map.
27
28
  import { normalizeConnectionFromWire } from "./normalizers.js";
29
+ import { normalizeResourceIdFields } from "./resource-id.js";
28
30
  /**
29
31
  * F-15 (W1.9) — downstream probe of api.wootsup.com from `apimapper_health`.
30
32
  *
@@ -72,13 +74,16 @@ async function probeApimapperApi() {
72
74
  /**
73
75
  * Register the connection CRUD + probe + sample + pipeline tools.
74
76
  *
75
- * @param server the tool registrar (essentials forward, rest captured).
76
- * @param elicitation optional elicitation capability (the real McpServer, or
77
- * any `{ server: { elicitInput } }`). Supplied only by the host wiring in
78
- * index.ts. When omitted, `connection_create` falls back to a structured
79
- * error on genuine credential ambiguity instead of prompting.
77
+ * Chat-first disambiguation (decision 2026-06-15): on genuine credential
78
+ * ambiguity `connection_create` returns a structured candidate-list error
79
+ * (the assistant asks in plain chat) rather than driving the native
80
+ * elicitation picker. The picker is reserved for OAuth authorization
81
+ * (`apimapper_oauth_authorize_begin` in credentials.ts), so this module no
82
+ * longer takes an elicitation capability.
83
+ *
84
+ * @param server the tool registrar (essentials forward, rest captured).
80
85
  */
81
- export function registerConnectionTools(server, elicitation) {
86
+ export function registerConnectionTools(server) {
82
87
  // ── apimapper_health ───────────────────────────────────────────────
83
88
  server.registerTool("apimapper_health", {
84
89
  title: "API Mapper REST Health",
@@ -159,17 +164,28 @@ export function registerConnectionTools(server, elicitation) {
159
164
  // rc.10 A5 (2026-05-19) — description states the default limit
160
165
  // already covers a typical install. The Maria-walkthrough log
161
166
  // showed AI defaulting to limit:200/500 reflexively.
162
- description: "List all API Mapper connections. The default limit of 50 already covers a typical install " +
163
- "most customers run with 5-25 connections, increase the limit only if you have a specific " +
164
- "reason to expect more. Use apimapper_connection_get for full details of a single connection." +
165
- "\n\nExample:\n apimapper_connection_list({}) // returns up to 50 connections in one call\n" +
166
- " apimapper_connection_list({ source: 'user' }) // narrow to user-created only",
167
+ description: "List API Mapper connections a LEAN projection by default (id / name / source / host / health). " +
168
+ "The default limit of 25 already covers a typical install most customers run with 5-25 " +
169
+ "connections, increase the limit only if you have a specific reason to expect more. " +
170
+ "Pass `fields: ['all']` for the full dump (endpoint URL, method, auth_type, credential_id), " +
171
+ "or a list of specific extra columns. Use apimapper_connection_get for full details of a single connection." +
172
+ "\n\nExample:\n apimapper_connection_list({}) // lean projection, up to 25 connections\n" +
173
+ " apimapper_connection_list({ source: 'user' }) // narrow to user-created only\n" +
174
+ " apimapper_connection_list({ fields: ['all'] }) // full detail dump",
167
175
  inputSchema: {
168
176
  source: z
169
177
  .enum(["library", "demo", "user", "all"])
170
178
  .default("all")
171
179
  .describe("Filter by origin"),
172
- limit: z.number().min(1).max(500).default(50).describe("Max items (1-500). Default 50 already covers a typical install — do not raise unless you have evidence the list is larger."),
180
+ limit: z.number().min(1).max(500).default(25).describe("Max items (1-500). Default 25 already covers a typical install — do not raise unless you have evidence the list is larger."),
181
+ // F198 — opt-in full detail. The default projection is the lean
182
+ // id/name/source/host/health set; `fields` widens it. `['all']` (or
183
+ // any non-empty array) returns the full endpoint/method/auth/cred dump.
184
+ fields: z
185
+ .array(z.string().min(1))
186
+ .optional()
187
+ .describe("Opt into extra columns. Omit for the lean default (id/name/source/host/health). " +
188
+ "Pass ['all'] for the full dump (endpoint URL, method, auth_type, credential_id)."),
173
189
  // W1.18 (F-32) — case-insensitive substring filter applied
174
190
  // in-memory AFTER the upstream fetch (no per-name REST call).
175
191
  // Min 2 chars at the schema boundary blocks the single-char
@@ -181,7 +197,7 @@ export function registerConnectionTools(server, elicitation) {
181
197
  .describe("Case-insensitive substring filter on connection name. Applied in-memory after the upstream fetch — does NOT change the REST query. Must be 2+ characters; single-char probes are blocked. Combine with `source` for narrower results."),
182
198
  },
183
199
  annotations: readOnly({ title: "List Connections", openWorld: true }),
184
- }, async ({ source, limit, name_query }) => {
200
+ }, async ({ source, limit, name_query, fields }) => {
185
201
  const r = await request("/connections");
186
202
  if (!r.success) {
187
203
  return restErrorResult(r, { source, limit, name_query }, { message: "connection list failed" });
@@ -199,18 +215,41 @@ export function registerConnectionTools(server, elicitation) {
199
215
  // subset, not the haystack. See `filterByNameQuery` JSDoc for the
200
216
  // case-folding + undefined-skip contract shared with flow_list.
201
217
  items = filterByNameQuery(items, name_query);
218
+ // F198 — capture the matched total BEFORE the slice so the footer can
219
+ // report exactly how many rows were dropped by the limit.
220
+ const totalMatched = items.length;
202
221
  items = items.slice(0, limit);
222
+ // F198 — opt into the full detail dump only when `fields` is supplied.
223
+ // Any non-empty `fields` array (typically ['all']) widens the table to
224
+ // the full column set; the default stays the lean projection.
225
+ const wantFull = Array.isArray(fields) && fields.length > 0;
226
+ // F198 — the honest "N more — filter with name_query or raise limit"
227
+ // note fires whenever the limit actually dropped matched rows. The
228
+ // compact-render note (withOverflowFooter) still rides along for the
229
+ // truncated-text case.
230
+ const overflowNote = truncatedByLimitNote(totalMatched, limit, "connections");
231
+ const baseFooter = overflowNote
232
+ ? `${CONNECTION_LIST_NEXT_STEPS}\n${overflowNote}`
233
+ : CONNECTION_LIST_NEXT_STEPS;
203
234
  // W3.1 — tableResult: ASCII table for the LLM + typed DataTable payload
204
- // for the Rich Card. T1 (W3.2): explicit compactColumns/compactMap drop
205
- // ENDPOINT/METHOD/CRED at 21+ rows. IA-7: id stays llmOnly. IA-10: the
206
- // footer carries the next-step guidance.
235
+ // for the Rich Card. Default = lean projection (id/name/source/host/
236
+ // health); `fields` = full dump. IA-7: id stays llmOnly. IA-10: the
237
+ // footer carries the next-step guidance + the F198 overflow note.
207
238
  return tableResult(toRows(items), {
208
- columns: CONNECTION_TABLE_COLUMNS,
239
+ columns: wantFull ? CONNECTION_FULL_COLUMNS : CONNECTION_TABLE_COLUMNS,
209
240
  compactColumns: CONNECTION_COMPACT_COLUMNS,
210
- map: mapConnectionRow,
241
+ map: wantFull ? mapConnectionRowFull : mapConnectionRow,
211
242
  compactMap: compactConnectionRow,
212
- header: (n) => `${n} connections`,
213
- footer: CONNECTION_LIST_NEXT_STEPS,
243
+ // F198 the "N more" overflow note rides in the HEADER (rendered
244
+ // first, never truncated) so it survives even when a 21+ row table
245
+ // compacts to 2000 chars and drops the footer. The footer keeps the
246
+ // next-step guidance + the compact-render note for the non-overflow
247
+ // truncation case.
248
+ header: (n) => (overflowNote ? `${n} connections — ${overflowNote}` : `${n} connections`),
249
+ // Minor (2026-06-10): at 21+ rows the toolkit compacts the LLM-visible
250
+ // table to 2000 chars — append an honest "use filters" note so the
251
+ // agent does not mistake a truncated list for the full set.
252
+ footer: withOverflowFooter(baseFooter, items.length, "connections"),
214
253
  });
215
254
  });
216
255
  // ── apimapper_connection_get ───────────────────────────────────────
@@ -255,26 +294,44 @@ export function registerConnectionTools(server, elicitation) {
255
294
  // W3.1 — detailResult: grouped key-value detail for the Rich Card.
256
295
  // IA-7: opaque IDs are copyable code entries. IA-10: a dedicated
257
296
  // "Next steps" group carries the follow-up calls.
258
- return buildConnectionDetail(id, conn);
297
+ // F9 (2026-06-09): run the same camelCase→snake_case wire bridge the
298
+ // list path applies (F-LS-04) — buildConnectionDetail reads
299
+ // c.auth_type/credential_id; the raw wire shape carries authType/
300
+ // credentialId, so without this the detail rendered auth "none"
301
+ // while the list showed the real auth for the SAME connection.
302
+ return buildConnectionDetail(id, normalizeConnectionFromWire(conn));
259
303
  });
260
304
  // ── apimapper_connection_create ────────────────────────────────────
261
305
  server.registerTool("apimapper_connection_create", {
262
306
  title: "Create Connection",
263
307
  description: "Create a custom connection. **Use ONLY when no library template matches your target API.** " +
264
- "First call `apimapper_library_featured()` or `apimapper_library_list({ query: '<api-name>' })` — " +
308
+ "First call `apimapper_library_featured()` or `apimapper_library_list({ search: '<api-name>' })` (min 3 chars) — " +
265
309
  "Google Sheets, Calendly, Notion, Airtable, GitHub, Pexels, Unsplash, OpenWeatherMap, REST Countries, " +
266
310
  "Google Drive/Docs/Slides/Tasks all have ready-to-use templates with auto-configured auth and " +
267
311
  "field-detection. Library activation via `apimapper_library_activate({ id })` is the canonical " +
268
312
  "customer path; connection_create is the fallback for niche or unknown APIs. " +
269
313
  "The server enforces this: a custom create on an API already covered by a curated template is " +
270
314
  "blocked with a 409 naming the template to activate — pass `acknowledge_no_library: true` to " +
271
- "intentionally override that block." +
315
+ "intentionally override that block. " +
316
+ // A16 (2026-06-10): the guard matches on the HOST, so a genuinely
317
+ // uncovered endpoint never blocks — do NOT set acknowledge_no_library
318
+ // pre-emptively.
319
+ "The guard matches on the request HOST against the curated library; a connection to an " +
320
+ "uncovered host is NEVER blocked and needs no `acknowledge_no_library` — only set that flag " +
321
+ "AFTER you actually receive a 409." +
322
+ "\n\nNote: connection auth_type spellings differ from apimapper_credential_create's: " +
323
+ "here it is `basic` / `oauth2`, whereas credential_create uses `basic_auth` / `oauth2_code` / `oauth2_cc`. " +
324
+ "Use the spelling for the tool you are calling." +
272
325
  "\n\nExample:\n apimapper_connection_create({ name: 'Pexels API', endpoint: 'https://api.pexels.com/v1/search', method: 'GET', auth_type: 'bearer', items_path: 'photos' })",
273
326
  inputSchema: {
274
327
  name: z.string().min(1).describe('Connection name (e.g., "My Blog API")'),
275
328
  endpoint: z.string().describe('Endpoint path or full URL (e.g., "https://api.example.com/posts")'),
276
329
  method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET").describe("HTTP method"),
277
- auth_type: z.enum(["none", "api_key", "bearer", "basic", "oauth2"]).default("none").describe("Auth type (snake_case wire key)"),
330
+ auth_type: z
331
+ .enum(["none", "api_key", "bearer", "basic", "oauth2"])
332
+ .default("none")
333
+ .describe("Auth type (snake_case wire key). " +
334
+ "NB: credential_create spells these `basic_auth` (here `basic`) and `oauth2_code`/`oauth2_cc` (here `oauth2`)."),
278
335
  credential_id: z.string().optional().describe("Credential ID (snake_case wire key) if auth_type requires one"),
279
336
  items_path: z.string().optional().describe('JSONPath to items array (snake_case wire key)'),
280
337
  cache_ttl: z.number().int().min(0).default(3600).describe("Cache TTL in seconds (snake_case wire key)"),
@@ -288,9 +345,19 @@ export function registerConnectionTools(server, elicitation) {
288
345
  acknowledge_no_library: z.boolean().optional().describe("Set true ONLY when no library template fits and you intentionally need a custom connection. " +
289
346
  "The server blocks custom creates on APIs that already have a curated library template; " +
290
347
  "this flag is the audited override. Prefer apimapper_library_activate({ id }) when a template exists."),
348
+ // Phase 2.7 (change protocol): the action is IMPLICIT
349
+ // (`connection.created`) — only `summary` is needed; the handler
350
+ // defaults the action. Threaded into the POST body as
351
+ // `change: { summary }` (conditional, never a top-level key).
352
+ summary: z
353
+ .string()
354
+ .max(280)
355
+ .optional()
356
+ .describe('Optional one-line summary of this change for the change history ' +
357
+ '(e.g., "Added a Pexels photo connection").'),
291
358
  },
292
359
  annotations: creating({ title: "Create Connection", openWorld: true }),
293
- }, async (input) => {
360
+ }, async ({ summary, ...input }) => {
294
361
  // W3.6 — when the connection's auth_type needs a credential and none is
295
362
  // given: exactly 1 stored credential → auto-pick; >1 → elicitChoice;
296
363
  // elicitChoice null (unsupported client / declined) → structured
@@ -314,31 +381,39 @@ export function registerConnectionTools(server, elicitation) {
314
381
  resolvedInput = { ...input, credential_id: creds[0].id };
315
382
  }
316
383
  else {
384
+ // Chat-first disambiguation (decision 2026-06-15): the native
385
+ // elicitation picker renders as a clunky collapsed TUI in some
386
+ // hosts. For an ordinary credential pick the assistant asking in
387
+ // plain chat is nicer, so we skip the picker and return the
388
+ // structured candidate-list error directly — its `candidates`
389
+ // carry id+label, so the assistant sees the names and can ask.
390
+ // (The OAuth authorize path KEEPS the picker: explicit consent on
391
+ // WHICH credential to authorize is security-relevant.)
317
392
  const candidates = creds.map((c) => ({
318
393
  id: c.id,
319
394
  label: c.name,
320
395
  }));
321
- const picked = elicitation
322
- ? await elicitChoice(elicitation, `The "${input.name}" connection (auth_type: ${input.auth_type}) needs a ` +
323
- "credential. Pick which stored credential to use.", candidates.map((c) => c.id))
324
- : null;
325
- if (picked === null) {
326
- return ambiguityFallbackError({
327
- code: "credential_ambiguous",
328
- paramName: "credential_id",
329
- what: "credentials",
330
- candidates,
331
- extraDetails: { name: input.name, auth_type: input.auth_type },
332
- });
333
- }
334
- resolvedInput = { ...input, credential_id: picked };
396
+ return ambiguityFallbackError({
397
+ code: "credential_ambiguous",
398
+ paramName: "credential_id",
399
+ what: "credentials",
400
+ candidates,
401
+ extraDetails: { name: input.name, auth_type: input.auth_type },
402
+ });
335
403
  }
336
404
  }
405
+ // Phase 2.7 — implicit action (`connection.created`); attach `change:
406
+ // { summary }` only when supplied so the body is unchanged otherwise.
407
+ // `summary` was destructured out of `input` above, so it never leaks as a
408
+ // top-level connection column.
409
+ const createBody = summary !== undefined
410
+ ? { ...resolvedInput, change: { summary } }
411
+ : resolvedInput;
337
412
  // PHP fromControllerResponse(_, 'connection', 201) → {success, connection:{…}}.
338
413
  // Audit: F-A1-02.
339
414
  const r = await request("/connections", {
340
415
  method: "POST",
341
- body: JSON.stringify(resolvedInput),
416
+ body: JSON.stringify(createBody),
342
417
  });
343
418
  if (!r.success) {
344
419
  // M1 (MCP-relay audit) — wire-shape parity with the admin-ui. On the
@@ -388,7 +463,7 @@ export function registerConnectionTools(server, elicitation) {
388
463
  name: conn?.name,
389
464
  source: conn?.source,
390
465
  next_steps: [
391
- "Use apimapper_connection_test to verify reachability, then apimapper_flow_create to wire it into a flow.",
466
+ 'Verify reachability via apimapper_advanced({ tool: "apimapper_connection_test", arguments: { id } }), then build a flow with apimapper_flow_setup_with_sources (or apimapper_advanced({ tool: "apimapper_flow_create" })).',
392
467
  ],
393
468
  }, false, { maxChars: 2000 });
394
469
  });
@@ -411,14 +486,28 @@ export function registerConnectionTools(server, elicitation) {
411
486
  // .strict() allow-list here risks rejecting a legit field the
412
487
  // server accepts, so the schema stays permissive.
413
488
  'Unknown keys are dropped by the server (column-whitelisted on write).'),
489
+ // Phase 2.7 (change protocol): the action is IMPLICIT
490
+ // (`connection.updated`) — only `summary` is needed; the handler
491
+ // defaults the action. Threaded into the PUT body as
492
+ // `change: { summary }` (sibling of the patch fields).
493
+ summary: z
494
+ .string()
495
+ .max(280)
496
+ .optional()
497
+ .describe('Optional one-line summary of this change (e.g., "Pointed the endpoint at /v2/posts").'),
414
498
  },
415
499
  annotations: mutating({ title: "Update Connection", openWorld: true }),
416
- }, async ({ id, patch }) => {
500
+ }, async ({ id, patch, summary }) => {
501
+ // Phase 2.7 — implicit action (`connection.updated`); attach `change:
502
+ // { summary }` only when supplied so the body is unchanged otherwise.
503
+ const updateBody = summary !== undefined
504
+ ? { ...patch, change: { summary } }
505
+ : patch;
417
506
  // PHP fromControllerResponse(_, 'connection') → {success, connection:{…}}.
418
507
  // Audit: F-A1-03.
419
508
  const r = await request(`/connections/${encodeURIComponent(id)}`, {
420
509
  method: "PUT",
421
- body: JSON.stringify(patch),
510
+ body: JSON.stringify(updateBody),
422
511
  });
423
512
  if (!r.success) {
424
513
  return restErrorResult(r, { id }, { message: "connection update failed" });
@@ -432,7 +521,7 @@ export function registerConnectionTools(server, elicitation) {
432
521
  id: conn?.id,
433
522
  name: conn?.name,
434
523
  next_steps: [
435
- "Re-run apimapper_connection_test if endpoint/auth changed; flows referencing this connection may need to be republished.",
524
+ 'Re-test if endpoint/auth changed via apimapper_advanced({ tool: "apimapper_connection_test", arguments: { id } }); flows referencing this connection may need to be republished via apimapper_flow_full_recompile_publish.',
436
525
  ],
437
526
  }, false, { maxChars: 2000 });
438
527
  });
@@ -535,7 +624,7 @@ export function registerConnectionTools(server, elicitation) {
535
624
  name: conn?.name,
536
625
  applied_patch_keys: Object.keys(patch),
537
626
  next_steps: [
538
- "Verify with apimapper_connection_get; re-run apimapper_connection_test before re-publishing flows.",
627
+ 'Verify via apimapper_advanced({ tool: "apimapper_connection_get", arguments: { id } }); re-test via apimapper_advanced({ tool: "apimapper_connection_test", arguments: { id } }) before re-publishing flows.',
539
628
  ],
540
629
  }, false, { maxChars: 2000 });
541
630
  });
@@ -551,14 +640,29 @@ export function registerConnectionTools(server, elicitation) {
551
640
  .boolean()
552
641
  .default(false)
553
642
  .describe("Must be true to execute. On confirm:false, returns a preview."),
643
+ // Phase 2.7 (change protocol): the action is IMPLICIT
644
+ // (`connection.deleted`) — only `summary` is needed; the handler
645
+ // defaults the action. Attached to the DELETE body as
646
+ // `change: { summary }` (the delete otherwise has no body), and only on
647
+ // the real delete — the preview branch never records a change.
648
+ summary: z
649
+ .string()
650
+ .max(280)
651
+ .optional()
652
+ .describe('Optional one-line summary of this deletion (e.g., "Removed the unused Pexels connection").'),
554
653
  },
555
654
  annotations: destructive({ title: "Delete Connection", openWorld: true }),
556
- }, async ({ id, confirm }) => {
655
+ }, async ({ id, confirm, summary }) => {
557
656
  if (!confirm) {
558
657
  // PHP fromControllerResponse(_, 'connection') wraps as
559
658
  // {success, connection:{…}}. Unwrap defensively. Audit: F-A1-04.
560
659
  const preview = await request(`/connections/${encodeURIComponent(id)}`);
561
- const conn = preview.success ? unwrapEntity(preview.data, "connection") : undefined;
660
+ const rawConn = preview.success ? unwrapEntity(preview.data, "connection") : undefined;
661
+ // F9 (2026-06-09): same wire-shape bridge as connection_get — the
662
+ // preview target read auth_type off the raw camelCase wire shape.
663
+ const conn = rawConn
664
+ ? normalizeConnectionFromWire(rawConn)
665
+ : undefined;
562
666
  return formatResult({
563
667
  preview: true,
564
668
  warning: "DESTRUCTIVE — Connection delete cannot be undone. Dependent flows will break.",
@@ -574,7 +678,15 @@ export function registerConnectionTools(server, elicitation) {
574
678
  instruction: "Ask user to confirm, then call again with confirm: true.",
575
679
  });
576
680
  }
577
- const r = await request(`/connections/${encodeURIComponent(id)}`, { method: "DELETE" });
681
+ // Phase 2.7 implicit action (`connection.deleted`); attach `change:
682
+ // { summary }` only when supplied so the request is unchanged otherwise.
683
+ const r = await request(`/connections/${encodeURIComponent(id)}`, summary !== undefined
684
+ ? {
685
+ method: "DELETE",
686
+ body: JSON.stringify({ change: { summary } }),
687
+ headers: { "Content-Type": "application/json" },
688
+ }
689
+ : { method: "DELETE" });
578
690
  if (!r.success) {
579
691
  return restErrorResult(r, { id }, { message: "connection delete failed" });
580
692
  }
@@ -672,6 +784,11 @@ export function registerConnectionTools(server, elicitation) {
672
784
  "the response items in-memory — this does NOT trigger multi-page fetch upstream (the " +
673
785
  "request is single-shot). Use `fields` to project each item down to a whitelist of leaf " +
674
786
  "keys, cutting response size on sparse queries." +
787
+ "\n\nF144: if a library/template connection returns a 'Please configure …' / 'Please select an " +
788
+ "endpoint' diagnostic, the missing values are TEMPLATE FIELDS — supply them here via " +
789
+ "`template_fields` (e.g., { spreadsheet_id: '…' }) rather than re-creating the connection or " +
790
+ "starting a new OAuth flow. A connection whose template_fields are already resolved IS " +
791
+ "configured; inspect them with apimapper_connection_get({ id })." +
675
792
  "\n\nExample:\n" +
676
793
  " apimapper_connection_data({ id: 'con_abc123', limit: 10 }) // first 10 items\n" +
677
794
  " apimapper_connection_data({ id: 'con_abc123', limit: 5, offset: 10 }) // skip 10, take 5\n" +
@@ -684,7 +801,11 @@ export function registerConnectionTools(server, elicitation) {
684
801
  .trim()
685
802
  .min(1)
686
803
  .optional()
687
- .describe('Override endpoint (e.g., "Scheduled Events"). Empty-string and whitespace-only values are rejected at the schema boundary to prevent "?endpoint=" query garbage; omit the field entirely to use the connection default.'),
804
+ .describe('Named endpoint SELECTION NOT a URL. This is the label of a configured endpoint on the ' +
805
+ 'connection (e.g., "Scheduled Events", "Get Values"), the same names connection_create exposes ' +
806
+ 'as endpoints[]. List them via apimapper_connection_get({ id }). Empty-string and ' +
807
+ 'whitespace-only values are rejected at the schema boundary to prevent "?endpoint=" query ' +
808
+ "garbage; omit the field entirely to use the connection default."),
688
809
  template_fields: z
689
810
  .record(z.string(), z.union([z.string(), z.number().finite(), z.boolean()]))
690
811
  .optional()
@@ -726,7 +847,11 @@ export function registerConnectionTools(server, elicitation) {
726
847
  // implicitly, but the explicit guard documents the contract and keeps
727
848
  // TypeScript happy under the wider schema.
728
849
  if (template_fields) {
729
- for (const [k, v] of Object.entries(template_fields)) {
850
+ // A5 (2026-06-10): normalize a pasted Drive/Sheets URL in
851
+ // template_fields.spreadsheet_id to the bare ID before it becomes a
852
+ // query param — the {{spreadsheet_id}} placeholder needs the bare id.
853
+ const normalizedFields = normalizeResourceIdFields(template_fields);
854
+ for (const [k, v] of Object.entries(normalizedFields)) {
730
855
  params.set(k, typeof v === "string" ? v : String(v));
731
856
  }
732
857
  }
@@ -789,6 +914,25 @@ export function registerConnectionTools(server, elicitation) {
789
914
  ? pickFields(item, fields)
790
915
  : item);
791
916
  }
917
+ // F198 (Desktop-E2E #2 forensics, 2026-06-12): a 0-row read used to ship
918
+ // the full {ok, shape, item_count, payload:[], body, content_type}
919
+ // envelope — pure overhead when there is nothing to show. Collapse it to
920
+ // a compact 1-liner that names the likely culprits (template_fields /
921
+ // range) and forwards the upstream diagnostic verbatim when present.
922
+ // Only the array-empty case collapses; an empty object/scalar payload is
923
+ // a different (rare) shape and keeps the full envelope.
924
+ if (Array.isArray(trim.payload) && trim.itemCount === 0) {
925
+ const diagnosticMsg = diagnostic && typeof diagnostic.message === "string"
926
+ ? ` ${diagnostic.message}`
927
+ : "";
928
+ return formatResult({
929
+ ok: true,
930
+ item_count: 0,
931
+ note: "0 rows — check template_fields (e.g. spreadsheet_id / range) or the connection " +
932
+ "endpoint; sample the config with apimapper_connection_get({ id })." +
933
+ diagnosticMsg,
934
+ }, false, { maxChars: 600 });
935
+ }
792
936
  return formatResult({
793
937
  ok: true,
794
938
  // F-38 — surface a stable, narrow shape enum instead of typeof.
@@ -817,12 +961,14 @@ export function registerConnectionTools(server, elicitation) {
817
961
  title: "List Connection Resources",
818
962
  description: "List browseable resources on a connection (drive files, sheets, IG media). " +
819
963
  "REST contract: `field` (resource-picker field name) + optional `query` filter." +
820
- "\n\nExample:\n apimapper_connection_resources({ id: 'con_drive_xyz', field: 'fileId', query: 'budget' })",
964
+ // F213: gateway-only tool show the call routed THROUGH the read
965
+ // gateway so the first live invocation doesn't fail invalid_arguments.
966
+ "\n\nExample (gateway-routed):\n apimapper_advanced_read({ tool: \"apimapper_connection_resources\", arguments: { id: \"con_drive_xyz\", field: \"fileId\", query: \"budget\" } })",
821
967
  inputSchema: {
822
968
  id: z.string().describe("Connection ID. Use apimapper_connection_list to find."),
823
969
  field: z
824
970
  .string()
825
- .describe('Resource-picker field name (e.g., "spreadsheet_id", "drive_file_id"). REST wire-key: field.'),
971
+ .describe('REQUIRED — resource-picker field name (e.g., "spreadsheet_id", "drive_file_id"). REST wire-key: field.'),
826
972
  // rc.10 A4 (2026-05-19): min(3) blocks the single-char "p", "j", "u"
827
973
  // probing pattern observed in Maria-walkthrough logs.
828
974
  // rc.13 W1.17 (F-29): Master-Doc-Drift — Master requested min(2), kept
@@ -868,21 +1014,29 @@ export function registerConnectionTools(server, elicitation) {
868
1014
  server.registerTool("apimapper_connection_pipeline_update", {
869
1015
  title: "Update Connection Pipeline",
870
1016
  description: "Update the connection's pipeline (endpoints, default_params, default_headers, headers, items_path, items_shape, body, body_type, graphql_variables, response_type)." +
1017
+ "\n\nWorks on connections WITHOUT a stored pipeline: the protocol keys version/enabled/steps are NOT required — send only the connection-column keys you care about (F184)." +
1018
+ "\n\nPARTIAL UPDATES ARE SAFE (F145) — send only the keys you want to change:" +
1019
+ "\n • Object-map keys (default_params, default_headers, headers, graphql_variables) MERGE into the stored map: changing one entry preserves the others. Pass a value of null to delete a single stored entry (e.g. { default_params: { stale_key: null } })." +
1020
+ "\n • Scalar keys (items_path, items_shape, body, body_type, response_type) replace their single value." +
1021
+ "\n • List keys (endpoints) are REPLACED wholesale (positional merge is ambiguous) — resend every endpoint you want to keep. If a replace drops endpoints, the response carries a `warnings` array naming what was removed." +
871
1022
  "\n\nitems_shape values: 'flat' (default, list of objects), 'auto' (best-effort detection), 'headers_rows' (pivot 2D array [[h1,h2],[v1,v2],...] into named-object rows — use this for Google-Sheets-style responses), 'object_keys' (turn a keyed object into a list)." +
872
- "\n\nExample:\n apimapper_connection_pipeline_update({ id: 'con_abc123', pipeline: { items_path: 'photos', items_shape: 'auto', default_params: { per_page: '20' } } })" +
1023
+ "\n\nExample (partial — preserves any other stored params):\n apimapper_connection_pipeline_update({ id: 'con_abc123', pipeline: { items_path: 'photos', items_shape: 'auto', default_params: { per_page: '20' } } })" +
873
1024
  "\n\nGoogle-Sheets 2D array example:\n apimapper_connection_pipeline_update({ id: 'con_sheet', pipeline: { items_shape: 'headers_rows' } })",
874
1025
  inputSchema: {
875
1026
  id: z.string().describe("Connection ID. Use apimapper_connection_list to find."),
876
1027
  pipeline: z
877
1028
  .record(z.string(), z.unknown())
878
- .describe('Pipeline JSON. Accepts connection-column keys: endpoints, default_params, default_headers, headers, items_path, items_shape, body, body_type, graphql_variables, response_type. ' +
1029
+ .describe('Pipeline JSON (partial allowed). Accepts connection-column keys: endpoints, default_params, default_headers, headers, items_path, items_shape, body, body_type, graphql_variables, response_type. ' +
1030
+ 'Object-map keys (default_params/default_headers/headers/graphql_variables) MERGE into stored values (null deletes one entry); scalar keys replace; list keys (endpoints) replace wholesale and report dropped entries in a warnings array. ' +
879
1031
  'items_shape options: "flat" | "auto" | "headers_rows" | "object_keys". ' +
880
1032
  'Example: {"items_path":"data","items_shape":"headers_rows"}.'),
881
1033
  },
882
1034
  annotations: mutating({ title: "Update Connection Pipeline", openWorld: true }),
883
1035
  }, async ({ id, pipeline }) => {
884
1036
  // PHP ConnectionPipelineHandler uses fromControllerResponse(_, 'connection')
885
- // → {success, connection:{…}}. Unwrap defensively. Audit: F-A1-07.
1037
+ // → {success, connection:{…}}. F145: a list-key (endpoints) full-replace
1038
+ // also carries a top-level `warnings` array naming dropped entries.
1039
+ // Unwrap defensively. Audit: F-A1-07.
886
1040
  const r = await request(`/connections/${encodeURIComponent(id)}/pipeline`, {
887
1041
  method: "PUT",
888
1042
  body: JSON.stringify(pipeline),
@@ -891,7 +1045,20 @@ export function registerConnectionTools(server, elicitation) {
891
1045
  return restErrorResult(r, { id }, { message: "connection pipeline update failed" });
892
1046
  }
893
1047
  const conn = unwrapEntity(r.data, "connection");
894
- return formatResult({ updated: true, id: conn?.id, name: conn?.name }, false, { maxChars: 2000 });
1048
+ // Surface F145 dropped-entry warnings so the agent never silently loses
1049
+ // endpoints on a partial replace.
1050
+ const warnings = r.data && typeof r.data === "object" && Array.isArray(r.data.warnings)
1051
+ ? r.data.warnings
1052
+ : undefined;
1053
+ const payload = {
1054
+ updated: true,
1055
+ id: conn?.id,
1056
+ name: conn?.name,
1057
+ };
1058
+ if (warnings && warnings.length > 0) {
1059
+ payload.warnings = warnings;
1060
+ }
1061
+ return formatResult(payload, false, { maxChars: 2000 });
895
1062
  });
896
1063
  }
897
1064
  //# sourceMappingURL=connections.js.map