@zackbart/connecta 0.15.0 → 0.16.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 (332) hide show
  1. package/AGENTS.md +28 -15
  2. package/CHANGELOG.md +493 -0
  3. package/README.md +19 -4
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +0 -1
  27. package/dist/catalog.d.ts +0 -1
  28. package/dist/catalog.js +0 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +0 -1
  42. package/dist/errors.js +0 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +5 -2
  78. package/dist/providers/cloudflare.js +435 -328
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -60
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -78
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -59
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +95 -27
  129. package/documentation/code-mode.md +12 -12
  130. package/documentation/connector-guides.md +172 -5
  131. package/documentation/connectors.md +98 -7
  132. package/documentation/linear.md +26 -9
  133. package/documentation/mcp-ui-design.md +2 -2
  134. package/documentation/mixpanel.md +48 -16
  135. package/documentation/notion.md +16 -0
  136. package/documentation/operations.md +283 -5
  137. package/documentation/operator-ui.md +120 -5
  138. package/documentation/provider-audit.md +160 -0
  139. package/documentation/provider-conventions.md +631 -0
  140. package/documentation/request-admission.md +167 -5
  141. package/documentation/rich-output-design.md +1 -2
  142. package/documentation/stripe.md +27 -12
  143. package/ethos.md +14 -9
  144. package/examples/worker/README.md +96 -12
  145. package/examples/worker/src/index.ts +46 -1
  146. package/examples/worker/wrangler.jsonc +14 -1
  147. package/package.json +4 -5
  148. package/templates/node/.dockerignore +10 -0
  149. package/templates/node/.env.example +40 -3
  150. package/templates/node/AGENTS.md +11 -1
  151. package/templates/node/Dockerfile +49 -0
  152. package/templates/node/README.md +117 -2
  153. package/templates/node/docker-compose.yml +39 -0
  154. package/templates/node/package.json +1 -1
  155. package/templates/node/src/file-activity.ts +182 -0
  156. package/templates/node/src/index.ts +80 -4
  157. package/assets/connecta-clay-hero.png +0 -0
  158. package/dist/access-tokens.d.ts.map +0 -1
  159. package/dist/access-tokens.js.map +0 -1
  160. package/dist/activity.d.ts.map +0 -1
  161. package/dist/activity.js.map +0 -1
  162. package/dist/apps-shell.d.ts.map +0 -1
  163. package/dist/apps-shell.js.map +0 -1
  164. package/dist/auth/bearer.d.ts.map +0 -1
  165. package/dist/auth/bearer.js.map +0 -1
  166. package/dist/auth/clerk.d.ts.map +0 -1
  167. package/dist/auth/clerk.js.map +0 -1
  168. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  169. package/dist/auth/downstream-oauth.js.map +0 -1
  170. package/dist/call-admission.d.ts.map +0 -1
  171. package/dist/call-admission.js.map +0 -1
  172. package/dist/catalog-fingerprint.d.ts.map +0 -1
  173. package/dist/catalog-fingerprint.js.map +0 -1
  174. package/dist/catalog-limits.d.ts.map +0 -1
  175. package/dist/catalog-limits.js.map +0 -1
  176. package/dist/catalog-service.d.ts.map +0 -1
  177. package/dist/catalog-service.js.map +0 -1
  178. package/dist/catalog.d.ts.map +0 -1
  179. package/dist/catalog.js.map +0 -1
  180. package/dist/concurrency.d.ts.map +0 -1
  181. package/dist/concurrency.js.map +0 -1
  182. package/dist/connector-scope.d.ts.map +0 -1
  183. package/dist/connector-scope.js.map +0 -1
  184. package/dist/connectors/api.d.ts.map +0 -1
  185. package/dist/connectors/api.js.map +0 -1
  186. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  187. package/dist/connectors/remote-mcp.js.map +0 -1
  188. package/dist/credentials.d.ts.map +0 -1
  189. package/dist/credentials.js.map +0 -1
  190. package/dist/errors.d.ts.map +0 -1
  191. package/dist/errors.js.map +0 -1
  192. package/dist/execute.d.ts.map +0 -1
  193. package/dist/execute.js.map +0 -1
  194. package/dist/executor-admission.d.ts.map +0 -1
  195. package/dist/executor-admission.js.map +0 -1
  196. package/dist/executor-result.d.ts.map +0 -1
  197. package/dist/executor-result.js.map +0 -1
  198. package/dist/executors/quickjs-child.d.ts.map +0 -1
  199. package/dist/executors/quickjs-child.js.map +0 -1
  200. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  201. package/dist/executors/quickjs-protocol.js.map +0 -1
  202. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  203. package/dist/executors/quickjs-runtime.js.map +0 -1
  204. package/dist/executors/quickjs.d.ts.map +0 -1
  205. package/dist/executors/quickjs.js.map +0 -1
  206. package/dist/favicon.d.ts.map +0 -1
  207. package/dist/favicon.js.map +0 -1
  208. package/dist/index.d.ts.map +0 -1
  209. package/dist/index.js.map +0 -1
  210. package/dist/invocation.d.ts.map +0 -1
  211. package/dist/invocation.js.map +0 -1
  212. package/dist/json-schema.d.ts.map +0 -1
  213. package/dist/json-schema.js.map +0 -1
  214. package/dist/mcp-result.d.ts.map +0 -1
  215. package/dist/mcp-result.js.map +0 -1
  216. package/dist/meta-tools.d.ts.map +0 -1
  217. package/dist/meta-tools.js.map +0 -1
  218. package/dist/node.d.ts.map +0 -1
  219. package/dist/node.js.map +0 -1
  220. package/dist/operator-ui/generated.d.ts.map +0 -1
  221. package/dist/operator-ui/generated.js.map +0 -1
  222. package/dist/operator-ui/model.d.ts.map +0 -1
  223. package/dist/operator-ui/model.js.map +0 -1
  224. package/dist/providers/cloudflare.d.ts.map +0 -1
  225. package/dist/providers/cloudflare.js.map +0 -1
  226. package/dist/providers/linear.d.ts.map +0 -1
  227. package/dist/providers/linear.js.map +0 -1
  228. package/dist/providers/mixpanel.d.ts.map +0 -1
  229. package/dist/providers/mixpanel.js.map +0 -1
  230. package/dist/providers/notion.d.ts.map +0 -1
  231. package/dist/providers/notion.js.map +0 -1
  232. package/dist/providers/stripe.d.ts.map +0 -1
  233. package/dist/providers/stripe.js.map +0 -1
  234. package/dist/registry.d.ts.map +0 -1
  235. package/dist/registry.js.map +0 -1
  236. package/dist/retired-toolkits.d.ts.map +0 -1
  237. package/dist/retired-toolkits.js.map +0 -1
  238. package/dist/routes/access-tokens.d.ts.map +0 -1
  239. package/dist/routes/access-tokens.js.map +0 -1
  240. package/dist/routes/activity.d.ts.map +0 -1
  241. package/dist/routes/activity.js.map +0 -1
  242. package/dist/routes/credentials.d.ts.map +0 -1
  243. package/dist/routes/credentials.js.map +0 -1
  244. package/dist/routes/mcp.d.ts.map +0 -1
  245. package/dist/routes/mcp.js.map +0 -1
  246. package/dist/routes/oauth.d.ts.map +0 -1
  247. package/dist/routes/oauth.js.map +0 -1
  248. package/dist/routes/shared.d.ts.map +0 -1
  249. package/dist/routes/shared.js.map +0 -1
  250. package/dist/routes/ui.d.ts.map +0 -1
  251. package/dist/routes/ui.js.map +0 -1
  252. package/dist/server.d.ts.map +0 -1
  253. package/dist/server.js.map +0 -1
  254. package/dist/skills.d.ts.map +0 -1
  255. package/dist/skills.js.map +0 -1
  256. package/dist/storage/file.d.ts.map +0 -1
  257. package/dist/storage/file.js.map +0 -1
  258. package/dist/storage/memory.d.ts.map +0 -1
  259. package/dist/storage/memory.js.map +0 -1
  260. package/dist/timeout.d.ts.map +0 -1
  261. package/dist/timeout.js.map +0 -1
  262. package/dist/tool-safety.d.ts.map +0 -1
  263. package/dist/tool-safety.js.map +0 -1
  264. package/dist/types.d.ts.map +0 -1
  265. package/dist/types.js.map +0 -1
  266. package/dist/ui.d.ts.map +0 -1
  267. package/dist/ui.js.map +0 -1
  268. package/dist/validate.d.ts.map +0 -1
  269. package/dist/validate.js.map +0 -1
  270. package/dist/version.d.ts.map +0 -1
  271. package/dist/version.js.map +0 -1
  272. package/examples/node/README.md +0 -52
  273. package/examples/node/src/index.ts +0 -71
  274. package/src/access-tokens.ts +0 -289
  275. package/src/activity.ts +0 -234
  276. package/src/apps-shell.ts +0 -370
  277. package/src/auth/bearer.ts +0 -61
  278. package/src/auth/clerk.ts +0 -553
  279. package/src/auth/downstream-oauth.ts +0 -575
  280. package/src/call-admission.ts +0 -521
  281. package/src/catalog-fingerprint.ts +0 -52
  282. package/src/catalog-limits.ts +0 -11
  283. package/src/catalog-service.ts +0 -1153
  284. package/src/catalog.ts +0 -790
  285. package/src/concurrency.ts +0 -39
  286. package/src/connector-scope.ts +0 -66
  287. package/src/connectors/api.ts +0 -159
  288. package/src/connectors/remote-mcp.ts +0 -1090
  289. package/src/credentials.ts +0 -428
  290. package/src/errors.ts +0 -364
  291. package/src/execute.ts +0 -1406
  292. package/src/executor-admission.ts +0 -328
  293. package/src/executor-result.ts +0 -107
  294. package/src/executors/quickjs-child.ts +0 -172
  295. package/src/executors/quickjs-protocol.ts +0 -99
  296. package/src/executors/quickjs-runtime.ts +0 -470
  297. package/src/executors/quickjs.ts +0 -776
  298. package/src/favicon.ts +0 -53
  299. package/src/index.ts +0 -666
  300. package/src/invocation.ts +0 -655
  301. package/src/json-schema.ts +0 -11
  302. package/src/mcp-result.ts +0 -43
  303. package/src/meta-tools.ts +0 -1607
  304. package/src/node.ts +0 -206
  305. package/src/operator-ui/browser.css +0 -423
  306. package/src/operator-ui/browser.ts +0 -1281
  307. package/src/operator-ui/generated.ts +0 -4
  308. package/src/operator-ui/model.ts +0 -111
  309. package/src/providers/cloudflare.ts +0 -3803
  310. package/src/providers/linear.ts +0 -301
  311. package/src/providers/mixpanel.ts +0 -228
  312. package/src/providers/notion.ts +0 -1879
  313. package/src/providers/stripe.ts +0 -306
  314. package/src/registry.ts +0 -1085
  315. package/src/retired-toolkits.ts +0 -14
  316. package/src/routes/access-tokens.ts +0 -115
  317. package/src/routes/activity.ts +0 -215
  318. package/src/routes/credentials.ts +0 -254
  319. package/src/routes/mcp.ts +0 -429
  320. package/src/routes/oauth.ts +0 -322
  321. package/src/routes/shared.ts +0 -247
  322. package/src/routes/ui.ts +0 -152
  323. package/src/server.ts +0 -195
  324. package/src/skills.ts +0 -271
  325. package/src/storage/file.ts +0 -123
  326. package/src/storage/memory.ts +0 -44
  327. package/src/timeout.ts +0 -84
  328. package/src/tool-safety.ts +0 -15
  329. package/src/types.ts +0 -523
  330. package/src/ui.ts +0 -744
  331. package/src/validate.ts +0 -387
  332. package/src/version.ts +0 -7
@@ -16,6 +16,7 @@
16
16
  * required-key list, and a declared output shape.
17
17
  */
18
18
  import { api } from "../connectors/api.js";
19
+ import { guardedFetch, } from "../connectors/guarded-fetch.js";
19
20
  import { ConnectorCallError } from "../errors.js";
20
21
  /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
21
22
  export const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
@@ -91,11 +92,52 @@ function admissionPolicy(maxConcurrency) {
91
92
  ],
92
93
  };
93
94
  }
94
- const DEFAULT_CREDENTIAL = {
95
+ const API_TOKEN_CREDENTIAL = {
95
96
  label: "Cloudflare API token",
96
97
  description: "A scoped API token (My Profile → API Tokens → Create Token), not a Global API Key. Grant only the permissions the deployment needs: zone-scoped \"Zone Read\", \"Zone Settings Write\", \"DNS Write\", \"Cache Purge\", and the phase-specific Rules product Read permissions as needed; account-scoped \"Workers Scripts Read/Write\", \"Workers KV Storage Read/Write\", \"Workers R2 Storage Read/Write\", or \"Cloudflare Pages Read/Write\" for the platform tools.",
97
98
  placeholder: "Paste API token",
98
99
  };
100
+ const GLOBAL_API_KEY_CREDENTIAL = {
101
+ label: "Cloudflare Global API Key",
102
+ description: "Legacy user-scoped authentication. The key has the same access as its Cloudflare user across every account and zone that user can reach. Prefer a scoped API token when possible.",
103
+ fields: [
104
+ {
105
+ name: "email",
106
+ label: "Account email",
107
+ description: "The verified email address for the Cloudflare user that owns the Global API Key.",
108
+ placeholder: "you@example.com",
109
+ inputType: "email",
110
+ },
111
+ {
112
+ name: "apiKey",
113
+ label: "Global API Key",
114
+ description: "The legacy Global API Key from My Profile → API Tokens.",
115
+ placeholder: "Paste Global API Key",
116
+ inputType: "password",
117
+ },
118
+ ],
119
+ };
120
+ function credentialConfig(authentication, override) {
121
+ if (authentication === "apiToken") {
122
+ const credential = override ?? API_TOKEN_CREDENTIAL;
123
+ if (credential.fields?.length) {
124
+ throw new Error("cloudflare() API token authentication requires a single-value credential.");
125
+ }
126
+ return credential;
127
+ }
128
+ const credential = override
129
+ ? {
130
+ ...GLOBAL_API_KEY_CREDENTIAL,
131
+ ...override,
132
+ fields: override.fields ?? GLOBAL_API_KEY_CREDENTIAL.fields,
133
+ }
134
+ : GLOBAL_API_KEY_CREDENTIAL;
135
+ const fields = credential.fields?.map((field) => field.name).sort();
136
+ if (fields?.join(",") !== "apiKey,email") {
137
+ throw new Error('cloudflare() Global API Key authentication requires credential fields named "email" and "apiKey".');
138
+ }
139
+ return credential;
140
+ }
99
141
  function asRecord(value) {
100
142
  return value && typeof value === "object" && !Array.isArray(value)
101
143
  ? value
@@ -188,7 +230,7 @@ function failureFor(status, headers, errors) {
188
230
  }
189
231
  const authCoded = [...codes].some((code) => AUTH_ERROR_CODES.has(code));
190
232
  if (status === 401 || status === 403 || authCoded) {
191
- return new ConnectorCallError("auth_required", `Cloudflare rejected the API token (HTTP ${status}). ${detail} Check that the token is valid and carries the permission this call needs.`);
233
+ return new ConnectorCallError("auth_required", `Cloudflare rejected the configured credential (HTTP ${status}). ${detail} Check that it is valid and has permission to access this resource.`);
192
234
  }
193
235
  if (status === 400 || status === 409 || status === 422) {
194
236
  return new ConnectorCallError("invalid_args", `Cloudflare rejected the request (HTTP ${status}). ${detail}`);
@@ -201,72 +243,93 @@ function failureFor(status, headers, errors) {
201
243
  }
202
244
  return new ConnectorCallError("connector_call_failed", `Cloudflare request failed (HTTP ${status}). ${detail}`);
203
245
  }
204
- async function readToken(ctx) {
246
+ // --- The request path --------------------------------------------------------
247
+ /**
248
+ * The largest response this connection will read.
249
+ *
250
+ * Generous rather than tight, because `cloudflare_api_get` legitimately
251
+ * downloads Worker scripts and R2 objects. It is a ceiling on absurdity, not a
252
+ * quota: anything approaching it is already far past whatever `maxResultBytes`
253
+ * the deployment set, so the caller was never going to see it whole.
254
+ */
255
+ const CLOUDFLARE_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
256
+ const AUTHENTICATION_CONTEXT = Symbol("cloudflareAuthentication");
257
+ function withAuthentication(ctx, authentication) {
258
+ return { ...ctx, [AUTHENTICATION_CONTEXT]: authentication };
259
+ }
260
+ async function readAuthenticationHeaders(ctx) {
261
+ if (ctx[AUTHENTICATION_CONTEXT] === "globalApiKey") {
262
+ const values = await ctx.credential?.getAll();
263
+ const email = values?.["email"];
264
+ const apiKey = values?.["apiKey"];
265
+ if (!email || !apiKey) {
266
+ throw new ConnectorCallError("auth_required", "No Cloudflare Global API Key and account email are configured for this connector. An operator must add both before any call can run.");
267
+ }
268
+ return { "X-Auth-Email": email, "X-Auth-Key": apiKey };
269
+ }
205
270
  const token = await ctx.credential?.get();
206
271
  if (!token) {
207
272
  throw new ConnectorCallError("auth_required", "No Cloudflare API token is configured for this connector. An operator must add one before any call can run.");
208
273
  }
209
- return token;
274
+ return { Authorization: `Bearer ${token}` };
210
275
  }
211
- function buildUrl(base, spec) {
212
- const url = new URL(`${base.replace(/\/+$/, "")}${spec.path}`);
213
- for (const [key, value] of Object.entries(spec.query ?? {})) {
214
- if (value === undefined)
215
- continue;
216
- url.searchParams.set(key, String(value));
217
- }
218
- return url.toString();
219
- }
220
- async function fetchCloudflare(base, spec, ctx) {
221
- const token = await readToken(ctx);
222
- if (spec.body !== undefined && spec.rawBody !== undefined) {
223
- throw new Error("A Cloudflare request cannot have both JSON and raw bodies.");
224
- }
225
- let response;
226
- try {
227
- response = await fetch(buildUrl(base, spec), {
228
- method: spec.method,
229
- headers: {
230
- Authorization: `Bearer ${token}`,
231
- Accept: "application/json",
232
- ...(spec.body !== undefined
233
- ? { "Content-Type": "application/json" }
234
- : {}),
235
- ...Object.fromEntries(Object.entries(spec.headers ?? {}).filter((entry) => entry[1] !== undefined)),
236
- },
237
- ...(spec.body !== undefined
238
- ? { body: JSON.stringify(spec.body) }
239
- : spec.rawBody !== undefined
240
- ? { body: spec.rawBody }
241
- : {}),
242
- ...(ctx.signal ? { signal: ctx.signal } : {}),
243
- });
244
- }
245
- catch (cause) {
246
- throw new ConnectorCallError("unavailable", `Could not reach the Cloudflare API: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
247
- }
248
- return response;
276
+ /**
277
+ * The one transport every Cloudflare tool goes through.
278
+ *
279
+ * URL confinement, `ctx.signal`, redirect refusal, bounded reads, and the
280
+ * "could not reach the provider" normalization all live in the shared helper.
281
+ * What stays here is what only Cloudflare knows: which headers prove identity,
282
+ * and what a status code means once it arrives.
283
+ */
284
+ function cloudflareTransport(baseUrl) {
285
+ return guardedFetch({
286
+ provider: "Cloudflare",
287
+ baseUrl,
288
+ headers: { Accept: "application/json" },
289
+ maxResponseBytes: CLOUDFLARE_MAX_RESPONSE_BYTES,
290
+ authenticate: (ctx) => readAuthenticationHeaders(ctx),
291
+ });
249
292
  }
250
- async function callCloudflare(base, spec, ctx) {
251
- const response = await fetchCloudflare(base, spec, ctx);
252
- let envelope;
253
- try {
254
- envelope = (await response.json());
255
- }
256
- catch (cause) {
257
- // A gateway error page, not JSON: the status is the only real signal left.
258
- throw response.ok
259
- ? new ConnectorCallError("unavailable", "Cloudflare returned a non-JSON body for a successful status.", { cause })
260
- : failureFor(response.status, response.headers, []);
261
- }
262
- const errors = Array.isArray(envelope.errors) ? envelope.errors : [];
263
- if (!response.ok || envelope.success === false) {
264
- throw failureFor(response.status, response.headers, errors);
265
- }
266
- return {
267
- result: envelope.result,
268
- resultInfo: envelope.result_info,
269
- };
293
+ async function callCloudflare(send, spec, ctx) {
294
+ return await send(spec, ctx, async (response) => {
295
+ let envelope;
296
+ let parseFailure;
297
+ try {
298
+ envelope = (await response.json());
299
+ }
300
+ catch (cause) {
301
+ // A transport failure is not a parse failure. The connector's byte
302
+ // ceiling fires from inside this read and is deliberately non-retryable;
303
+ // routing it through the branch below would relabel it as a retryable
304
+ // `unavailable` and tell an agent to retry a response that will exceed
305
+ // the ceiling every time.
306
+ if (cause instanceof ConnectorCallError)
307
+ throw cause;
308
+ parseFailure = cause;
309
+ }
310
+ if (envelope === undefined) {
311
+ // A gateway error page or an empty body, not an envelope: the status is
312
+ // the only real signal left.
313
+ throw response.ok
314
+ ? new ConnectorCallError("unavailable", "Cloudflare returned a non-JSON body for a successful status.", parseFailure !== undefined ? { cause: parseFailure } : {})
315
+ : failureFor(response.status, response.headers, []);
316
+ }
317
+ const errors = Array.isArray(envelope.errors) ? envelope.errors : [];
318
+ if (!response.ok || envelope.success === false) {
319
+ throw failureFor(response.status, response.headers, errors);
320
+ }
321
+ const isV4Envelope = "success" in envelope ||
322
+ "result" in envelope ||
323
+ "result_info" in envelope ||
324
+ "messages" in envelope;
325
+ return {
326
+ // `/graphql` and a small number of product APIs return ordinary JSON
327
+ // instead of the standard v4 envelope. Preserve that document whole so
328
+ // the raw tools cover them too.
329
+ result: isV4Envelope ? envelope.result : envelope,
330
+ resultInfo: isV4Envelope ? envelope.result_info : undefined,
331
+ };
332
+ });
270
333
  }
271
334
  function base64FromBytes(bytes) {
272
335
  let binary = "";
@@ -275,33 +338,34 @@ function base64FromBytes(bytes) {
275
338
  }
276
339
  return btoa(binary);
277
340
  }
278
- async function callCloudflareContent(base, spec, ctx, responseType) {
279
- const response = await fetchCloudflare(base, spec, ctx);
280
- if (!response.ok) {
281
- let errors = [];
282
- try {
283
- const envelope = (await response.clone().json());
284
- if (Array.isArray(envelope.errors))
285
- errors = envelope.errors;
341
+ async function callCloudflareContent(send, spec, ctx, responseType) {
342
+ return await send(spec, ctx, async (response) => {
343
+ if (!response.ok) {
344
+ let errors = [];
345
+ try {
346
+ const envelope = (await response.json());
347
+ if (envelope && Array.isArray(envelope.errors))
348
+ errors = envelope.errors;
349
+ }
350
+ catch {
351
+ // A raw or gateway error body has no structured detail to preserve,
352
+ // and one past the byte ceiling has none worth reporting over the
353
+ // status that already failed this call. Either way the throw below
354
+ // classifies the status, so nothing is swallowed into a success.
355
+ }
356
+ throw failureFor(response.status, response.headers, errors);
286
357
  }
287
- catch {
288
- // A raw or gateway error body has no structured detail to preserve.
358
+ const common = {
359
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
360
+ ...(response.headers.get("etag")
361
+ ? { etag: response.headers.get("etag") }
362
+ : {}),
363
+ };
364
+ if (responseType === "text") {
365
+ return { ...common, text: await response.text() };
289
366
  }
290
- throw failureFor(response.status, response.headers, errors);
291
- }
292
- const common = {
293
- contentType: response.headers.get("content-type") ?? "application/octet-stream",
294
- ...(response.headers.get("etag")
295
- ? { etag: response.headers.get("etag") }
296
- : {}),
297
- };
298
- if (responseType === "text") {
299
- return { ...common, text: await response.text() };
300
- }
301
- return {
302
- ...common,
303
- base64: base64FromBytes(new Uint8Array(await response.arrayBuffer())),
304
- };
367
+ return { ...common, base64: base64FromBytes(await response.bytes()) };
368
+ });
305
369
  }
306
370
  /**
307
371
  * Cloudflare's `result_info` reshaped into the one question an agent actually
@@ -570,8 +634,8 @@ const RAW_INPUT_PROPERTY = {
570
634
  * an out-of-range value with a 400, so each caller passes its own. Encoding
571
635
  * them in the schema turns a wasted round trip into a local repair — but only
572
636
  * where the bound is really Cloudflare's. `bounds` records who chose the
573
- * range, because `strictValidation` refuses an out-of-range `perPage` locally
574
- * and an agent deserves to know whether the wall it hit is the API's or ours:
637
+ * range, because `api()` refuses an out-of-range `perPage` locally and an
638
+ * agent deserves to know whether the wall it hit is the API's or ours:
575
639
  *
576
640
  * - `"cloudflare"` — the schema's own documented minimum and maximum.
577
641
  * - `"clamped"` — Cloudflare accepts more; this connection caps it lower.
@@ -602,6 +666,24 @@ function pagingInputProperties(minPerPage, maxPerPage, options = {}) {
602
666
  },
603
667
  };
604
668
  }
669
+ /**
670
+ * The second pagination convention, and the schemas say so.
671
+ *
672
+ * Rulesets, KV keys, and both R2 listings page by cursor rather than by page
673
+ * number, because that is how Cloudflare built them. The guide names the four,
674
+ * but an agent reading one tool's schema should not have to fetch the guide to
675
+ * learn that the `page` object it expected is not coming: absence of
676
+ * `nextCursor` is the loop condition here, and it is stated on both ends
677
+ * ([#342](https://github.com/zackbart/connecta/issues/342)).
678
+ */
679
+ const CURSOR_INPUT_PROPERTY = {
680
+ type: "string",
681
+ description: "Opaque cursor from a previous call's nextCursor. This endpoint pages by cursor, not page number.",
682
+ };
683
+ const NEXT_CURSOR_OUTPUT_PROPERTY = {
684
+ type: "string",
685
+ description: "Pass back as `cursor` to continue. Absent when the listing is complete — this is the only signal; there is no page object.",
686
+ };
605
687
  function listOutputSchema(key, item) {
606
688
  return {
607
689
  type: "object",
@@ -787,6 +869,8 @@ function headersFromArgs(value) {
787
869
  const headers = {};
788
870
  const forbidden = new Set([
789
871
  "authorization",
872
+ "x-auth-email",
873
+ "x-auth-key",
790
874
  "cookie",
791
875
  "host",
792
876
  "content-length",
@@ -865,7 +949,7 @@ const OPEN_OBJECT_OUTPUT_SCHEMA = {
865
949
  };
866
950
  const QUERY_INPUT_PROPERTY = {
867
951
  type: "array",
868
- description: "Optional query parameters as name/value pairs. Each parameter name may appear once.",
952
+ description: "Query parameters as name/value pairs; each name may appear once.",
869
953
  items: {
870
954
  type: "object",
871
955
  properties: {
@@ -876,9 +960,14 @@ const QUERY_INPUT_PROPERTY = {
876
960
  additionalProperties: false,
877
961
  },
878
962
  };
963
+ // Shared by all three escape hatches, and the compact renderer inlines it
964
+ // three times over. The refused-header list is the connector's boundary rather
965
+ // than a fact the caller composes with, so it lives in the usage guide; keeping
966
+ // it here pushed cloudflare_api_upload's compact input past the 1,024-byte
967
+ // budget ([#342](https://github.com/zackbart/connecta/issues/342)).
879
968
  const HEADERS_INPUT_PROPERTY = {
880
969
  type: "array",
881
- description: "Optional provider headers as name/value pairs, for example cf-r2-jurisdiction, Range, If-None-Match, or Cloudflare product metadata. Authorization, Cookie, Host, Content-Length, Content-Type, and Transfer-Encoding are connector-owned and refused.",
970
+ description: "Endpoint headers as name/value pairs, e.g. cf-r2-jurisdiction or Range. Connector-owned headers are refused.",
882
971
  items: {
883
972
  type: "object",
884
973
  properties: {
@@ -924,53 +1013,82 @@ const R2_OBJECT_SCHEMA = {
924
1013
  },
925
1014
  required: ["key"],
926
1015
  };
927
- function buildTools(scope) {
928
- const { base } = scope;
1016
+ function buildTools(scope, authentication) {
1017
+ const { send } = scope;
929
1018
  const zoneArg = (args) => requireScope(args["zoneId"], scope.zoneId, "zoneId");
930
1019
  const accountArg = (args) => requireScope(args["accountId"], scope.accountId, "accountId");
931
1020
  const readOnly = { readOnlyHint: true, destructiveHint: false };
932
- return [
933
- {
934
- name: "verify_api_token",
935
- description: "Verify the configured Cloudflare API token and report its status. Use this first when any other tool fails with an authentication error, to separate a bad token from a missing permission.",
936
- annotations: readOnly,
937
- inputSchema: {
938
- type: "object",
939
- properties: {},
940
- required: [],
941
- additionalProperties: false,
942
- },
943
- outputSchema: {
944
- type: "object",
945
- properties: {
946
- id: { type: "string" },
947
- status: {
948
- type: "string",
949
- description: "\"active\" for a usable token.",
1021
+ const tools = [
1022
+ authentication === "apiToken"
1023
+ ? {
1024
+ name: "verify_api_token",
1025
+ description: "Verify the configured Cloudflare API token and report its status. Use this first when any other tool fails with an authentication error, to separate a bad token from a missing permission.",
1026
+ annotations: readOnly,
1027
+ inputSchema: {
1028
+ type: "object",
1029
+ properties: {},
1030
+ required: [],
1031
+ additionalProperties: false,
1032
+ },
1033
+ outputSchema: {
1034
+ type: "object",
1035
+ properties: {
1036
+ id: { type: "string" },
1037
+ status: {
1038
+ type: "string",
1039
+ description: "\"active\" for a usable token.",
1040
+ },
1041
+ notBefore: { type: "string" },
1042
+ expiresOn: { type: "string" },
1043
+ },
1044
+ required: ["status"],
1045
+ },
1046
+ handler: async (_args, ctx) => {
1047
+ const { result } = await callCloudflare(send, { method: "GET", path: "/user/tokens/verify" }, ctx);
1048
+ const token = asRecord(result);
1049
+ return {
1050
+ id: token["id"],
1051
+ status: token["status"],
1052
+ ...(token["not_before"] !== undefined
1053
+ ? { notBefore: token["not_before"] }
1054
+ : {}),
1055
+ ...(token["expires_on"] !== undefined
1056
+ ? { expiresOn: token["expires_on"] }
1057
+ : {}),
1058
+ };
1059
+ },
1060
+ }
1061
+ : {
1062
+ name: "verify_global_api_key",
1063
+ description: "Verify the configured Cloudflare Global API Key and account email by retrieving the authenticated user. Use this first when another tool fails with an authentication error.",
1064
+ annotations: readOnly,
1065
+ inputSchema: {
1066
+ type: "object",
1067
+ properties: {},
1068
+ required: [],
1069
+ additionalProperties: false,
1070
+ },
1071
+ outputSchema: {
1072
+ type: "object",
1073
+ properties: {
1074
+ id: { type: "string" },
1075
+ email: { type: "string" },
1076
+ status: {
1077
+ type: "string",
1078
+ description: "\"active\" when Cloudflare accepts the email and key.",
1079
+ },
950
1080
  },
951
- notBefore: { type: "string" },
952
- expiresOn: { type: "string" },
1081
+ required: ["email", "status"],
1082
+ },
1083
+ handler: async (_args, ctx) => {
1084
+ const { result } = await callCloudflare(send, { method: "GET", path: "/user" }, ctx);
1085
+ const user = asRecord(result);
1086
+ return { id: user["id"], email: user["email"], status: "active" };
953
1087
  },
954
- required: ["status"],
955
- },
956
- handler: async (_args, ctx) => {
957
- const { result } = await callCloudflare(base, { method: "GET", path: "/user/tokens/verify" }, ctx);
958
- const token = asRecord(result);
959
- return {
960
- id: token["id"],
961
- status: token["status"],
962
- ...(token["not_before"] !== undefined
963
- ? { notBefore: token["not_before"] }
964
- : {}),
965
- ...(token["expires_on"] !== undefined
966
- ? { expiresOn: token["expires_on"] }
967
- : {}),
968
- };
969
1088
  },
970
- },
971
1089
  {
972
1090
  name: "cloudflare_api_get",
973
- description: "Call any GET endpoint under Cloudflare's v4 API with this connector's token. Use a named tool when one exists; use this read-only escape hatch for Images, Stream, Email Routing, D1, Queues, Access, Tunnels, Analytics, and newer product endpoints the curated surface does not yet name.",
1091
+ description: "Call any GET endpoint under Cloudflare's v4 API with this connector's credential. Prefer a named tool when one exists; this read-only hatch covers the products the named surface does not reach, such as Images, Stream, D1, and Queues.",
974
1092
  annotations: readOnly,
975
1093
  inputSchema: {
976
1094
  type: "object",
@@ -1019,9 +1137,9 @@ function buildTools(scope) {
1019
1137
  ...(headers !== undefined ? { headers } : {}),
1020
1138
  };
1021
1139
  if (responseType === "text" || responseType === "base64") {
1022
- return await callCloudflareContent(base, spec, ctx, responseType);
1140
+ return await callCloudflareContent(send, spec, ctx, responseType);
1023
1141
  }
1024
- const { result, resultInfo } = await callCloudflare(base, spec, ctx);
1142
+ const { result, resultInfo } = await callCloudflare(send, spec, ctx);
1025
1143
  return {
1026
1144
  result,
1027
1145
  ...(resultInfo !== undefined ? { resultInfo } : {}),
@@ -1030,7 +1148,7 @@ function buildTools(scope) {
1030
1148
  },
1031
1149
  {
1032
1150
  name: "cloudflare_api_mutate",
1033
- description: "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's token. This is the approval-gated escape hatch for managing Cloudflare products without waiting for a named tool. It does not support multipart or binary uploads.",
1151
+ description: "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's credential. The approval-gated write hatch for products the named surface does not reach. No multipart or binary uploads.",
1034
1152
  annotations: { readOnlyHint: false, destructiveHint: true },
1035
1153
  inputSchema: {
1036
1154
  type: "object",
@@ -1072,7 +1190,7 @@ function buildTools(scope) {
1072
1190
  const method = String(args["method"]);
1073
1191
  const query = queryFromArgs(args["query"]);
1074
1192
  const headers = headersFromArgs(args["headers"]);
1075
- const { result, resultInfo } = await callCloudflare(base, {
1193
+ const { result, resultInfo } = await callCloudflare(send, {
1076
1194
  method,
1077
1195
  path: cloudflareApiPath(args["path"]),
1078
1196
  ...(query !== undefined ? { query } : {}),
@@ -1095,31 +1213,31 @@ function buildTools(scope) {
1095
1213
  method: {
1096
1214
  type: "string",
1097
1215
  enum: ["POST", "PUT"],
1098
- description: "HTTP upload method required by the Cloudflare endpoint.",
1216
+ description: "Upload method the Cloudflare endpoint requires.",
1099
1217
  },
1100
1218
  path: {
1101
1219
  type: "string",
1102
1220
  minLength: 1,
1103
- description: "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
1221
+ description: "Path below /client/v4, beginning with '/'. No query string.",
1104
1222
  },
1105
1223
  query: QUERY_INPUT_PROPERTY,
1106
1224
  headers: HEADERS_INPUT_PROPERTY,
1107
1225
  contentType: {
1108
1226
  type: "string",
1109
1227
  minLength: 1,
1110
- description: "Content-Type for a raw text/base64 body. Omit for multipart because fetch supplies the boundary.",
1228
+ description: "Content-Type for a raw text or base64 body. Omit for multipart.",
1111
1229
  },
1112
1230
  textBody: {
1113
1231
  type: "string",
1114
- description: "Raw UTF-8 request body. Mutually exclusive with base64Body and multipart fields/files.",
1232
+ description: "Raw UTF-8 body. Exclusive with base64Body and fields/files.",
1115
1233
  },
1116
1234
  base64Body: {
1117
1235
  type: "string",
1118
- description: "Base64-encoded request bytes. Mutually exclusive with textBody and multipart fields/files.",
1236
+ description: "Base64-encoded body bytes. Exclusive with textBody and fields/files.",
1119
1237
  },
1120
1238
  fields: {
1121
1239
  type: "array",
1122
- description: "String fields for a multipart/form-data request.",
1240
+ description: "String fields of a multipart/form-data request.",
1123
1241
  items: {
1124
1242
  type: "object",
1125
1243
  properties: {
@@ -1134,7 +1252,7 @@ function buildTools(scope) {
1134
1252
  },
1135
1253
  files: {
1136
1254
  type: "array",
1137
- description: "File parts for multipart/form-data. Each file needs exactly one of text or base64.",
1255
+ description: "Multipart file parts. Each needs exactly one of text or base64.",
1138
1256
  items: {
1139
1257
  type: "object",
1140
1258
  properties: {
@@ -1165,7 +1283,7 @@ function buildTools(scope) {
1165
1283
  const query = queryFromArgs(args["query"]);
1166
1284
  const headers = headersFromArgs(args["headers"]);
1167
1285
  const upload = uploadBody(args);
1168
- const { result } = await callCloudflare(base, {
1286
+ const { result } = await callCloudflare(send, {
1169
1287
  method: String(args["method"]),
1170
1288
  path: cloudflareApiPath(args["path"]),
1171
1289
  ...(query !== undefined ? { query } : {}),
@@ -1196,7 +1314,7 @@ function buildTools(scope) {
1196
1314
  },
1197
1315
  outputSchema: listOutputSchema("accounts", ACCOUNT_SCHEMA),
1198
1316
  handler: async (args, ctx) => {
1199
- const { result, resultInfo } = await callCloudflare(base, {
1317
+ const { result, resultInfo } = await callCloudflare(send, {
1200
1318
  method: "GET",
1201
1319
  path: "/accounts",
1202
1320
  query: {
@@ -1241,7 +1359,7 @@ function buildTools(scope) {
1241
1359
  },
1242
1360
  outputSchema: listOutputSchema("zones", ZONE_SCHEMA),
1243
1361
  handler: async (args, ctx) => {
1244
- const { result, resultInfo } = await callCloudflare(base, {
1362
+ const { result, resultInfo } = await callCloudflare(send, {
1245
1363
  method: "GET",
1246
1364
  path: "/zones",
1247
1365
  query: {
@@ -1279,7 +1397,7 @@ function buildTools(scope) {
1279
1397
  },
1280
1398
  outputSchema: ZONE_SCHEMA,
1281
1399
  handler: async (args, ctx) => {
1282
- const { result } = await callCloudflare(base, { method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` }, ctx);
1400
+ const { result } = await callCloudflare(send, { method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` }, ctx);
1283
1401
  return args["raw"] === true ? result : projectZone(result);
1284
1402
  },
1285
1403
  },
@@ -1297,7 +1415,7 @@ function buildTools(scope) {
1297
1415
  },
1298
1416
  outputSchema: listOutputSchema("settings", OPEN_OBJECT_OUTPUT_SCHEMA),
1299
1417
  handler: async (args, ctx) => {
1300
- const { result, resultInfo } = await callCloudflare(base, {
1418
+ const { result, resultInfo } = await callCloudflare(send, {
1301
1419
  method: "GET",
1302
1420
  path: `/zones/${encodePathSegment(zoneArg(args))}/settings`,
1303
1421
  }, ctx);
@@ -1323,7 +1441,7 @@ function buildTools(scope) {
1323
1441
  },
1324
1442
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1325
1443
  handler: async (args, ctx) => {
1326
- const { result } = await callCloudflare(base, {
1444
+ const { result } = await callCloudflare(send, {
1327
1445
  method: "GET",
1328
1446
  path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1329
1447
  }, ctx);
@@ -1354,7 +1472,7 @@ function buildTools(scope) {
1354
1472
  },
1355
1473
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1356
1474
  handler: async (args, ctx) => {
1357
- const { result } = await callCloudflare(base, {
1475
+ const { result } = await callCloudflare(send, {
1358
1476
  method: "PATCH",
1359
1477
  path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1360
1478
  body: { value: args["value"] },
@@ -1376,10 +1494,7 @@ function buildTools(scope) {
1376
1494
  maximum: 50,
1377
1495
  description: "Rulesets per request, 1 to 50.",
1378
1496
  },
1379
- cursor: {
1380
- type: "string",
1381
- description: "Opaque cursor returned as nextCursor by the previous call.",
1382
- },
1497
+ cursor: CURSOR_INPUT_PROPERTY,
1383
1498
  },
1384
1499
  required: scopeRequired("zoneId", scope.zoneId),
1385
1500
  additionalProperties: false,
@@ -1388,12 +1503,12 @@ function buildTools(scope) {
1388
1503
  type: "object",
1389
1504
  properties: {
1390
1505
  rulesets: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1391
- nextCursor: { type: "string" },
1506
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1392
1507
  },
1393
1508
  required: ["rulesets"],
1394
1509
  },
1395
1510
  handler: async (args, ctx) => {
1396
- const { result, resultInfo } = await callCloudflare(base, {
1511
+ const { result, resultInfo } = await callCloudflare(send, {
1397
1512
  method: "GET",
1398
1513
  path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets`,
1399
1514
  query: {
@@ -1429,7 +1544,7 @@ function buildTools(scope) {
1429
1544
  },
1430
1545
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1431
1546
  handler: async (args, ctx) => {
1432
- const { result } = await callCloudflare(base, {
1547
+ const { result } = await callCloudflare(send, {
1433
1548
  method: "GET",
1434
1549
  path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets/${encodePathSegment(requireString(args, "rulesetId"))}`,
1435
1550
  }, ctx);
@@ -1481,7 +1596,7 @@ function buildTools(scope) {
1481
1596
  },
1482
1597
  outputSchema: listOutputSchema("records", DNS_RECORD_SCHEMA),
1483
1598
  handler: async (args, ctx) => {
1484
- const { result, resultInfo } = await callCloudflare(base, {
1599
+ const { result, resultInfo } = await callCloudflare(send, {
1485
1600
  method: "GET",
1486
1601
  path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
1487
1602
  query: {
@@ -1521,7 +1636,7 @@ function buildTools(scope) {
1521
1636
  },
1522
1637
  outputSchema: DNS_RECORD_SCHEMA,
1523
1638
  handler: async (args, ctx) => {
1524
- const { result } = await callCloudflare(base, {
1639
+ const { result } = await callCloudflare(send, {
1525
1640
  method: "GET",
1526
1641
  path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
1527
1642
  }, ctx);
@@ -1552,7 +1667,7 @@ function buildTools(scope) {
1552
1667
  required: ["id"],
1553
1668
  }),
1554
1669
  handler: async (args, ctx) => {
1555
- const { result, resultInfo } = await callCloudflare(base, {
1670
+ const { result, resultInfo } = await callCloudflare(send, {
1556
1671
  method: "GET",
1557
1672
  path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
1558
1673
  }, ctx);
@@ -1583,7 +1698,7 @@ function buildTools(scope) {
1583
1698
  },
1584
1699
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1585
1700
  handler: async (args, ctx) => {
1586
- const { result } = await callCloudflare(base, {
1701
+ const { result } = await callCloudflare(send, {
1587
1702
  method: "GET",
1588
1703
  path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/settings`,
1589
1704
  }, ctx);
@@ -1609,7 +1724,7 @@ function buildTools(scope) {
1609
1724
  },
1610
1725
  outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
1611
1726
  handler: async (args, ctx) => {
1612
- const { result } = await callCloudflare(base, {
1727
+ const { result } = await callCloudflare(send, {
1613
1728
  method: "GET",
1614
1729
  path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments`,
1615
1730
  }, ctx);
@@ -1648,7 +1763,7 @@ function buildTools(scope) {
1648
1763
  },
1649
1764
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1650
1765
  handler: async (args, ctx) => {
1651
- const { result } = await callCloudflare(base, {
1766
+ const { result } = await callCloudflare(send, {
1652
1767
  method: "GET",
1653
1768
  path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
1654
1769
  }, ctx);
@@ -1686,7 +1801,7 @@ function buildTools(scope) {
1686
1801
  },
1687
1802
  handler: async (args, ctx) => {
1688
1803
  const scriptName = requireString(args, "scriptName");
1689
- await callCloudflare(base, {
1804
+ await callCloudflare(send, {
1690
1805
  method: "DELETE",
1691
1806
  path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(scriptName)}`,
1692
1807
  query: { force: optionalBoolean(args, "force") },
@@ -1718,7 +1833,7 @@ function buildTools(scope) {
1718
1833
  required: ["id", "title"],
1719
1834
  }),
1720
1835
  handler: async (args, ctx) => {
1721
- const { result, resultInfo } = await callCloudflare(base, {
1836
+ const { result, resultInfo } = await callCloudflare(send, {
1722
1837
  method: "GET",
1723
1838
  path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
1724
1839
  query: {
@@ -1753,7 +1868,7 @@ function buildTools(scope) {
1753
1868
  },
1754
1869
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1755
1870
  handler: async (args, ctx) => {
1756
- const { result } = await callCloudflare(base, {
1871
+ const { result } = await callCloudflare(send, {
1757
1872
  method: "GET",
1758
1873
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
1759
1874
  }, ctx);
@@ -1780,7 +1895,7 @@ function buildTools(scope) {
1780
1895
  },
1781
1896
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1782
1897
  handler: async (args, ctx) => {
1783
- const { result } = await callCloudflare(base, {
1898
+ const { result } = await callCloudflare(send, {
1784
1899
  method: "POST",
1785
1900
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces`,
1786
1901
  body: { title: requireString(args, "title") },
@@ -1817,7 +1932,7 @@ function buildTools(scope) {
1817
1932
  },
1818
1933
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1819
1934
  handler: async (args, ctx) => {
1820
- const { result } = await callCloudflare(base, {
1935
+ const { result } = await callCloudflare(send, {
1821
1936
  method: "PUT",
1822
1937
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
1823
1938
  body: { title: requireString(args, "title") },
@@ -1852,7 +1967,7 @@ function buildTools(scope) {
1852
1967
  },
1853
1968
  handler: async (args, ctx) => {
1854
1969
  const namespaceId = requireString(args, "namespaceId");
1855
- await callCloudflare(base, {
1970
+ await callCloudflare(send, {
1856
1971
  method: "DELETE",
1857
1972
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(namespaceId)}`,
1858
1973
  }, ctx);
@@ -1882,10 +1997,7 @@ function buildTools(scope) {
1882
1997
  maximum: 1000,
1883
1998
  description: "Keys per request, 10 to 1000. Defaults to 1000.",
1884
1999
  },
1885
- cursor: {
1886
- type: "string",
1887
- description: "Opaque cursor returned as nextCursor by the previous call.",
1888
- },
2000
+ cursor: CURSOR_INPUT_PROPERTY,
1889
2001
  },
1890
2002
  required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
1891
2003
  additionalProperties: false,
@@ -1894,12 +2006,12 @@ function buildTools(scope) {
1894
2006
  type: "object",
1895
2007
  properties: {
1896
2008
  keys: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1897
- nextCursor: { type: "string" },
2009
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1898
2010
  },
1899
2011
  required: ["keys"],
1900
2012
  },
1901
2013
  handler: async (args, ctx) => {
1902
- const { result, resultInfo } = await callCloudflare(base, {
2014
+ const { result, resultInfo } = await callCloudflare(send, {
1903
2015
  method: "GET",
1904
2016
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/keys`,
1905
2017
  query: {
@@ -1956,7 +2068,7 @@ function buildTools(scope) {
1956
2068
  },
1957
2069
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1958
2070
  handler: async (args, ctx) => {
1959
- const { result } = await callCloudflare(base, {
2071
+ const { result } = await callCloudflare(send, {
1960
2072
  method: "POST",
1961
2073
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/get`,
1962
2074
  body: {
@@ -1991,12 +2103,34 @@ function buildTools(scope) {
1991
2103
  items: {
1992
2104
  type: "object",
1993
2105
  properties: {
1994
- key: { type: "string", minLength: 1, maxLength: 512 },
1995
- value: { type: "string", maxLength: 26_214_400 },
1996
- expiration: { type: "number" },
1997
- expiration_ttl: { type: "number", minimum: 60 },
1998
- metadata: { type: ["object", "array", "string", "number", "boolean", "null"] },
1999
- base64: { type: "boolean" },
2106
+ key: {
2107
+ type: "string",
2108
+ minLength: 1,
2109
+ maxLength: 512,
2110
+ description: "Key name, up to Cloudflare's 512 bytes.",
2111
+ },
2112
+ value: {
2113
+ type: "string",
2114
+ maxLength: 26_214_400,
2115
+ description: "Value, up to Cloudflare's 25 MiB.",
2116
+ },
2117
+ expiration: {
2118
+ type: "number",
2119
+ description: "Absolute expiry as a Unix timestamp in seconds.",
2120
+ },
2121
+ expiration_ttl: {
2122
+ type: "number",
2123
+ minimum: 60,
2124
+ description: "Relative expiry in seconds; Cloudflare's floor is 60.",
2125
+ },
2126
+ metadata: {
2127
+ type: ["object", "array", "string", "number", "boolean", "null"],
2128
+ description: "JSON metadata returned beside the key by list_kv_keys.",
2129
+ },
2130
+ base64: {
2131
+ type: "boolean",
2132
+ description: "Treat value as base64 and store the decoded bytes.",
2133
+ },
2000
2134
  },
2001
2135
  required: ["key", "value"],
2002
2136
  additionalProperties: false,
@@ -2012,7 +2146,7 @@ function buildTools(scope) {
2012
2146
  },
2013
2147
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2014
2148
  handler: async (args, ctx) => {
2015
- const { result } = await callCloudflare(base, {
2149
+ const { result } = await callCloudflare(send, {
2016
2150
  method: "PUT",
2017
2151
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk`,
2018
2152
  body: args["entries"],
@@ -2050,7 +2184,7 @@ function buildTools(scope) {
2050
2184
  },
2051
2185
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2052
2186
  handler: async (args, ctx) => {
2053
- const { result } = await callCloudflare(base, {
2187
+ const { result } = await callCloudflare(send, {
2054
2188
  method: "POST",
2055
2189
  path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/delete`,
2056
2190
  body: args["keys"],
@@ -2076,10 +2210,7 @@ function buildTools(scope) {
2076
2210
  maximum: 1000,
2077
2211
  description: "Buckets per request, 1 to 1000. Defaults to 20.",
2078
2212
  },
2079
- cursor: {
2080
- type: "string",
2081
- description: "Opaque cursor from a previous call's nextCursor. R2 paginates by cursor, not page number.",
2082
- },
2213
+ cursor: CURSOR_INPUT_PROPERTY,
2083
2214
  jurisdiction: R2_JURISDICTION_PROPERTY,
2084
2215
  raw: RAW_INPUT_PROPERTY,
2085
2216
  },
@@ -2103,15 +2234,12 @@ function buildTools(scope) {
2103
2234
  required: ["name"],
2104
2235
  },
2105
2236
  },
2106
- nextCursor: {
2107
- type: "string",
2108
- description: "Pass back as `cursor` to continue. Absent when the listing is complete.",
2109
- },
2237
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
2110
2238
  },
2111
2239
  required: ["buckets"],
2112
2240
  },
2113
2241
  handler: async (args, ctx) => {
2114
- const { result, resultInfo } = await callCloudflare(base, {
2242
+ const { result, resultInfo } = await callCloudflare(send, {
2115
2243
  method: "GET",
2116
2244
  path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
2117
2245
  query: {
@@ -2151,7 +2279,7 @@ function buildTools(scope) {
2151
2279
  },
2152
2280
  outputSchema: R2_BUCKET_SCHEMA,
2153
2281
  handler: async (args, ctx) => {
2154
- const { result } = await callCloudflare(base, {
2282
+ const { result } = await callCloudflare(send, {
2155
2283
  method: "GET",
2156
2284
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2157
2285
  headers: r2Headers(args),
@@ -2185,7 +2313,7 @@ function buildTools(scope) {
2185
2313
  },
2186
2314
  outputSchema: R2_BUCKET_SCHEMA,
2187
2315
  handler: async (args, ctx) => {
2188
- const { result } = await callCloudflare(base, {
2316
+ const { result } = await callCloudflare(send, {
2189
2317
  method: "POST",
2190
2318
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets`,
2191
2319
  headers: r2Headers(args),
@@ -2227,7 +2355,7 @@ function buildTools(scope) {
2227
2355
  },
2228
2356
  outputSchema: R2_BUCKET_SCHEMA,
2229
2357
  handler: async (args, ctx) => {
2230
- const { result } = await callCloudflare(base, {
2358
+ const { result } = await callCloudflare(send, {
2231
2359
  method: "PATCH",
2232
2360
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2233
2361
  headers: {
@@ -2262,7 +2390,7 @@ function buildTools(scope) {
2262
2390
  },
2263
2391
  handler: async (args, ctx) => {
2264
2392
  const bucketName = requireString(args, "bucketName");
2265
- await callCloudflare(base, {
2393
+ await callCloudflare(send, {
2266
2394
  method: "DELETE",
2267
2395
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(bucketName)}`,
2268
2396
  headers: r2Headers(args),
@@ -2300,10 +2428,7 @@ function buildTools(scope) {
2300
2428
  maximum: 1000,
2301
2429
  description: "Objects per request, 1 to 1000.",
2302
2430
  },
2303
- cursor: {
2304
- type: "string",
2305
- description: "Opaque cursor returned as nextCursor by the previous call.",
2306
- },
2431
+ cursor: CURSOR_INPUT_PROPERTY,
2307
2432
  },
2308
2433
  required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2309
2434
  additionalProperties: false,
@@ -2313,13 +2438,13 @@ function buildTools(scope) {
2313
2438
  properties: {
2314
2439
  objects: { type: "array", items: R2_OBJECT_SCHEMA },
2315
2440
  commonPrefixes: { type: "array", items: { type: "string" } },
2316
- nextCursor: { type: "string" },
2441
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
2317
2442
  truncated: { type: "boolean" },
2318
2443
  },
2319
2444
  required: ["objects", "truncated"],
2320
2445
  },
2321
2446
  handler: async (args, ctx) => {
2322
- const { result, resultInfo } = await callCloudflare(base, {
2447
+ const { result, resultInfo } = await callCloudflare(send, {
2323
2448
  method: "GET",
2324
2449
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects`,
2325
2450
  headers: r2Headers(args),
@@ -2377,7 +2502,7 @@ function buildTools(scope) {
2377
2502
  },
2378
2503
  handler: async (args, ctx) => {
2379
2504
  const objectKey = requireString(args, "objectKey");
2380
- await callCloudflare(base, {
2505
+ await callCloudflare(send, {
2381
2506
  method: "DELETE",
2382
2507
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects/${encodeObjectKey(objectKey)}`,
2383
2508
  headers: r2Headers(args),
@@ -2385,25 +2510,17 @@ function buildTools(scope) {
2385
2510
  return { deleted: true, objectKey };
2386
2511
  },
2387
2512
  },
2388
- {
2389
- name: "get_r2_metrics",
2390
- description: "Get account-level R2 object-count and storage-size metrics split by storage class and publication state.",
2391
- annotations: readOnly,
2392
- inputSchema: {
2393
- type: "object",
2394
- properties: { accountId: scopeProperty("accountId", scope.accountId) },
2395
- required: scopeRequired("accountId", scope.accountId),
2396
- additionalProperties: false,
2397
- },
2398
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2399
- handler: async (args, ctx) => {
2400
- const { result } = await callCloudflare(base, {
2401
- method: "GET",
2402
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/metrics`,
2403
- }, ctx);
2404
- return asRecord(result);
2405
- },
2406
- },
2513
+ // No `get_r2_metrics`, `set_r2_cors`, or `delete_r2_cors` here on purpose.
2514
+ // The #350 measurement found `get_r2_metrics` an unprojected,
2515
+ // output-schema-less wrapper around a path, and `set_r2_cors` carrying a
2516
+ // free-form `additionalProperties: true` rule body, so its schema did not
2517
+ // validate the part of the call that actually fails. `delete_r2_cors`
2518
+ // measured clean — a fixed `{deleted}` confirmation behind a closed output
2519
+ // schema and went anyway, as the other half of one policy pair: with the
2520
+ // write unnamed, a named delete would change a CORS policy through a
2521
+ // different route than setting it does. Reading a policy is still named;
2522
+ // changing one takes the approval-gated raw route, exactly as
2523
+ // structured-data DNS records already do.
2407
2524
  {
2408
2525
  name: "get_r2_cors",
2409
2526
  description: "Get the browser CORS rules configured on an R2 bucket.",
@@ -2420,7 +2537,7 @@ function buildTools(scope) {
2420
2537
  },
2421
2538
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2422
2539
  handler: async (args, ctx) => {
2423
- const { result } = await callCloudflare(base, {
2540
+ const { result } = await callCloudflare(send, {
2424
2541
  method: "GET",
2425
2542
  path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2426
2543
  headers: r2Headers(args),
@@ -2428,65 +2545,6 @@ function buildTools(scope) {
2428
2545
  return asRecord(result);
2429
2546
  },
2430
2547
  },
2431
- {
2432
- name: "set_r2_cors",
2433
- description: "Replace an R2 bucket's CORS policy. Supply the complete desired rule list; omitted existing rules are removed.",
2434
- annotations: { readOnlyHint: false, destructiveHint: true },
2435
- inputSchema: {
2436
- type: "object",
2437
- properties: {
2438
- accountId: scopeProperty("accountId", scope.accountId),
2439
- bucketName: R2_BUCKET_NAME_PROPERTY,
2440
- jurisdiction: R2_JURISDICTION_PROPERTY,
2441
- rules: {
2442
- type: "array",
2443
- maxItems: 100,
2444
- description: "Complete CORS rule list using Cloudflare fields: allowed.methods, allowed.origins, optional allowed.headers, id, exposeHeaders, and maxAgeSeconds.",
2445
- items: { type: "object", additionalProperties: true },
2446
- },
2447
- },
2448
- required: [...scopeRequired("accountId", scope.accountId), "bucketName", "rules"],
2449
- additionalProperties: false,
2450
- },
2451
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2452
- handler: async (args, ctx) => {
2453
- const { result } = await callCloudflare(base, {
2454
- method: "PUT",
2455
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2456
- headers: r2Headers(args),
2457
- body: { rules: args["rules"] },
2458
- }, ctx);
2459
- return asRecord(result);
2460
- },
2461
- },
2462
- {
2463
- name: "delete_r2_cors",
2464
- description: "Remove the complete CORS policy from an R2 bucket.",
2465
- annotations: { readOnlyHint: false, destructiveHint: true },
2466
- inputSchema: {
2467
- type: "object",
2468
- properties: {
2469
- accountId: scopeProperty("accountId", scope.accountId),
2470
- bucketName: R2_BUCKET_NAME_PROPERTY,
2471
- jurisdiction: R2_JURISDICTION_PROPERTY,
2472
- },
2473
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2474
- additionalProperties: false,
2475
- },
2476
- outputSchema: {
2477
- type: "object",
2478
- properties: { deleted: { type: "boolean" } },
2479
- required: ["deleted"],
2480
- },
2481
- handler: async (args, ctx) => {
2482
- await callCloudflare(base, {
2483
- method: "DELETE",
2484
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2485
- headers: r2Headers(args),
2486
- }, ctx);
2487
- return { deleted: true };
2488
- },
2489
- },
2490
2548
  {
2491
2549
  name: "list_pages_projects",
2492
2550
  description: "List Cloudflare Pages projects in an account, with their production branch and latest deployment.",
@@ -2522,7 +2580,7 @@ function buildTools(scope) {
2522
2580
  required: ["name"],
2523
2581
  }),
2524
2582
  handler: async (args, ctx) => {
2525
- const { result, resultInfo } = await callCloudflare(base, {
2583
+ const { result, resultInfo } = await callCloudflare(send, {
2526
2584
  method: "GET",
2527
2585
  path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects`,
2528
2586
  query: {
@@ -2558,7 +2616,7 @@ function buildTools(scope) {
2558
2616
  },
2559
2617
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2560
2618
  handler: async (args, ctx) => {
2561
- const { result } = await callCloudflare(base, {
2619
+ const { result } = await callCloudflare(send, {
2562
2620
  method: "GET",
2563
2621
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}`,
2564
2622
  }, ctx);
@@ -2590,7 +2648,7 @@ function buildTools(scope) {
2590
2648
  },
2591
2649
  outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
2592
2650
  handler: async (args, ctx) => {
2593
- const { result, resultInfo } = await callCloudflare(base, {
2651
+ const { result, resultInfo } = await callCloudflare(send, {
2594
2652
  method: "GET",
2595
2653
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments`,
2596
2654
  query: {
@@ -2634,7 +2692,7 @@ function buildTools(scope) {
2634
2692
  },
2635
2693
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2636
2694
  handler: async (args, ctx) => {
2637
- const { result } = await callCloudflare(base, {
2695
+ const { result } = await callCloudflare(send, {
2638
2696
  method: "GET",
2639
2697
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
2640
2698
  }, ctx);
@@ -2657,7 +2715,7 @@ function buildTools(scope) {
2657
2715
  },
2658
2716
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2659
2717
  handler: async (args, ctx) => {
2660
- const { result } = await callCloudflare(base, {
2718
+ const { result } = await callCloudflare(send, {
2661
2719
  method: "POST",
2662
2720
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/retry`,
2663
2721
  }, ctx);
@@ -2680,7 +2738,7 @@ function buildTools(scope) {
2680
2738
  },
2681
2739
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2682
2740
  handler: async (args, ctx) => {
2683
- const { result } = await callCloudflare(base, {
2741
+ const { result } = await callCloudflare(send, {
2684
2742
  method: "POST",
2685
2743
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/rollback`,
2686
2744
  }, ctx);
@@ -2704,7 +2762,7 @@ function buildTools(scope) {
2704
2762
  outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["deleted", "deploymentId"] },
2705
2763
  handler: async (args, ctx) => {
2706
2764
  const deploymentId = requireString(args, "deploymentId");
2707
- await callCloudflare(base, {
2765
+ await callCloudflare(send, {
2708
2766
  method: "DELETE",
2709
2767
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(deploymentId)}`,
2710
2768
  }, ctx);
@@ -2726,7 +2784,7 @@ function buildTools(scope) {
2726
2784
  },
2727
2785
  outputSchema: listOutputSchema("domains", OPEN_OBJECT_OUTPUT_SCHEMA),
2728
2786
  handler: async (args, ctx) => {
2729
- const { result } = await callCloudflare(base, {
2787
+ const { result } = await callCloudflare(send, {
2730
2788
  method: "GET",
2731
2789
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
2732
2790
  }, ctx);
@@ -2749,7 +2807,7 @@ function buildTools(scope) {
2749
2807
  },
2750
2808
  outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2751
2809
  handler: async (args, ctx) => {
2752
- const { result } = await callCloudflare(base, {
2810
+ const { result } = await callCloudflare(send, {
2753
2811
  method: "POST",
2754
2812
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
2755
2813
  body: { name: requireString(args, "domain") },
@@ -2774,7 +2832,7 @@ function buildTools(scope) {
2774
2832
  outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, domain: { type: "string" } }, required: ["deleted", "domain"] },
2775
2833
  handler: async (args, ctx) => {
2776
2834
  const domain = requireString(args, "domain");
2777
- await callCloudflare(base, {
2835
+ await callCloudflare(send, {
2778
2836
  method: "DELETE",
2779
2837
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains/${encodePathSegment(domain)}`,
2780
2838
  }, ctx);
@@ -2796,7 +2854,7 @@ function buildTools(scope) {
2796
2854
  },
2797
2855
  outputSchema: { type: "object", properties: { purged: { type: "boolean" } }, required: ["purged"] },
2798
2856
  handler: async (args, ctx) => {
2799
- await callCloudflare(base, {
2857
+ await callCloudflare(send, {
2800
2858
  method: "POST",
2801
2859
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/purge_build_cache`,
2802
2860
  }, ctx);
@@ -2819,7 +2877,7 @@ function buildTools(scope) {
2819
2877
  outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, projectName: { type: "string" } }, required: ["deleted", "projectName"] },
2820
2878
  handler: async (args, ctx) => {
2821
2879
  const projectName = requireString(args, "projectName");
2822
- await callCloudflare(base, {
2880
+ await callCloudflare(send, {
2823
2881
  method: "DELETE",
2824
2882
  path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(projectName)}`,
2825
2883
  }, ctx);
@@ -2831,7 +2889,7 @@ function buildTools(scope) {
2831
2889
  // `destructiveHint` stays unset. `readOnlyHint: false` already routes it
2832
2890
  // through call_destructive_tool.
2833
2891
  name: "create_dns_record",
2834
- description: "Create a content-based DNS record (A, AAAA, CNAME, MX, NS, OPENPGPKEY, PTR, TXT) in a zone. Check for an existing record with list_dns_records first: Cloudflare rejects a duplicate rather than replacing it. Record types that carry structured data, such as SRV and CAA, are readable here but not creatable.",
2892
+ description: "Create a content-based DNS record in a zone; the type enum lists the creatable types. Check list_dns_records first Cloudflare rejects a duplicate rather than replacing it. Structured types like SRV and CAA are readable but not creatable.",
2835
2893
  annotations: { readOnlyHint: false },
2836
2894
  inputSchema: {
2837
2895
  type: "object",
@@ -2886,7 +2944,7 @@ function buildTools(scope) {
2886
2944
  },
2887
2945
  outputSchema: DNS_RECORD_SCHEMA,
2888
2946
  handler: async (args, ctx) => {
2889
- const { result } = await callCloudflare(base, {
2947
+ const { result } = await callCloudflare(send, {
2890
2948
  method: "POST",
2891
2949
  path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
2892
2950
  body: {
@@ -2987,7 +3045,7 @@ function buildTools(scope) {
2987
3045
  },
2988
3046
  });
2989
3047
  }
2990
- const { result } = await callCloudflare(base, {
3048
+ const { result } = await callCloudflare(send, {
2991
3049
  method: "PATCH",
2992
3050
  path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
2993
3051
  body,
@@ -3021,7 +3079,7 @@ function buildTools(scope) {
3021
3079
  },
3022
3080
  handler: async (args, ctx) => {
3023
3081
  const recordId = String(args["recordId"]);
3024
- await callCloudflare(base, {
3082
+ await callCloudflare(send, {
3025
3083
  method: "DELETE",
3026
3084
  path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(recordId)}`,
3027
3085
  }, ctx);
@@ -3121,7 +3179,7 @@ function buildTools(scope) {
3121
3179
  });
3122
3180
  }
3123
3181
  const variant = everything ? "everything" : targeted[0];
3124
- await callCloudflare(base, {
3182
+ await callCloudflare(send, {
3125
3183
  method: "POST",
3126
3184
  path: `/zones/${encodeURIComponent(zoneId)}/purge_cache`,
3127
3185
  body: everything
@@ -3132,8 +3190,12 @@ function buildTools(scope) {
3132
3190
  },
3133
3191
  },
3134
3192
  ];
3193
+ return tools.map((tool) => ({
3194
+ ...tool,
3195
+ handler: (args, ctx) => tool.handler(args, withAuthentication(ctx, authentication)),
3196
+ }));
3135
3197
  }
3136
- function usageGuide(purpose, scope, instructions) {
3198
+ function usageGuide(purpose, scope, instructions, authentication) {
3137
3199
  const accountInstructions = instructions?.trim();
3138
3200
  const zoneLine = scope.zoneId
3139
3201
  ? `This connector defaults to zone \`${scope.zoneId}\`; omit \`zoneId\` unless the request names a different domain.`
@@ -3141,18 +3203,22 @@ function usageGuide(purpose, scope, instructions) {
3141
3203
  const accountLine = scope.accountId
3142
3204
  ? `It defaults to account \`${scope.accountId}\`; omit \`accountId\` unless the request names a different account.`
3143
3205
  : "It declares no default account. `list_accounts` supplies the `accountId` the Workers, KV, R2, and Pages tools need.";
3206
+ const authenticationLine = authentication === "apiToken"
3207
+ ? "The API token is operator-managed and scoped by permission. An `auth_required` failure means the token is missing, invalid, or lacks that call's permission. Call `verify_api_token` first."
3208
+ : "The Global API Key and account email are operator-managed. The key has the same access as its Cloudflare user. An `auth_required` failure means one field is missing, the pair is invalid, or the user lacks access. Call `verify_global_api_key` first.";
3144
3209
  return `# Cloudflare usage
3145
3210
 
3146
3211
  Account purpose: ${purpose}
3147
3212
 
3148
3213
  - ${zoneLine}
3149
3214
  - ${accountLine}
3150
- - Prefer a named tool: its schema is complete, projected, and enough to call it without provider documentation. For an operation without a named tool, use \`cloudflare_api_get\` for GET, \`cloudflare_api_mutate\` for JSON POST/PUT/PATCH/DELETE, or \`cloudflare_api_upload\` for raw and multipart content. Raw tools take a path below \`/client/v4\`; their argument schemas are complete, but endpoint-specific query, header, and body fields come from Cloudflare's API reference. Use \`headers\` for endpoint-specific controls such as \`cf-r2-jurisdiction\`, ETags, and object metadata; authentication, host, content type, and request framing remain connector-owned.
3151
- - The raw tools cover the wider control plane without weakening routing: GET is explicitly read-only; every mutation and upload is destructive and must cross the host's approval boundary. The API token remains the hard provider-side permission boundary. Absolute URLs, traversal, and query strings embedded in \`path\` are refused locally.
3215
+ - Prefer a named tool: its schema is complete, projected, and enough to call it without provider documentation. For an operation without a named tool, use \`cloudflare_api_get\` for GET, \`cloudflare_api_mutate\` for JSON POST/PUT/PATCH/DELETE, or \`cloudflare_api_upload\` for raw and multipart content. Raw tools take a path below \`/client/v4\`; their argument schemas are complete, but endpoint-specific query, header, and body fields come from Cloudflare's API reference. Use \`headers\` for endpoint-specific controls such as \`cf-r2-jurisdiction\`, \`Range\`, \`If-None-Match\`, and Cloudflare product metadata. \`Authorization\`, \`Cookie\`, \`Host\`, \`Content-Length\`, \`Content-Type\`, and \`Transfer-Encoding\` are connector-owned and refused: authentication, host, content type, and request framing are not the caller's to set.
3216
+ - The raw tools cover the wider control plane without weakening routing: GET is explicitly read-only; every mutation and upload is destructive and must cross the host's approval boundary. The configured Cloudflare credential remains the hard provider-side permission boundary. Absolute URLs, traversal, and query strings embedded in \`path\` are refused locally.
3152
3217
  - Useful raw paths include \`/accounts/{accountId}/images/v1\` (Images), \`/accounts/{accountId}/stream\` (Stream), \`/zones/{zoneId}/email/routing/rules\` (Email Routing), \`/accounts/{accountId}/d1/database\` (D1), and \`/accounts/{accountId}/queues\` (Queues). On GET, use \`responseType: "text"\` or \`"base64"\` for non-JSON content. Direct-upload endpoints can issue upload URLs; \`cloudflare_api_upload\` can also send explicit text, base64 bytes, or multipart fields/files.
3153
- - Lists paginate with \`page\` and \`perPage\` and return a \`page\` object; request the next page only when \`page.hasMore\` is true. \`list_zone_rulesets\`, \`list_r2_buckets\`, \`list_r2_objects\`, and \`list_kv_keys\` instead return \`nextCursor\`; \`list_worker_scripts\` is unpaginated.
3218
+ - Two R2 areas are deliberately unnamed. Read a bucket's CORS policy with \`get_r2_cors\`, then change it with \`cloudflare_api_mutate\` \`PUT\` or \`DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors\`, rule fields per Cloudflare's reference — and read account storage totals with \`cloudflare_api_get\` at \`/accounts/{accountId}/r2/metrics\`.
3219
+ - Lists paginate with \`page\` and \`perPage\` and return a \`page\` object; request the next page only when \`page.hasMore\` is true. \`list_zone_rulesets\`, \`list_r2_buckets\`, \`list_r2_objects\`, and \`list_kv_keys\` page by cursor instead: pass \`cursor\`, continue while \`nextCursor\` is present, and expect no \`page\` object. Their schemas say so too. \`list_worker_scripts\` is unpaginated.
3154
3220
  - Results are projected to the fields that identify and describe a resource. Pass \`raw: true\` on a read when you genuinely need a field the projection drops.
3155
- - The API token is operator-managed and scoped by permission, not by role. An \`auth_required\` failure means the token is missing, invalid, or lacks that call's permission — it is never fixed by retrying. Call \`verify_api_token\` to tell a dead token from a missing permission, then report which permission is needed rather than trying other tools.
3221
+ - ${authenticationLine}
3156
3222
  - A \`rate_limited\` failure carries the wait window. Cloudflare's limit is 1,200 requests per five minutes per user, counted across the dashboard and every token, so do not fan out speculatively; filter server-side with \`name\`, \`type\`, and \`content\` instead of listing everything and filtering locally.
3157
3223
  - Named creates that only add a resource are write-routed without claiming destruction. Updates, overwrites, deletes, rollbacks, cache purges, \`cloudflare_api_mutate\`, and \`cloudflare_api_upload\` are destructive. Read current state before changing it, and prefer a targeted \`purge_cache\` over \`everything\`.
3158
3224
  ${accountInstructions
@@ -3169,42 +3235,83 @@ export function cloudflare(id, options) {
3169
3235
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
3170
3236
  throw new Error("cloudflare() maxConcurrency must be a positive integer.");
3171
3237
  }
3238
+ const authentication = options.authentication ?? "apiToken";
3239
+ if (authentication !== "apiToken" && authentication !== "globalApiKey") {
3240
+ throw new Error('cloudflare() authentication must be "apiToken" or "globalApiKey".');
3241
+ }
3172
3242
  const scope = {
3173
- base: options.baseUrl?.trim() || CLOUDFLARE_API_BASE,
3243
+ send: cloudflareTransport(options.baseUrl?.trim() || CLOUDFLARE_API_BASE),
3174
3244
  accountId: options.accountId?.trim() || undefined,
3175
3245
  zoneId: options.zoneId?.trim() || undefined,
3176
3246
  };
3177
3247
  return api(id, {
3178
3248
  title: options.title ?? "Cloudflare",
3179
3249
  description: `Cloudflare control-plane access for zones, DNS, Workers, KV, R2, Pages, media, email, and other v4 APIs — ${purpose}`,
3180
- credential: options.credential ?? DEFAULT_CREDENTIAL,
3250
+ credential: credentialConfig(authentication, options.credential),
3181
3251
  callAdmission: admissionPolicy(maxConcurrency),
3182
- usageGuide: usageGuide(purpose, scope, options.instructions),
3183
- // The schemas are hand-written and closed; a schema that cannot be
3184
- // enforced is a bug in this file, not input to pass through.
3185
- strictValidation: true,
3252
+ usageGuide: {
3253
+ content: usageGuide(purpose, scope, options.instructions, authentication),
3254
+ // Explicit rather than derived: the first content line is the zone
3255
+ // scoping rule, which varies per deployment and reads as an instruction
3256
+ // rather than as the routing fact a browsing agent needs.
3257
+ summary: "Zone and account scoping, named-vs-raw routing, two pagination shapes, and lean-vs-raw results.",
3258
+ // Deliberately not `required`. Every named tool's schema is complete
3259
+ // enough to call it correctly on its own, and the scoping convention the
3260
+ // guide carries is repeated on each `zoneId` and `accountId` property —
3261
+ // so forcing the guide into context before every operation would spend
3262
+ // tokens on a sequence the schemas already express.
3263
+ },
3186
3264
  ...(options.maxResultBytes !== undefined
3187
3265
  ? { maxResultBytes: options.maxResultBytes }
3188
3266
  : {}),
3189
- tools: buildTools(scope),
3190
- async testCredential(value, ctx) {
3191
- try {
3192
- const { result } = await callCloudflare(scope.base, { method: "GET", path: "/user/tokens/verify" }, {
3193
- ...ctx,
3194
- credential: { get: async () => value, getAll: async () => ({ value }) },
3195
- });
3196
- const status = asRecord(result)["status"];
3197
- return status === "active"
3198
- ? { ok: true, message: "Token verified: active." }
3199
- : { ok: false, message: `Token status is "${String(status)}".` };
3200
- }
3201
- catch (error) {
3202
- return {
3203
- ok: false,
3204
- message: error instanceof Error ? error.message : String(error),
3205
- };
3267
+ tools: buildTools(scope, authentication),
3268
+ ...(authentication === "apiToken"
3269
+ ? {
3270
+ async testCredential(value, ctx) {
3271
+ try {
3272
+ const { result } = await callCloudflare(scope.send, { method: "GET", path: "/user/tokens/verify" }, withAuthentication({
3273
+ ...ctx,
3274
+ credential: {
3275
+ get: async () => value,
3276
+ getAll: async () => ({ value }),
3277
+ },
3278
+ }, authentication));
3279
+ const status = asRecord(result)["status"];
3280
+ return status === "active"
3281
+ ? { ok: true, message: "Token verified: active." }
3282
+ : { ok: false, message: `Token status is "${String(status)}".` };
3283
+ }
3284
+ catch (error) {
3285
+ return {
3286
+ ok: false,
3287
+ message: error instanceof Error ? error.message : String(error),
3288
+ };
3289
+ }
3290
+ },
3206
3291
  }
3207
- },
3292
+ : {
3293
+ async testCredentials(values, ctx) {
3294
+ try {
3295
+ const { result } = await callCloudflare(scope.send, { method: "GET", path: "/user" }, withAuthentication({
3296
+ ...ctx,
3297
+ credential: {
3298
+ get: async (field) => field ? values[field] ?? null : null,
3299
+ getAll: async () => values,
3300
+ },
3301
+ }, authentication));
3302
+ const email = asRecord(result)["email"];
3303
+ return {
3304
+ ok: true,
3305
+ message: `Global API Key verified for ${String(email)}.`,
3306
+ };
3307
+ }
3308
+ catch (error) {
3309
+ return {
3310
+ ok: false,
3311
+ message: error instanceof Error ? error.message : String(error),
3312
+ };
3313
+ }
3314
+ },
3315
+ }),
3208
3316
  });
3209
3317
  }
3210
- //# sourceMappingURL=cloudflare.js.map