@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
@@ -7,20 +7,118 @@ Join items from two sources on a shared key, optionally filter the join, surface
7
7
  - You have two APIs whose rows belong together but live behind separate endpoints (orders + customers, Calendly slots + Sheet schedule, GitHub issues + sprint planning).
8
8
  - The keys you want to join on either (a) are already string-equal, or (b) need a small Transform to be made comparable (e.g. ISO datetime → weekday name).
9
9
 
10
- ## The canonical recipe
10
+ ## The canonical recipe (REST / MCP — no UI dragging)
11
11
 
12
12
  ```
13
13
  Source-A ─┐
14
- ├─► Merge (mode: join, key: <field>) ─► Filter ─► Output
14
+ ├─► Merge (strategy: join, joinKey: <field>) ─► Filter ─► Output
15
15
  Source-B ─┘
16
16
  ```
17
17
 
18
- 1. Drag both Source nodes onto the canvas, wire them up.
19
- 2. Drag a Merge node between them. Set:
20
- - `mode: join` — emit one row per Source-A item, with Source-B fields nested under `b`.
21
- - `key`: the field name present on BOTH sides (e.g. `weekday`).
22
- 3. (Optional) drag a Filter node after Merge — typical predicate uses both A- and B-side fields.
23
- 4. Wire to Output (YOOtheme Source) and Publish.
18
+ Build the whole thing in ONE call — `apimapper_flow_setup_with_sources` lays out the source merge → (filter) → output graph for you:
19
+
20
+ ```jsonc
21
+ apimapper_flow_setup_with_sources({
22
+ name: "Properties + Ratings",
23
+ sources: [
24
+ { connection: "con_properties" }, // Source-A
25
+ { connection: "con_ratings" } // Source-B
26
+ ],
27
+ merge: {
28
+ strategy: "join", // emit one row per matched A item, B fields flat-merged on (NOT nested under `b`)
29
+ joinKey: "property_id", // field on Source-A
30
+ joinKeyRight: "propertyId", // field on Source-B — OMIT when both sides name it the same
31
+ joinType: "left" // "left" keeps every A row; "inner" drops A rows with no B match
32
+ },
33
+ // optional: filter runs AFTER merge, references both A- and B-side fields by their bare names
34
+ filter: { field: "rating", operator: "gte", value: 4 },
35
+ output: { type: "yootheme", name: "RatedProperties" }
36
+ })
37
+ ```
38
+
39
+ Key points:
40
+ - `strategy: "join"` (NOT `mode`), `joinKey` (NOT `key`). The old UI words `mode`/`key` are not REST parameters.
41
+ - `joinKey` is the Source-A field; `joinKeyRight` is the Source-B field. When both sides use the **same** field name, set only `joinKey` (`joinKeyRight` defaults to it).
42
+ - Discover the right `joinKey`/`joinKeyRight` BEFORE you build — see the next section.
43
+ - Already have a saved flow? Edit the merge node with `apimapper_flow_update` (same `strategy`/`joinKey`/`joinKeyRight`/`joinType` node-data keys).
44
+
45
+ ## Discovering the join key
46
+
47
+ You almost never know the join key up front, and **guessing wrong yields 0 matched rows with no error**. Discover it by sampling both sources first.
48
+
49
+ 1. **Sample Source-A:**
50
+ ```
51
+ apimapper_connection_data({ id: "con_properties", limit: 3 })
52
+ ```
53
+ Note the candidate key fields and their values, e.g. `property_id: "P-100"` (a **string**).
54
+ 2. **Sample Source-B:**
55
+ ```
56
+ apimapper_connection_data({ id: "con_ratings", limit: 3 })
57
+ ```
58
+ e.g. `propertyId: 42` (a **number**).
59
+ 3. **Pick a key that is present and comparable on BOTH sides.** The join compares values with `===`-style equality, so the two keys must match in **type AND format**:
60
+ - string `"P-100"` never equals number `42` — even `"42"` never equals `42`.
61
+ - `" P-100"` (trailing/leading space) never equals `"P-100"`.
62
+ - `"p-100"` never equals `"P-100"` (case matters).
63
+ 4. **If the keys aren't comparable as-is, bridge them with a Transform** before the merge (see "When the keys aren't string-equal" below) — e.g. `to_string(propertyId)` on the number side, or a `date_weekday(...)` projection for datetime↔weekday joins.
64
+
65
+ ### When the join still returns 0 rows — the `merge_no_match` signal
66
+
67
+ If `apimapper_graph_preview` returns `item_count: 0` for a join, check the response for `merge_diagnostics[<mergeNodeId>]`:
68
+
69
+ ```jsonc
70
+ "merge_diagnostics": {
71
+ "merge-1": {
72
+ "reason": "merge_no_match",
73
+ "message": "Merge 'merge-1' (join inner) matched 0 rows: left joinKey 'property_id' (2 items) never equalled right joinKeyRight 'propertyId' (2 items). …",
74
+ "joinKey": "property_id",
75
+ "joinKeyRight": "propertyId",
76
+ "left_key_sample": ["P-100", "P-200"],
77
+ "right_key_sample": [42, 77]
78
+ }
79
+ }
80
+ ```
81
+
82
+ The `left_key_sample` / `right_key_sample` show the actual key values from each side. Compare them: above, the left side is strings (`"P-100"`) and the right side is numbers (`42`) — they can never match. Fix it by either picking a different key or adding a Transform to make them comparable, then re-preview.
83
+
84
+ ## The flat-merge contract (READ THIS)
85
+
86
+ The Merge join does **NOT** nest the B-side under a `b` object. There is no `b.` prefix and no option to enable one. Each matched B row is merged **flat onto the A row**, field by field, with this collision rule:
87
+
88
+ | Situation | Result |
89
+ |-----------|--------|
90
+ | B has a field A does **not** have | B's value is copied verbatim at the **top level** (e.g. `open`, `close`, `rating`, `email`). |
91
+ | Both sides carry the field with the **same** value | Kept once from A — no duplicate, no prefix. |
92
+ | Both sides carry the field with **different** values | A's value stays under the original key; **B's value is renamed `branch_1_<key>`** (the `branch_1` prefix denotes the secondary input handle `input-1`). |
93
+
94
+ So if Source-A is the orders feed and Source-B (a products sheet) carries `rating`, the merged row exposes `rating` at the top level — bind to `rating`, **not** `b.rating` (which is always null). Only when a field name collides does the B value move, and it moves to `branch_1_<field>`, never `b.<field>`.
95
+
96
+ ### Worked example
97
+
98
+ ```
99
+ Source-A (orders) row: { "id": 7, "product": "Widget", "status": "shipped" }
100
+ Source-B (products) row: { "id": 7, "rating": 4.5, "status": "active" }
101
+ ```
102
+
103
+ Join key `id`. The merged row is:
104
+
105
+ ```json
106
+ {
107
+ "id": 7,
108
+ "product": "Widget",
109
+ "status": "shipped",
110
+ "rating": 4.5,
111
+ "branch_1_status": "active"
112
+ }
113
+ ```
114
+
115
+ - `rating` — B-only field, flat at top level.
116
+ - `status` — present on BOTH with different values → A's `"shipped"` keeps the key, B's `"active"` becomes `branch_1_status`.
117
+ - `id` — same value on both sides → kept once, no prefix.
118
+
119
+ Bind YOOtheme element fields to `rating` and `branch_1_status`; binding to `b.rating` returns nothing.
120
+
121
+ > **`join` is left-join by default** (every A row survives; A rows with no B match pass through unchanged). Set `joinType: inner` to drop A rows that have no match. A 1:N or N:M match emits one merged row per B match (the cartesian product of the matching rows).
24
122
 
25
123
  ## When the keys aren't string-equal — date-primitive bridging
26
124
 
@@ -32,7 +130,7 @@ This is the case Cold-AI #5 walk-through hit: Calendly emits `start_time` as ISO
32
130
 
33
131
  ```
34
132
  Calendly available_times ─► Transform (project day+local_time) ─┐
35
- ├─► Merge (key: day) ─► Filter ─► Output
133
+ ├─► Merge (strategy: join, joinKey: day) ─► Filter ─► Output
36
134
  Google Sheet schedule ──────────────────────────────────────┘
37
135
  ```
38
136
 
@@ -56,21 +154,25 @@ Each Calendly row now carries:
56
154
 
57
155
  #### Step 2 — Merge on `day`
58
156
 
59
- Merge mode `join`, key `day`. Sheet row arrives as `b` on each Calendly row:
157
+ Merge `strategy: "join"`, `joinKey: "day"` (both sides name it `day`/`weekday` — set `joinKeyRight: "weekday"` if the Sheet column is `weekday` rather than `day`). The matching Sheet row is **flat-merged** onto each Calendly row — its `weekday`, `open`, and `close` fields land at the top level (the Calendly side carries `day`/`local_time`/`start_time`, so there is no field-name collision and nothing gets a `branch_1_` prefix):
60
158
 
61
159
  ```json
62
160
  {
63
161
  "day": "Wednesday",
64
162
  "local_time": "09:00",
65
163
  "start_time": "2026-06-03T07:00:00+00:00",
66
- "b": { "weekday": "Wednesday", "open": "09:00", "close": "17:00" }
164
+ "weekday": "Wednesday",
165
+ "open": "09:00",
166
+ "close": "17:00"
67
167
  }
68
168
  ```
69
169
 
70
170
  #### Step 3 — Filter to slots inside business hours
71
171
 
172
+ Reference the Sheet's `open`/`close` by their **bare top-level names** — there is no `b.` prefix:
173
+
72
174
  ```
73
- [?time_in_window(local_time, b.open, b.close)]
175
+ [?time_in_window(local_time, open, close)]
74
176
  ```
75
177
 
76
178
  `time_in_window` is left-closed, right-open ([from, to)) — a 17:00 close-time does NOT include a 17:00 slot. Cross-midnight windows (e.g. bar `22:00..02:00`) are also handled.
@@ -89,9 +191,12 @@ Wire to a YOOtheme Output. Slots that survive Steps 1-3 become the rows of a YOO
89
191
 
90
192
  ## Pitfalls
91
193
 
194
+ - **No `b.` namespace.** B-side fields are flat-merged onto the row (see "The flat-merge contract"). Bind to the bare field name (`rating`, `open`), never `b.rating` / `b.open` — those resolve to null. The only time a B value moves is a name collision, and then it moves to `branch_1_<key>`, not `b.<key>`.
195
+ - **Collisions are silent unless you name them.** If both sources carry a field of the same name with different values (e.g. both have `status`), A wins the original key and B is preserved as `branch_1_status`. If you actually wanted B's value, either rename it in a Transform on the B side before the Merge, or read `branch_1_status` downstream.
92
196
  - **Timezone matters.** A slot at `2026-06-04T23:30:00Z` is Wednesday in UTC but Thursday in Berlin. Always supply the second `tz` arg matching the customer's business timezone.
93
197
  - **`null` rows propagate.** A Calendly row with a malformed `start_time` projects to `{day: null, local_time: null, start_time: <bad>}`. The Merge will silently emit it under a `null` key — pre-filter with `[?day]` if you need to drop them.
94
198
  - **Depth limit.** Don't try to do the whole projection + merge + filter inside ONE expression. JMESPath caps depth at 10. Two transform nodes piped is cleaner and stays under the limit.
199
+ - **0 rows? Read `merge_diagnostics`.** A join that matches nothing comes back as `item_count: 0`, not an error. `apimapper_graph_preview` attaches `merge_diagnostics[<mergeNodeId>]` (`reason: merge_no_match`) with the keys and a `left_key_sample`/`right_key_sample` so you can spot the type/format mismatch. Don't guess — sample both sides with `apimapper_connection_data` (see "Discovering the join key").
95
200
 
96
201
  ## See also
97
202
 
@@ -2,93 +2,184 @@
2
2
 
3
3
  Wiring OAuth-protected sources (Pexels API key, Google Sheets/Drive, Instagram Graph, Facebook Pages, Notion, Airtable, …) through API Mapper.
4
4
 
5
+ > **Schema note (read first).** Every snippet below uses the EXACT tool parameters the server accepts. The wire contract is snake_case: credentials carry `auth_type` / `auth_data` / `oauth_provider`; connections carry `endpoint` / `auth_type` / `credential_id`. The credential's auth shape is `auth_type` (never a bare `type`), its secrets live under `auth_data` (no top-level `scopes`), and a connection's request is described by `endpoint` + `method` (there is no driver-slug parameter and no nested driver-config object). Older drafts that showed those phantom params were wrong and the tools reject them.
6
+
7
+ ## Tool-surface map (which tools are top-level vs gateway)
8
+
9
+ | Tool | Surface | How to call |
10
+ |------|---------|-------------|
11
+ | `apimapper_library_featured`, `apimapper_library_list`, `apimapper_library_activate` | top-level | call directly |
12
+ | `apimapper_connection_create`, `apimapper_connection_data`, `apimapper_connection_list` | top-level | call directly |
13
+ | `apimapper_credential_list`, `apimapper_oauth_authorize_begin` | top-level | call directly |
14
+ | `apimapper_credential_create`, `apimapper_credential_get`, `apimapper_connection_test` | **advanced** | via `apimapper_advanced({ tool, arguments })` |
15
+
16
+ A bare `apimapper_credential_create({…})` returns "No such tool" — it is reachable **only** through the `apimapper_advanced` gateway. The same is true of `apimapper_credential_get` and `apimapper_connection_test`.
17
+
5
18
  ## When to use OAuth vs static key
6
19
 
7
- | Auth shape | Examples | Credential type |
8
- |------------|----------|-----------------|
9
- | Static API key in header | Pexels, NewsAPI, OpenWeather | `bearer` or `apikey` |
10
- | OAuth2 authorization-code | Google Sheets/Drive, Instagram, Facebook, Notion | `oauth2_authcode` |
11
- | OAuth2 client-credentials | Spotify (catalog), some B2B APIs | `oauth2_clientcred` |
12
- | Personal access token | GitHub, GitLab, Airtable | `bearer` |
20
+ | Auth shape | Examples | `auth_type` (credential) |
21
+ |------------|----------|--------------------------|
22
+ | Static API key | Pexels, NewsAPI, OpenWeather | `api_key` |
23
+ | Bearer / personal access token | GitHub, GitLab, Airtable, Notion | `bearer` |
24
+ | Basic auth (user + pass) | legacy/internal APIs | `basic_auth` |
25
+ | OAuth2 authorization-code (user consent + refresh token) | Google Sheets/Drive, Instagram, Facebook | `oauth2_code` |
26
+ | OAuth2 client-credentials (machine-to-machine) | Spotify catalog, some B2B APIs | `oauth2_cc` |
13
27
 
14
- If the API requires a user-consent screen and returns a `refresh_token`, you want `oauth2_authcode`. Everything else is `bearer`/`apikey`.
28
+ If the API needs a user-consent screen and returns a `refresh_token`, use `oauth2_code`. Everything else is `bearer` / `api_key` / `basic_auth`.
15
29
 
16
- ## Wiring an OAuth source full flow
30
+ ## The canonical path for a covered API: activate a library template
17
31
 
18
- The two-step pattern is **always**: create credential bind credential to connection. Credentials are reusable across connections.
32
+ For Google Sheets, Drive, Docs, Slides, Tasks, Calendly, Notion, Airtable, GitHub, Pexels, Unsplash, OpenWeatherMap, REST Countries and more, **do not hand-build a credential + connection.** Activate the curated template — it pre-wires the auth shape, scopes, and field detection, and the server's library-first guard will block a custom create on a covered host anyway.
19
33
 
20
- ### Step 1 — Create the credential
34
+ ```jsonc
35
+ // 1. Find the template.
36
+ apimapper_library_list({ search: "google sheets" }) // min 3 chars
21
37
 
38
+ // 2. (OAuth templates) reuse an existing credential if you have one,
39
+ // otherwise create one — see "Creating an OAuth credential" below.
40
+ apimapper_credential_list({}) // top-level; find a reusable cred
41
+
42
+ // 3. Activate. credential_id is auto-resolved when exactly ONE OAuth
43
+ // credential for the provider exists; pass it explicitly otherwise.
44
+ apimapper_library_activate({
45
+ id: "google-sheets",
46
+ credential_id: "cred_google", // optional when unique
47
+ extra_fields: { spreadsheet_id: "1AbC2dEf...REPLACE_WITH_ID" }
48
+ })
49
+ ```
50
+
51
+ `extra_fields` carries the template placeholder values (for Sheets that is the **bare** spreadsheet id — see the note below). `apimapper_library_activate` produces a ready connection; you never touch `connection_create` for a covered API.
52
+
53
+ > **Spreadsheet id, not URL.** `extra_fields.spreadsheet_id` wants the bare id (`1AbC2dEf…`), not the full `https://docs.google.com/spreadsheets/d/…/edit` Drive URL. Paste only the path segment between `/d/` and the next `/`.
54
+
55
+ ## Creating an OAuth credential (when none is reusable)
56
+
57
+ `apimapper_credential_create` is an **advanced** tool — call it through the gateway. It does **not** return an authorize URL; you trigger consent separately with `apimapper_oauth_authorize_begin`.
58
+
59
+ ```jsonc
60
+ // Step 1 — create the oauth2_code credential (via the gateway).
61
+ apimapper_advanced({
62
+ tool: "apimapper_credential_create",
63
+ arguments: {
64
+ name: "My Google Account",
65
+ auth_type: "oauth2_code",
66
+ oauth_provider: "google",
67
+ auth_data: {
68
+ client_id: "…",
69
+ client_secret: "…",
70
+ authorization_url: "https://accounts.google.com/o/oauth2/v2/auth",
71
+ token_url: "https://oauth2.googleapis.com/token",
72
+ scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"]
73
+ }
74
+ }
75
+ })
76
+ // → { created: true, id: "cred_…", name, auth_type } (NO authorize_url here)
22
77
  ```
23
- apimapper_credential_create(
24
- name="My Google Account",
25
- type="oauth2_authcode",
26
- provider="google",
27
- scopes=["https://www.googleapis.com/auth/spreadsheets.readonly"]
28
- )
78
+
79
+ ```jsonc
80
+ // Step 2 — begin authorization → get the consent URL.
81
+ apimapper_oauth_authorize_begin({ credential_id: "cred_…" })
82
+ // → { authorize_url: "https://accounts.google.com/o/oauth2/v2/auth?…" }
29
83
  ```
30
84
 
31
- The tool returns a `credential_id` and an `authorize_url`. The caller (Claude / ChatGPT / Cursor) **must** surface the `authorize_url` to the user — the user clicks it, completes consent, the OAuth callback writes the `access_token` + `refresh_token` into the credential record.
85
+ The caller (Claude / ChatGPT / Cursor) **must surface `authorize_url` to the user** — the user clicks it, completes consent, and the OAuth callback writes the `access_token` + `refresh_token` into the credential. If you skip surfacing the URL the credential stays `pending` and every call returns 401.
32
86
 
33
- If you skip surfacing the URL, the credential will stay in `pending` state and every subsequent call will return 401.
87
+ `apimapper_oauth_authorize_begin` also resolves by `provider` when you omit `credential_id`: `apimapper_oauth_authorize_begin({ provider: "google" })` picks the unique `oauth2_code` credential for that provider (or asks you to choose when several match).
34
88
 
35
- ### Step 2 Begin authorization (when refresh failed)
89
+ ### Re-authorizing an expired credential
36
90
 
37
- If the credential is `expired` or the refresh token was revoked, re-trigger consent:
91
+ If the credential is `expired` or its refresh token was revoked, re-trigger consent — **never delete-and-recreate** (that wipes the refresh token):
38
92
 
93
+ ```jsonc
94
+ apimapper_oauth_authorize_begin({ credential_id: "cred_…" })
39
95
  ```
40
- apimapper_oauth_authorize_begin(credential_id="cred_abc123")
96
+
97
+ Custom-provider (non-catalog) credentials re-authorize the same way — the server now recovers the stored `authorization_url` / `token_url` from the credential's `auth_data`, so a bare `apimapper_oauth_authorize_begin({ credential_id })` works without re-passing `oauth_config`.
98
+
99
+ ### A CUSTOM OAuth provider not in the catalog (authorization-code)
100
+
101
+ For an authorization-code provider that has **no** library template (e.g. Teamleader, a self-hosted Keycloak, a niche SaaS), drive the whole consent flow in one call by passing `oauth_config` to `apimapper_oauth_authorize_begin`. You do **not** pre-create the credential — the callback creates it for you.
102
+
103
+ ```jsonc
104
+ apimapper_oauth_authorize_begin({
105
+ client_id: "…",
106
+ client_secret: "…",
107
+ credential_name: "Teamleader Prod", // display name for the new credential
108
+ oauth_config: {
109
+ authorization_url: "https://app.teamleader.eu/oauth2/authorize",
110
+ token_url: "https://app.teamleader.eu/oauth2/access_token",
111
+ scopes: [], // OPTIONAL — omit/empty for scope-less providers
112
+ use_pkce: false // OPTIONAL — default true; turn off if the provider rejects PKCE
113
+ }
114
+ })
115
+ // → { authorize_url: "https://app.teamleader.eu/oauth2/authorize?…",
116
+ // redirect_uri: "https://your-site/wp-json/api-mapper/v1/oauth/callback",
117
+ // state: "…" }
41
118
  ```
42
119
 
43
- Returns a new `authorize_url`. Same surface-it-to-the-user rule applies.
120
+ **Before opening `authorize_url`, add the returned `redirect_uri` to the provider's allowed-redirect list.** A mismatched redirect URI is the single most common custom-provider failure — the provider rejects the callback with `redirect_uri_mismatch` and no credential is created. The `redirect_uri` is platform-specific (WordPress `…/wp-json/api-mapper/v1/oauth/callback`; Joomla a path-based `…/index.php/apimapper/oauth/callback`), so always whitelist the exact value the response gives you.
44
121
 
45
- ### Step 3 — Create the connection bound to the credential
122
+ Notes:
123
+ - **Scopes are optional.** Some providers (Teamleader class) reject a `scope` parameter entirely — pass `scopes: []` or omit it. For comma-separated providers, the server normalizes the delimiter.
124
+ - **PKCE defaults on.** Leave `use_pkce` unset for modern providers; set it to `false` only if the provider does not support PKCE.
125
+ - The `oauth_config` you pass is **never** echoed back in the response (secret hygiene) — only `authorize_url`, `redirect_uri`, and `state` are returned.
46
126
 
47
- ```
48
- apimapper_connection_create(
49
- name="Sheets Marketing OKRs",
50
- source_type="google_sheets",
51
- credential_id="cred_abc123",
52
- config={ "spreadsheet_id": "1AbC...", "range": "OKRs!A1:F100" }
53
- )
127
+ ## Creating a CUSTOM connection (only when no template fits)
128
+
129
+ When the API is genuinely **not** covered by any library template, build a custom connection. `apimapper_connection_create` is top-level; its real parameters are `endpoint`, `method`, `auth_type` (`none` / `api_key` / `bearer` / `basic` / `oauth2`), `credential_id`, `items_path`, `cache_ttl`.
130
+
131
+ ```jsonc
132
+ // Static-key example (Pexels-style, if it had no template).
133
+ apimapper_connection_create({
134
+ name: "My Niche API",
135
+ endpoint: "https://api.example.com/v1/items",
136
+ method: "GET",
137
+ auth_type: "bearer",
138
+ credential_id: "cred_…", // the bearer credential created via the gateway
139
+ items_path: "data" // JSON path to the array of rows
140
+ })
54
141
  ```
55
142
 
56
- `source_type` selects the upstream driver (REST request shape, response normalizer, schema profiler). `config` is driver-specific.
143
+ The request shape comes entirely from `endpoint` + `method`, and auth from `credential_id` — there is no driver-slug parameter and no nested driver-config object to set. If the host is already covered by a template the server returns a **409** naming the template to activate; only set `acknowledge_no_library: true` **after** you receive that 409 and are sure the endpoint is genuinely uncovered.
57
144
 
58
- ### Step 4 — Verify
145
+ ## Verify
59
146
 
60
- ```
61
- apimapper_connection_test(connection_id="conn_xyz")
147
+ ```jsonc
148
+ // connection_test is advanced — call via the gateway.
149
+ apimapper_advanced({ tool: "apimapper_connection_test", arguments: { connection_id: "conn_…" } })
150
+
151
+ // Or sample real rows (top-level):
152
+ apimapper_connection_data({ id: "conn_…", limit: 3 })
62
153
  ```
63
154
 
64
- Returns one of:
65
- - `{ ok: true, sample: { ... } }` — green, you can build a flow on this
66
- - `{ ok: false, error: "401", hint: "credential expired → call apimapper_oauth_authorize_begin" }`
67
- - `{ ok: false, error: "scope_missing", hint: "credential lacks scope X → recreate with scopes=[...]" }`
155
+ A green test (or non-empty `connection_data`) means you can build a flow on the connection.
68
156
 
69
157
  ## Common pitfalls
70
158
 
71
159
  1. **Recreating credentials after expiry** — wipes the `refresh_token`. Always `apimapper_oauth_authorize_begin` first; only delete-and-recreate if the user revoked access on the provider side.
72
- 2. **Hardcoding tokens into `connection.config.auth.token`** — bypasses the credential system, breaks rotation, fails on token expiry. Always use `credential_id`.
73
- 3. **Forgetting to surface the `authorize_url`** — the user can't grant consent telepathically. Treat the URL as a required UI step, not a debug log line.
74
- 4. **Scope drift** — provider adds new scopes (e.g. Google's `drive.readonly` split). Add them to the credential definition and re-authorize; pre-existing tokens lack them silently until a call returns 403.
75
- 5. **Sharing a credential across users** — credentials are per-user (tied to the WordPress/Joomla user that created them). Cross-user share is not supported in 2.0.x.
160
+ 2. **Hardcoding tokens** — bypasses the credential system, breaks rotation. Always reference a `credential_id`.
161
+ 3. **Forgetting to surface `authorize_url`** — the user can't grant consent telepathically. Treat the URL as a required UI step.
162
+ 4. **Calling `apimapper_credential_create` directly** — it is advanced; route it through `apimapper_advanced`. Same for `apimapper_credential_get` and `apimapper_connection_test`.
163
+ 5. **Custom-creating a covered API** — you'll hit a 409. Activate the template instead (`apimapper_library_activate`).
164
+ 6. **Pasting a Drive URL where a bare id is expected** — `extra_fields.spreadsheet_id` is the bare id, not the URL.
165
+ 7. **Sharing a credential across users** — credentials are per-user (tied to the WordPress/Joomla user that created them). Cross-user share is not supported in 2.0.x.
76
166
 
77
167
  ## Provider-specific notes
78
168
 
79
- - **Google** (Sheets/Drive/Calendar/Tasks) — needs `access_type=offline` and `prompt=consent` to issue a refresh token. The MCP server adds these automatically. If you bypass the MCP and craft the URL by hand, you must add them.
80
- - **Instagram Graph** — token expires every 60 days, refresh-token flow is non-standard (long-lived → exchange). API Mapper's refresh handler covers it; do not call the Meta endpoints directly.
81
- - **Notion / Airtable** — issue personal access tokens, not full OAuth. Use `bearer` credential type and paste the token; no `authorize_url` step.
82
- - **Pexels** — static API key; use `apikey` credential type. No consent screen.
169
+ - **Google** (Sheets/Drive/Calendar/Tasks) — needs `access_type=offline` + `prompt=consent` for a refresh token; the server adds these automatically. Activate the `google-sheets` / `google-drive` templates rather than hand-building.
170
+ - **Instagram Graph** — token expires every 60 days; the refresh flow is non-standard (long-lived → exchange). The server's refresh handler covers it.
171
+ - **Notion / Airtable** — issue personal access tokens, not full OAuth. Use `auth_type: 'bearer'` and paste the token in `auth_data: { token: "…" }`; no `authorize_url` step.
172
+ - **Pexels** — static API key; `auth_type: 'api_key'`, `auth_data: { api_key: "…" }`. No consent screen.
83
173
 
84
174
  ## Diagnose flow
85
175
 
86
- ```
87
- apimapper_credential_get(credential_id="cred_abc123")
88
- # { status: "expired", last_refresh_at: "2026-04-12T...", scopes: [...] }
176
+ ```jsonc
177
+ apimapper_diagnose({ connection_id: "conn_…" })
178
+ // consolidates credential state + last test + recent logs
89
179
 
90
- apimapper_diagnose(connection_id="conn_xyz")
91
- #consolidates credential state + last connection_test + recent logs
180
+ apimapper_advanced({ tool: "apimapper_credential_get", arguments: { id: "cred_…" } })
181
+ //sanitised metadata: { status: "expired", scopes: [...] } (secrets redacted)
92
182
  ```
93
183
 
94
- For schema/transform issues (not auth issues), see `skill://apimapper/troubleshooting`.
184
+ For schema/transform issues (not auth issues), see `apimapper_get_skill({ topic: "troubleshooting" })`.
185
+ For the post-publish handoff into the YOOtheme Builder, see `apimapper_get_skill({ topic: "yootheme-source-to-builder-handoff" })`.
@@ -5,10 +5,10 @@ Diagnostic checklist when API Mapper tools return errors or flows misbehave. Wor
5
5
  ## Quick triage
6
6
 
7
7
  ```
8
- apimapper_diagnose(connection_id="conn_xyz") # one-call summary
8
+ apimapper_diagnose({ token: "amk_live_…", siteUrl: "https://example.com/wp" }) # validate a token+siteUrl pair
9
9
  ```
10
10
 
11
- `apimapper_diagnose` (Phase 8 composite tool) consolidates: credential state, last connection_test, recent error logs, license tier, schema profile. Use it FIRST before drilling into individual tools.
11
+ `apimapper_diagnose` takes a **`token` + `siteUrl`** pair (NOT a `connection_id`). It runs token decode, an identity probe (WordPress first, Joomla fallback), an `iss`/site-URL match (anti-phishing), and capability + scope checks, then returns `ready: true/false` plus structured `blockers[]` and a recommendation. Use it FIRST when a token won't authenticate or before creating a profile — it pinpoints whether the problem is the token, the site URL, the platform, or a missing scope.
12
12
 
13
13
  If `apimapper_diagnose` is unavailable in your version, run the manual checks below.
14
14