@zackbart/connecta 0.15.1 → 0.16.1

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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  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 +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -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 +30 -3
  42. package/dist/errors.js +1 -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 +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  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 -60
  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 +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,3999 +0,0 @@
1
- /**
2
- * Cloudflare's REST API as a deliberate, hand-written tool surface.
3
- *
4
- * Deliberately SDK-free. The `cloudflare` npm package is a generated client
5
- * whose value is typed wrappers and pagination helpers — both of which this
6
- * connection re-projects anyway, because an agent needs a lean result shape,
7
- * not Cloudflare's full response object. Every call here is `fetch` against
8
- * documented paths with a Bearer token, which keeps the provider Workers-clean,
9
- * adds no dependency (optional peer or otherwise), and leaves the published
10
- * surface exactly where `ethos.md` puts it.
11
- *
12
- * The tools are hand-written rather than generated because a generated wrapper
13
- * is what motivated this file: a compact schema that says `arguments?: {}[]`
14
- * forces an agent to read operation documentation before it can call anything.
15
- * Every tool below therefore carries a complete input schema, an accurate
16
- * required-key list, and a declared output shape.
17
- */
18
- import { api, type ApiTool } from "../connectors/api.js";
19
- import { ConnectorCallError } from "../errors.js";
20
- import type {
21
- Connector,
22
- ConnectorCallAdmissionPolicy,
23
- ConnectorContext,
24
- ConnectorCredentialConfig,
25
- JsonSchema,
26
- } from "../types.js";
27
-
28
- /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
29
- export const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
30
-
31
- /** Authentication schemes accepted by Cloudflare's v4 API. */
32
- export type CloudflareAuthentication = "apiToken" | "globalApiKey";
33
-
34
- /**
35
- * Every DNS record type the records API accepts, for filtering a list.
36
- * Enumerated in the schema so an agent picks a legal type without reading
37
- * Cloudflare's documentation.
38
- */
39
- export const CLOUDFLARE_DNS_RECORD_TYPES = [
40
- "A",
41
- "AAAA",
42
- "CAA",
43
- "CERT",
44
- "CNAME",
45
- "DNSKEY",
46
- "DS",
47
- "HTTPS",
48
- "LOC",
49
- "MX",
50
- "NAPTR",
51
- "NS",
52
- "OPENPGPKEY",
53
- "PTR",
54
- "SMIMEA",
55
- "SRV",
56
- "SSHFP",
57
- "SVCB",
58
- "TLSA",
59
- "TXT",
60
- "URI",
61
- ] as const;
62
-
63
- /**
64
- * The record types whose value is a single `content` string — the eight this
65
- * connection can create and update.
66
- *
67
- * The other thirteen (CAA, CERT, DNSKEY, DS, HTTPS, LOC, NAPTR, SMIMEA, SRV,
68
- * SSHFP, SVCB, TLSA, URI) carry a per-type structured `data` object instead,
69
- * each with its own field set. Accepting them here would mean either a
70
- * free-form `data` passthrough — exactly the untyped `{}` this connection
71
- * exists to avoid — or thirteen more hand-written schemas for record types
72
- * that are rare in the day-to-day work this surface is for. They remain fully
73
- * readable and filterable; only the named create/update tools omit them. The
74
- * guarded raw mutation tool remains available for their documented bodies.
75
- */
76
- export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
77
- "A",
78
- "AAAA",
79
- "CNAME",
80
- "MX",
81
- "NS",
82
- "OPENPGPKEY",
83
- "PTR",
84
- "TXT",
85
- ] as const;
86
-
87
- export interface CloudflareOptions {
88
- /** Human-readable display name; defaults to "Cloudflare". */
89
- title?: string;
90
- /** Which account/estate this connection administers, and for whom. */
91
- purpose: string;
92
- /**
93
- * Default account id for account-scoped tools. When set, `accountId` becomes
94
- * an optional argument; when omitted, agents must pass one and can find it
95
- * with `list_accounts`.
96
- */
97
- accountId?: string;
98
- /**
99
- * Default zone id for zone-scoped tools. When set, `zoneId` becomes an
100
- * optional argument; when omitted, agents must pass one and can find it with
101
- * `list_zones`.
102
- */
103
- zoneId?: string;
104
- /** API base override for a proxy or a test double. Defaults to the v4 API. */
105
- baseUrl?: string;
106
- /** Authentication scheme. Defaults to the recommended scoped API token. */
107
- authentication?: CloudflareAuthentication;
108
- /** Credential presentation override; credentials are always operator-managed. */
109
- credential?: ConnectorCredentialConfig;
110
- /** Account-specific conventions appended to the maintained provider guide. */
111
- instructions?: string;
112
- /** Connector-specific inline result limit; omit to inherit the deployment. */
113
- maxResultBytes?: number;
114
- /** Simultaneous downstream calls. Defaults to 6. */
115
- maxConcurrency?: number;
116
- }
117
-
118
- /**
119
- * Cloudflare documents a global limit of 1,200 requests per five minutes per
120
- * user, counted cumulatively across the dashboard, API keys, and API tokens.
121
- * The budget mirrors that window; `maxConcurrency` is the part that actually
122
- * protects a shared token, because a single `execute_code` program can fan out
123
- * far faster than the window notices.
124
- */
125
- function admissionPolicy(maxConcurrency: number): ConnectorCallAdmissionPolicy {
126
- return {
127
- rules: [
128
- {
129
- maxConcurrency,
130
- budget: {
131
- kind: "rolling-window",
132
- maxCalls: 1200,
133
- windowMs: 300_000,
134
- },
135
- },
136
- ],
137
- };
138
- }
139
-
140
- const API_TOKEN_CREDENTIAL: ConnectorCredentialConfig = {
141
- label: "Cloudflare API token",
142
- description:
143
- "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.",
144
- placeholder: "Paste API token",
145
- };
146
-
147
- const GLOBAL_API_KEY_CREDENTIAL: ConnectorCredentialConfig = {
148
- label: "Cloudflare Global API Key",
149
- description:
150
- "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.",
151
- fields: [
152
- {
153
- name: "email",
154
- label: "Account email",
155
- description: "The verified email address for the Cloudflare user that owns the Global API Key.",
156
- placeholder: "you@example.com",
157
- inputType: "email",
158
- },
159
- {
160
- name: "apiKey",
161
- label: "Global API Key",
162
- description: "The legacy Global API Key from My Profile → API Tokens.",
163
- placeholder: "Paste Global API Key",
164
- inputType: "password",
165
- },
166
- ],
167
- };
168
-
169
- function credentialConfig(
170
- authentication: CloudflareAuthentication,
171
- override: ConnectorCredentialConfig | undefined,
172
- ): ConnectorCredentialConfig {
173
- if (authentication === "apiToken") {
174
- const credential = override ?? API_TOKEN_CREDENTIAL;
175
- if (credential.fields?.length) {
176
- throw new Error(
177
- "cloudflare() API token authentication requires a single-value credential.",
178
- );
179
- }
180
- return credential;
181
- }
182
-
183
- const credential = override
184
- ? {
185
- ...GLOBAL_API_KEY_CREDENTIAL,
186
- ...override,
187
- fields: override.fields ?? GLOBAL_API_KEY_CREDENTIAL.fields!,
188
- }
189
- : GLOBAL_API_KEY_CREDENTIAL;
190
- const fields = credential.fields?.map((field) => field.name).sort();
191
- if (fields?.join(",") !== "apiKey,email") {
192
- throw new Error(
193
- 'cloudflare() Global API Key authentication requires credential fields named "email" and "apiKey".',
194
- );
195
- }
196
- return credential;
197
- }
198
-
199
- // --- Cloudflare's response envelope -----------------------------------------
200
-
201
- interface CloudflareEnvelopeError {
202
- code?: number;
203
- message?: string;
204
- error_chain?: CloudflareEnvelopeError[];
205
- }
206
-
207
- interface CloudflareResultInfo {
208
- page?: number;
209
- per_page?: number;
210
- count?: number;
211
- total_count?: number;
212
- total_pages?: number;
213
- /** Cursor-paginated endpoints (R2 buckets, KV keys) report this instead. */
214
- cursor?: string;
215
- is_truncated?: boolean;
216
- delimited?: string[];
217
- cursors?: { after?: string; before?: string };
218
- }
219
-
220
- interface CloudflareEnvelope {
221
- success?: boolean;
222
- errors?: CloudflareEnvelopeError[];
223
- messages?: unknown[];
224
- result?: unknown;
225
- result_info?: CloudflareResultInfo;
226
- }
227
-
228
- type JsonRecord = Record<string, unknown>;
229
-
230
- function asRecord(value: unknown): JsonRecord {
231
- return value && typeof value === "object" && !Array.isArray(value)
232
- ? (value as JsonRecord)
233
- : {};
234
- }
235
-
236
- function asArray(value: unknown): unknown[] {
237
- return Array.isArray(value) ? value : [];
238
- }
239
-
240
- /** Flatten Cloudflare's error array (and any nested chain) into one line. */
241
- function describeErrors(errors: CloudflareEnvelopeError[]): string {
242
- const parts: string[] = [];
243
- const walk = (list: CloudflareEnvelopeError[]): void => {
244
- for (const entry of list) {
245
- const code = typeof entry.code === "number" ? entry.code : undefined;
246
- const message =
247
- typeof entry.message === "string" ? entry.message : "Unknown error";
248
- parts.push(code === undefined ? message : `${code}: ${message}`);
249
- if (Array.isArray(entry.error_chain)) walk(entry.error_chain);
250
- }
251
- };
252
- walk(errors);
253
- return parts.length > 0 ? parts.join("; ") : "Cloudflare reported no detail.";
254
- }
255
-
256
- function errorCodes(errors: CloudflareEnvelopeError[]): Set<number> {
257
- const codes = new Set<number>();
258
- const walk = (list: CloudflareEnvelopeError[]): void => {
259
- for (const entry of list) {
260
- if (typeof entry.code === "number") codes.add(entry.code);
261
- if (Array.isArray(entry.error_chain)) walk(entry.error_chain);
262
- }
263
- };
264
- walk(errors);
265
- return codes;
266
- }
267
-
268
- /**
269
- * Credential-shaped Cloudflare error codes that are *not* already implied by a
270
- * 401 or 403: a missing or malformed `Authorization` header, and the legacy
271
- * key/email headers. These arrive on HTTP 400, so status alone would misfile
272
- * them as an argument problem the agent could repair.
273
- *
274
- * Provenance matters here: Cloudflare publishes no official code table, so this
275
- * set comes from community reports and observed responses rather than
276
- * documentation. It is a narrow, well-attested list, not an authoritative one —
277
- * which is why status is the primary signal and these six only rescue the 400s
278
- * status gets wrong.
279
- *
280
- * Deliberately excludes 10000. Cloudflare returns 10000 for "Authentication
281
- * error" but has also been observed reusing it as a generic validation code
282
- * ("domain_name is required", "Invalid pagination cursor"), so routing on it
283
- * would risk telling an agent its token was broken when its arguments were.
284
- * Genuine 10000 auth failures arrive with 401 or 403 and are caught by status.
285
- *
286
- * All of these route to `auth_required`, whose recovery mode resolves to
287
- * `operator_config` because this connection declares an operator-managed
288
- * credential rather than an OAuth flow.
289
- */
290
- const AUTH_ERROR_CODES = new Set([1001, 6003, 6111, 9103, 9106, 9107]);
291
-
292
- /** Seconds in a `retry-after` header, converted to the milliseconds the core wants. */
293
- function retryAfterMs(headers: Headers): number | undefined {
294
- const raw = headers.get("retry-after");
295
- if (!raw) return undefined;
296
- const seconds = Number(raw.trim());
297
- if (!Number.isFinite(seconds) || seconds < 0) return undefined;
298
- return Math.trunc(seconds * 1000);
299
- }
300
-
301
- /**
302
- * Turn a failed Cloudflare response into a typed connector failure.
303
- *
304
- * Status is the primary signal and the error codes refine it, because
305
- * Cloudflare returns 403 for both "this token is invalid" and "this token
306
- * cannot do that" — an agent needs to stop retrying either way, and the
307
- * operator needs to know the token is the thing to fix.
308
- */
309
- function failureFor(
310
- status: number,
311
- headers: Headers,
312
- errors: CloudflareEnvelopeError[],
313
- ): ConnectorCallError {
314
- const detail = describeErrors(errors);
315
- const codes = errorCodes(errors);
316
- // 429 is checked before the auth codes on purpose: Cloudflare reuses the
317
- // generic 10000 code on throttled responses too, and reading a rate limit as
318
- // an auth failure would tell an agent to stop when it should wait.
319
- if (status === 429) {
320
- const wait = retryAfterMs(headers);
321
- return new ConnectorCallError(
322
- "rate_limited",
323
- `Cloudflare rate limit reached (HTTP 429). ${detail} The documented limit is 1,200 requests per five minutes per user, counted across the dashboard and every token.`,
324
- // Cloudflare blocks the remainder of the five-minute window when the
325
- // global limit trips, so the honest fallback is the whole window.
326
- { retryAfterMs: wait ?? 300_000 },
327
- );
328
- }
329
- const authCoded = [...codes].some((code) => AUTH_ERROR_CODES.has(code));
330
- if (status === 401 || status === 403 || authCoded) {
331
- return new ConnectorCallError(
332
- "auth_required",
333
- `Cloudflare rejected the configured credential (HTTP ${status}). ${detail} Check that it is valid and has permission to access this resource.`,
334
- );
335
- }
336
- if (status === 400 || status === 409 || status === 422) {
337
- return new ConnectorCallError(
338
- "invalid_args",
339
- `Cloudflare rejected the request (HTTP ${status}). ${detail}`,
340
- );
341
- }
342
- if (status === 404) {
343
- return new ConnectorCallError(
344
- "connector_call_failed",
345
- `Cloudflare found no such resource (HTTP 404). ${detail} Confirm the zone or account id with list_zones or list_accounts.`,
346
- );
347
- }
348
- if (status >= 500) {
349
- return new ConnectorCallError(
350
- "unavailable",
351
- `Cloudflare is unavailable (HTTP ${status}). ${detail}`,
352
- );
353
- }
354
- return new ConnectorCallError(
355
- "connector_call_failed",
356
- `Cloudflare request failed (HTTP ${status}). ${detail}`,
357
- );
358
- }
359
-
360
- // --- The request path --------------------------------------------------------
361
-
362
- interface RequestSpec {
363
- method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
364
- path: string;
365
- query?: Record<string, string | number | boolean | undefined>;
366
- headers?: Record<string, string | undefined>;
367
- body?: unknown;
368
- rawBody?: BodyInit;
369
- }
370
-
371
- interface CloudflareResponse {
372
- result: unknown;
373
- resultInfo: CloudflareResultInfo | undefined;
374
- }
375
-
376
- const AUTHENTICATION_CONTEXT = Symbol("cloudflareAuthentication");
377
-
378
- type CloudflareContext = ConnectorContext & {
379
- [AUTHENTICATION_CONTEXT]?: CloudflareAuthentication;
380
- };
381
-
382
- function withAuthentication(
383
- ctx: ConnectorContext,
384
- authentication: CloudflareAuthentication,
385
- ): CloudflareContext {
386
- return { ...ctx, [AUTHENTICATION_CONTEXT]: authentication };
387
- }
388
-
389
- async function readAuthenticationHeaders(
390
- ctx: CloudflareContext,
391
- ): Promise<Record<string, string>> {
392
- if (ctx[AUTHENTICATION_CONTEXT] === "globalApiKey") {
393
- const values = await ctx.credential?.getAll();
394
- const email = values?.["email"];
395
- const apiKey = values?.["apiKey"];
396
- if (!email || !apiKey) {
397
- throw new ConnectorCallError(
398
- "auth_required",
399
- "No Cloudflare Global API Key and account email are configured for this connector. An operator must add both before any call can run.",
400
- );
401
- }
402
- return { "X-Auth-Email": email, "X-Auth-Key": apiKey };
403
- }
404
-
405
- const token = await ctx.credential?.get();
406
- if (!token) {
407
- throw new ConnectorCallError(
408
- "auth_required",
409
- "No Cloudflare API token is configured for this connector. An operator must add one before any call can run.",
410
- );
411
- }
412
- return { Authorization: `Bearer ${token}` };
413
- }
414
-
415
- function buildUrl(base: string, spec: RequestSpec): string {
416
- const url = new URL(`${base.replace(/\/+$/, "")}${spec.path}`);
417
- for (const [key, value] of Object.entries(spec.query ?? {})) {
418
- if (value === undefined) continue;
419
- url.searchParams.set(key, String(value));
420
- }
421
- return url.toString();
422
- }
423
-
424
- async function fetchCloudflare(
425
- base: string,
426
- spec: RequestSpec,
427
- ctx: ConnectorContext,
428
- ): Promise<Response> {
429
- const authenticationHeaders = await readAuthenticationHeaders(ctx);
430
- if (spec.body !== undefined && spec.rawBody !== undefined) {
431
- throw new Error("A Cloudflare request cannot have both JSON and raw bodies.");
432
- }
433
- let response: Response;
434
- try {
435
- response = await fetch(buildUrl(base, spec), {
436
- method: spec.method,
437
- headers: {
438
- ...authenticationHeaders,
439
- Accept: "application/json",
440
- ...(spec.body !== undefined
441
- ? { "Content-Type": "application/json" }
442
- : {}),
443
- ...Object.fromEntries(
444
- Object.entries(spec.headers ?? {}).filter(
445
- (entry): entry is [string, string] => entry[1] !== undefined,
446
- ),
447
- ),
448
- },
449
- ...(spec.body !== undefined
450
- ? { body: JSON.stringify(spec.body) }
451
- : spec.rawBody !== undefined
452
- ? { body: spec.rawBody }
453
- : {}),
454
- ...(ctx.signal ? { signal: ctx.signal } : {}),
455
- });
456
- } catch (cause) {
457
- throw new ConnectorCallError(
458
- "unavailable",
459
- `Could not reach the Cloudflare API: ${
460
- cause instanceof Error ? cause.message : String(cause)
461
- }`,
462
- { cause },
463
- );
464
- }
465
- return response;
466
- }
467
-
468
- async function callCloudflare(
469
- base: string,
470
- spec: RequestSpec,
471
- ctx: ConnectorContext,
472
- ): Promise<CloudflareResponse> {
473
- const response = await fetchCloudflare(base, spec, ctx);
474
-
475
- let envelope: CloudflareEnvelope;
476
- try {
477
- envelope = (await response.json()) as CloudflareEnvelope;
478
- } catch (cause) {
479
- // A gateway error page, not JSON: the status is the only real signal left.
480
- throw response.ok
481
- ? new ConnectorCallError(
482
- "unavailable",
483
- "Cloudflare returned a non-JSON body for a successful status.",
484
- { cause },
485
- )
486
- : failureFor(response.status, response.headers, []);
487
- }
488
-
489
- const errors = Array.isArray(envelope.errors) ? envelope.errors : [];
490
- if (!response.ok || envelope.success === false) {
491
- throw failureFor(response.status, response.headers, errors);
492
- }
493
- const isV4Envelope =
494
- "success" in envelope ||
495
- "result" in envelope ||
496
- "result_info" in envelope ||
497
- "messages" in envelope;
498
- return {
499
- // `/graphql` and a small number of product APIs return ordinary JSON
500
- // instead of the standard v4 envelope. Preserve that document whole so
501
- // the raw tools cover them too.
502
- result: isV4Envelope ? envelope.result : envelope,
503
- resultInfo: isV4Envelope ? envelope.result_info : undefined,
504
- };
505
- }
506
-
507
- function base64FromBytes(bytes: Uint8Array): string {
508
- let binary = "";
509
- for (let offset = 0; offset < bytes.length; offset += 0x8000) {
510
- binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
511
- }
512
- return btoa(binary);
513
- }
514
-
515
- async function callCloudflareContent(
516
- base: string,
517
- spec: RequestSpec,
518
- ctx: ConnectorContext,
519
- responseType: "text" | "base64",
520
- ): Promise<JsonRecord> {
521
- const response = await fetchCloudflare(base, spec, ctx);
522
- if (!response.ok) {
523
- let errors: CloudflareEnvelopeError[] = [];
524
- try {
525
- const envelope = (await response.clone().json()) as CloudflareEnvelope;
526
- if (Array.isArray(envelope.errors)) errors = envelope.errors;
527
- } catch {
528
- // A raw or gateway error body has no structured detail to preserve.
529
- }
530
- throw failureFor(response.status, response.headers, errors);
531
- }
532
- const common = {
533
- contentType: response.headers.get("content-type") ?? "application/octet-stream",
534
- ...(response.headers.get("etag")
535
- ? { etag: response.headers.get("etag")! }
536
- : {}),
537
- };
538
- if (responseType === "text") {
539
- return { ...common, text: await response.text() };
540
- }
541
- return {
542
- ...common,
543
- base64: base64FromBytes(new Uint8Array(await response.arrayBuffer())),
544
- };
545
- }
546
-
547
- // --- Projections -------------------------------------------------------------
548
-
549
- interface PageInfo {
550
- page: number;
551
- perPage: number;
552
- count: number;
553
- totalCount?: number;
554
- totalPages?: number;
555
- hasMore: boolean;
556
- }
557
-
558
- /**
559
- * Cloudflare's `result_info` reshaped into the one question an agent actually
560
- * asks — is there another page? — with the raw counters kept alongside it.
561
- */
562
- function pageInfo(info: CloudflareResultInfo | undefined): PageInfo | undefined {
563
- if (!info) return undefined;
564
- // A cursor-only result_info carries no page counters; inventing them would
565
- // report `hasMore: false` on a listing that has more.
566
- if (
567
- info.page === undefined &&
568
- info.total_pages === undefined &&
569
- info.count === undefined
570
- ) {
571
- return undefined;
572
- }
573
- const page = typeof info.page === "number" ? info.page : 1;
574
- const totalPages =
575
- typeof info.total_pages === "number" ? info.total_pages : undefined;
576
- return {
577
- page,
578
- perPage: typeof info.per_page === "number" ? info.per_page : 0,
579
- count: typeof info.count === "number" ? info.count : 0,
580
- ...(typeof info.total_count === "number"
581
- ? { totalCount: info.total_count }
582
- : {}),
583
- ...(totalPages !== undefined ? { totalPages } : {}),
584
- hasMore: totalPages !== undefined ? page < totalPages : false,
585
- };
586
- }
587
-
588
- function projectAccount(value: unknown): JsonRecord {
589
- const account = asRecord(value);
590
- return {
591
- id: account["id"],
592
- name: account["name"],
593
- ...(account["type"] !== undefined ? { type: account["type"] } : {}),
594
- ...(account["created_on"] !== undefined
595
- ? { createdOn: account["created_on"] }
596
- : {}),
597
- };
598
- }
599
-
600
- function projectZone(value: unknown): JsonRecord {
601
- const zone = asRecord(value);
602
- const account = asRecord(zone["account"]);
603
- const plan = asRecord(zone["plan"]);
604
- return {
605
- id: zone["id"],
606
- name: zone["name"],
607
- status: zone["status"],
608
- paused: zone["paused"],
609
- type: zone["type"],
610
- accountId: account["id"],
611
- accountName: account["name"],
612
- ...(plan["name"] !== undefined ? { plan: plan["name"] } : {}),
613
- ...(Array.isArray(zone["name_servers"])
614
- ? { nameServers: zone["name_servers"] }
615
- : {}),
616
- createdOn: zone["created_on"],
617
- modifiedOn: zone["modified_on"],
618
- };
619
- }
620
-
621
- function projectDnsRecord(value: unknown): JsonRecord {
622
- const record = asRecord(value);
623
- return {
624
- id: record["id"],
625
- name: record["name"],
626
- type: record["type"],
627
- content: record["content"],
628
- ttl: record["ttl"],
629
- ...(record["proxied"] !== undefined
630
- ? { proxied: record["proxied"] }
631
- : {}),
632
- ...(record["priority"] !== undefined
633
- ? { priority: record["priority"] }
634
- : {}),
635
- ...(record["comment"] ? { comment: record["comment"] } : {}),
636
- ...(Array.isArray(record["tags"]) && record["tags"].length > 0
637
- ? { tags: record["tags"] }
638
- : {}),
639
- createdOn: record["created_on"],
640
- modifiedOn: record["modified_on"],
641
- };
642
- }
643
-
644
- function projectWorkerScript(value: unknown): JsonRecord {
645
- const script = asRecord(value);
646
- return {
647
- id: script["id"],
648
- createdOn: script["created_on"],
649
- modifiedOn: script["modified_on"],
650
- ...(script["usage_model"] !== undefined
651
- ? { usageModel: script["usage_model"] }
652
- : {}),
653
- };
654
- }
655
-
656
- function projectKvNamespace(value: unknown): JsonRecord {
657
- const namespace = asRecord(value);
658
- return {
659
- id: namespace["id"],
660
- title: namespace["title"],
661
- ...(namespace["supports_url_encoding"] !== undefined
662
- ? { supportsUrlEncoding: namespace["supports_url_encoding"] }
663
- : {}),
664
- };
665
- }
666
-
667
- function projectR2Bucket(value: unknown): JsonRecord {
668
- const bucket = asRecord(value);
669
- return {
670
- name: bucket["name"],
671
- ...(bucket["location"] !== undefined
672
- ? { location: bucket["location"] }
673
- : {}),
674
- ...(bucket["storage_class"] !== undefined
675
- ? { storageClass: bucket["storage_class"] }
676
- : {}),
677
- ...(bucket["jurisdiction"] !== undefined
678
- ? { jurisdiction: bucket["jurisdiction"] }
679
- : {}),
680
- ...(bucket["creation_date"] !== undefined
681
- ? { creationDate: bucket["creation_date"] }
682
- : {}),
683
- };
684
- }
685
-
686
- function projectR2Object(value: unknown): JsonRecord {
687
- const object = asRecord(value);
688
- return {
689
- key: object["key"],
690
- size: object["size"],
691
- etag: object["etag"],
692
- lastModified: object["last_modified"],
693
- ...(object["storage_class"] !== undefined
694
- ? { storageClass: object["storage_class"] }
695
- : {}),
696
- ...(object["http_metadata"] !== undefined
697
- ? { httpMetadata: object["http_metadata"] }
698
- : {}),
699
- ...(object["custom_metadata"] !== undefined
700
- ? { customMetadata: object["custom_metadata"] }
701
- : {}),
702
- };
703
- }
704
-
705
- function projectKvKey(value: unknown): JsonRecord {
706
- const key = asRecord(value);
707
- return {
708
- name: key["name"],
709
- ...(key["expiration"] !== undefined ? { expiration: key["expiration"] } : {}),
710
- ...(key["metadata"] !== undefined ? { metadata: key["metadata"] } : {}),
711
- };
712
- }
713
-
714
- function projectWorkerDeployment(value: unknown): JsonRecord {
715
- const deployment = asRecord(value);
716
- return {
717
- id: deployment["id"],
718
- ...(deployment["created_on"] !== undefined
719
- ? { createdOn: deployment["created_on"] }
720
- : {}),
721
- ...(deployment["source"] !== undefined ? { source: deployment["source"] } : {}),
722
- ...(deployment["strategy"] !== undefined
723
- ? { strategy: deployment["strategy"] }
724
- : {}),
725
- ...(deployment["versions"] !== undefined
726
- ? { versions: deployment["versions"] }
727
- : {}),
728
- };
729
- }
730
-
731
- function projectPagesDeployment(value: unknown): JsonRecord {
732
- const deployment = asRecord(value);
733
- return {
734
- id: deployment["id"],
735
- ...(deployment["project_name"] !== undefined
736
- ? { projectName: deployment["project_name"] }
737
- : {}),
738
- ...(deployment["environment"] !== undefined
739
- ? { environment: deployment["environment"] }
740
- : {}),
741
- ...(deployment["url"] !== undefined ? { url: deployment["url"] } : {}),
742
- ...(deployment["aliases"] !== undefined ? { aliases: deployment["aliases"] } : {}),
743
- ...(deployment["stage"] !== undefined ? { stage: deployment["stage"] } : {}),
744
- ...(deployment["latest_stage"] !== undefined
745
- ? { latestStage: deployment["latest_stage"] }
746
- : {}),
747
- ...(deployment["created_on"] !== undefined
748
- ? { createdOn: deployment["created_on"] }
749
- : {}),
750
- ...(deployment["modified_on"] !== undefined
751
- ? { modifiedOn: deployment["modified_on"] }
752
- : {}),
753
- };
754
- }
755
-
756
- function projectPagesDomain(value: unknown): JsonRecord {
757
- const domain = asRecord(value);
758
- return {
759
- id: domain["id"],
760
- name: domain["name"],
761
- ...(domain["status"] !== undefined ? { status: domain["status"] } : {}),
762
- ...(domain["verification_data"] !== undefined
763
- ? { verificationData: domain["verification_data"] }
764
- : {}),
765
- ...(domain["created_on"] !== undefined
766
- ? { createdOn: domain["created_on"] }
767
- : {}),
768
- };
769
- }
770
-
771
- function projectRuleset(value: unknown): JsonRecord {
772
- const ruleset = asRecord(value);
773
- return {
774
- id: ruleset["id"],
775
- name: ruleset["name"],
776
- kind: ruleset["kind"],
777
- phase: ruleset["phase"],
778
- ...(ruleset["description"] !== undefined
779
- ? { description: ruleset["description"] }
780
- : {}),
781
- ...(ruleset["version"] !== undefined ? { version: ruleset["version"] } : {}),
782
- ...(ruleset["last_updated"] !== undefined
783
- ? { lastUpdated: ruleset["last_updated"] }
784
- : {}),
785
- ...(ruleset["rules"] !== undefined ? { rules: ruleset["rules"] } : {}),
786
- };
787
- }
788
-
789
- function projectPagesProject(value: unknown): JsonRecord {
790
- const project = asRecord(value);
791
- const latest = asRecord(project["latest_deployment"]);
792
- return {
793
- name: project["name"],
794
- subdomain: project["subdomain"],
795
- ...(Array.isArray(project["domains"])
796
- ? { domains: project["domains"] }
797
- : {}),
798
- ...(project["production_branch"] !== undefined
799
- ? { productionBranch: project["production_branch"] }
800
- : {}),
801
- createdOn: project["created_on"],
802
- ...(latest["id"] !== undefined
803
- ? {
804
- latestDeployment: {
805
- id: latest["id"],
806
- environment: latest["environment"],
807
- url: latest["url"],
808
- createdOn: latest["created_on"],
809
- },
810
- }
811
- : {}),
812
- };
813
- }
814
-
815
- // --- Schema fragments --------------------------------------------------------
816
-
817
- const PAGE_OUTPUT_SCHEMA: JsonSchema = {
818
- type: "object",
819
- description:
820
- "Pagination counters from Cloudflare's result_info. Absent when the endpoint does not paginate.",
821
- properties: {
822
- page: { type: "integer" },
823
- perPage: { type: "integer" },
824
- count: { type: "integer", description: "Items on this page." },
825
- totalCount: { type: "integer" },
826
- totalPages: { type: "integer" },
827
- hasMore: {
828
- type: "boolean",
829
- description: "True when a further page exists; request page + 1.",
830
- },
831
- },
832
- required: ["page", "perPage", "count", "hasMore"],
833
- };
834
-
835
- const RAW_INPUT_PROPERTY: JsonSchema = {
836
- type: "boolean",
837
- description:
838
- "Return Cloudflare's unprojected result instead of the lean shape. Use only when a field the projection drops is genuinely needed; the raw shape is much larger.",
839
- };
840
-
841
- /**
842
- * Cloudflare's per-page bounds and default differ per endpoint and it rejects
843
- * an out-of-range value with a 400, so each caller passes its own. Encoding
844
- * them in the schema turns a wasted round trip into a local repair — but only
845
- * where the bound is really Cloudflare's. `bounds` records who chose the
846
- * range, because `strictValidation` refuses an out-of-range `perPage` locally
847
- * and an agent deserves to know whether the wall it hit is the API's or ours:
848
- *
849
- * - `"cloudflare"` — the schema's own documented minimum and maximum.
850
- * - `"clamped"` — Cloudflare accepts more; this connection caps it lower.
851
- * - `"undocumented"` — Cloudflare documents no bounds at all for the endpoint,
852
- * so the range is entirely this connection's choice.
853
- */
854
- function pagingInputProperties(
855
- minPerPage: number,
856
- maxPerPage: number,
857
- options: {
858
- defaultPerPage?: number;
859
- bounds?: "cloudflare" | "clamped" | "undocumented";
860
- } = {},
861
- ): Record<string, JsonSchema> {
862
- const { defaultPerPage, bounds = "cloudflare" } = options;
863
- const defaultNote =
864
- defaultPerPage === undefined
865
- ? " Cloudflare chooses the default."
866
- : ` Defaults to ${defaultPerPage}.`;
867
- const boundsNote =
868
- bounds === "clamped"
869
- ? ` The ${maxPerPage} ceiling is this connection's cap, not Cloudflare's limit.`
870
- : bounds === "undocumented"
871
- ? " Cloudflare documents no bounds for this endpoint; the range is this connection's own."
872
- : "";
873
- return {
874
- page: {
875
- type: "integer",
876
- minimum: 1,
877
- description: "1-based page number. Defaults to 1.",
878
- },
879
- perPage: {
880
- type: "integer",
881
- minimum: minPerPage,
882
- maximum: maxPerPage,
883
- description: `Items per page, ${minPerPage} to ${maxPerPage}.${defaultNote}${boundsNote}`,
884
- },
885
- };
886
- }
887
-
888
- function listOutputSchema(key: string, item: JsonSchema): JsonSchema {
889
- return {
890
- type: "object",
891
- properties: { [key]: { type: "array", items: item }, page: PAGE_OUTPUT_SCHEMA },
892
- required: [key],
893
- };
894
- }
895
-
896
- const ACCOUNT_SCHEMA: JsonSchema = {
897
- type: "object",
898
- properties: {
899
- id: { type: "string" },
900
- name: { type: "string" },
901
- type: { type: "string" },
902
- createdOn: { type: "string" },
903
- },
904
- required: ["id", "name"],
905
- };
906
-
907
- const ZONE_SCHEMA: JsonSchema = {
908
- type: "object",
909
- properties: {
910
- id: { type: "string", description: "Zone id — the argument every zone-scoped tool wants." },
911
- name: { type: "string", description: "Apex domain, e.g. example.com." },
912
- status: { type: "string" },
913
- paused: { type: "boolean" },
914
- type: { type: "string" },
915
- accountId: { type: "string" },
916
- accountName: { type: "string" },
917
- plan: { type: "string" },
918
- nameServers: { type: "array", items: { type: "string" } },
919
- createdOn: { type: "string" },
920
- modifiedOn: { type: "string" },
921
- },
922
- required: ["id", "name", "status"],
923
- };
924
-
925
- const DNS_RECORD_SCHEMA: JsonSchema = {
926
- type: "object",
927
- properties: {
928
- id: { type: "string" },
929
- name: { type: "string", description: "Fully qualified record name." },
930
- type: { type: "string", enum: [...CLOUDFLARE_DNS_RECORD_TYPES] },
931
- content: { type: "string" },
932
- ttl: { type: "integer", description: "Seconds; 1 means automatic." },
933
- proxied: { type: "boolean" },
934
- priority: { type: "integer" },
935
- comment: { type: "string" },
936
- tags: { type: "array", items: { type: "string" } },
937
- createdOn: { type: "string" },
938
- modifiedOn: { type: "string" },
939
- },
940
- required: ["id", "name", "type", "content", "ttl"],
941
- };
942
-
943
- // --- Tool construction -------------------------------------------------------
944
-
945
- interface Scoping {
946
- base: string;
947
- accountId: string | undefined;
948
- zoneId: string | undefined;
949
- }
950
-
951
- /**
952
- * Resolve a scope id from the call or the deployment default.
953
- *
954
- * The second layer, not the first: when the deployment declares no default the
955
- * schema already lists the key in `required`, so `api()` rejects an omitted id
956
- * before the handler runs. This catches what a JSON Schema string cannot — a
957
- * blank or whitespace-only id — and answers with the discovery tool's name
958
- * rather than a Cloudflare round trip that would 404.
959
- */
960
- function requireScope(
961
- provided: unknown,
962
- fallback: string | undefined,
963
- kind: "zoneId" | "accountId",
964
- ): string {
965
- const value = typeof provided === "string" ? provided.trim() : "";
966
- if (value) return value;
967
- if (fallback) return fallback;
968
- const discovery = kind === "zoneId" ? "list_zones" : "list_accounts";
969
- throw new ConnectorCallError(
970
- "invalid_args",
971
- `${kind} is required: this connector has no default ${kind}. Call ${discovery} to find it.`,
972
- {
973
- validation: {
974
- issues: [
975
- {
976
- path: `/${kind}`,
977
- code: "required",
978
- expected: `a Cloudflare ${kind === "zoneId" ? "zone" : "account"} id`,
979
- },
980
- ],
981
- },
982
- },
983
- );
984
- }
985
-
986
- /** A scope argument is only required when the deployment declared no default. */
987
- function scopeProperty(
988
- kind: "zoneId" | "accountId",
989
- fallback: string | undefined,
990
- ): JsonSchema {
991
- const noun = kind === "zoneId" ? "Zone" : "Account";
992
- const discovery = kind === "zoneId" ? "list_zones" : "list_accounts";
993
- return {
994
- type: "string",
995
- minLength: 1,
996
- description: fallback
997
- ? `${noun} id. Optional — defaults to this connector's configured ${kind}. Pass one to address a different ${noun.toLowerCase()}; ${discovery} lists them.`
998
- : `${noun} id. Required — this connector declares no default; ${discovery} returns it.`,
999
- };
1000
- }
1001
-
1002
- function scopeRequired(
1003
- kind: "zoneId" | "accountId",
1004
- fallback: string | undefined,
1005
- ): string[] {
1006
- return fallback ? [] : [kind];
1007
- }
1008
-
1009
- function optionalString(args: JsonRecord, key: string): string | undefined {
1010
- const value = args[key];
1011
- return typeof value === "string" && value.trim() !== ""
1012
- ? value.trim()
1013
- : undefined;
1014
- }
1015
-
1016
- function optionalNumber(args: JsonRecord, key: string): number | undefined {
1017
- const value = args[key];
1018
- return typeof value === "number" ? value : undefined;
1019
- }
1020
-
1021
- function optionalBoolean(args: JsonRecord, key: string): boolean | undefined {
1022
- const value = args[key];
1023
- return typeof value === "boolean" ? value : undefined;
1024
- }
1025
-
1026
- function requireString(args: JsonRecord, key: string): string {
1027
- const value = optionalString(args, key);
1028
- if (value) return value;
1029
- throw new ConnectorCallError("invalid_args", `${key} must not be blank.`);
1030
- }
1031
-
1032
- function encodePathSegment(value: string): string {
1033
- return encodeURIComponent(value);
1034
- }
1035
-
1036
- function encodeObjectKey(value: string): string {
1037
- return value
1038
- .split("/")
1039
- .map((segment) => {
1040
- if (segment === "." || segment === "..") {
1041
- throw new ConnectorCallError(
1042
- "invalid_args",
1043
- "objectKey cannot contain '.' or '..' path segments because URL normalization would change the target resource.",
1044
- );
1045
- }
1046
- return encodeURIComponent(segment);
1047
- })
1048
- .join("/");
1049
- }
1050
-
1051
- function cloudflareApiPath(value: unknown): string {
1052
- if (typeof value !== "string") {
1053
- throw new ConnectorCallError("invalid_args", "path must be a string.");
1054
- }
1055
- const path = value.trim();
1056
- if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\")) {
1057
- throw new ConnectorCallError(
1058
- "invalid_args",
1059
- "path must be a relative Cloudflare v4 path beginning with one slash and containing no backslashes.",
1060
- );
1061
- }
1062
- if (path.includes("?") || path.includes("#")) {
1063
- throw new ConnectorCallError(
1064
- "invalid_args",
1065
- "Put query parameters in the query array; path cannot contain '?' or '#'.",
1066
- );
1067
- }
1068
- for (const segment of path.split("/")) {
1069
- let decoded = segment;
1070
- let stable = false;
1071
- for (let pass = 0; pass < 20; pass += 1) {
1072
- let next: string;
1073
- try {
1074
- next = decodeURIComponent(decoded);
1075
- } catch {
1076
- throw new ConnectorCallError(
1077
- "invalid_args",
1078
- "path contains invalid percent encoding.",
1079
- );
1080
- }
1081
- if (next === decoded) {
1082
- stable = true;
1083
- break;
1084
- }
1085
- decoded = next;
1086
- }
1087
- if (!stable) {
1088
- throw new ConnectorCallError(
1089
- "invalid_args",
1090
- "path contains too many layers of percent encoding.",
1091
- );
1092
- }
1093
- if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
1094
- throw new ConnectorCallError(
1095
- "invalid_args",
1096
- "path cannot contain encoded or literal traversal, slash, or backslash segments.",
1097
- );
1098
- }
1099
- }
1100
- const normalized = new URL(`https://connecta.invalid/client/v4${path}`);
1101
- if (!normalized.pathname.startsWith("/client/v4/")) {
1102
- throw new ConnectorCallError(
1103
- "invalid_args",
1104
- "path normalization escaped the Cloudflare v4 API base.",
1105
- );
1106
- }
1107
- return path;
1108
- }
1109
-
1110
- function queryFromArgs(
1111
- value: unknown,
1112
- ): Record<string, string | number | boolean | undefined> | undefined {
1113
- if (!Array.isArray(value) || value.length === 0) return undefined;
1114
- const query: Record<string, string> = {};
1115
- for (const item of value) {
1116
- const entry = asRecord(item);
1117
- query[String(entry["name"])] = String(entry["value"]);
1118
- }
1119
- return query;
1120
- }
1121
-
1122
- function headersFromArgs(value: unknown): Record<string, string> | undefined {
1123
- if (!Array.isArray(value) || value.length === 0) return undefined;
1124
- const headers: Record<string, string> = {};
1125
- const forbidden = new Set([
1126
- "authorization",
1127
- "x-auth-email",
1128
- "x-auth-key",
1129
- "cookie",
1130
- "host",
1131
- "content-length",
1132
- "content-type",
1133
- "transfer-encoding",
1134
- ]);
1135
- for (const item of value) {
1136
- const entry = asRecord(item);
1137
- const name = String(entry["name"]).trim();
1138
- if (forbidden.has(name.toLowerCase())) {
1139
- throw new ConnectorCallError(
1140
- "invalid_args",
1141
- `The raw Cloudflare tools do not allow the ${name} header. Authentication and request framing are connector-owned; use contentType for a raw upload body.`,
1142
- );
1143
- }
1144
- headers[name] = String(entry["value"]);
1145
- }
1146
- return headers;
1147
- }
1148
-
1149
- function r2Headers(args: JsonRecord): Record<string, string | undefined> {
1150
- return { "cf-r2-jurisdiction": optionalString(args, "jurisdiction") };
1151
- }
1152
-
1153
- function bytesFromBase64(value: string): Uint8Array<ArrayBuffer> {
1154
- try {
1155
- const binary = atob(value);
1156
- return Uint8Array.from(binary, (character) => character.charCodeAt(0));
1157
- } catch (cause) {
1158
- throw new ConnectorCallError(
1159
- "invalid_args",
1160
- "base64Body and multipart file base64 values must be valid base64.",
1161
- { cause },
1162
- );
1163
- }
1164
- }
1165
-
1166
- function uploadBody(args: JsonRecord): {
1167
- rawBody: BodyInit;
1168
- headers?: Record<string, string | undefined>;
1169
- } {
1170
- const fields = asArray(args["fields"]);
1171
- const files = asArray(args["files"]);
1172
- const hasMultipart = fields.length > 0 || files.length > 0;
1173
- const textBody = typeof args["textBody"] === "string" ? args["textBody"] : undefined;
1174
- const base64Body =
1175
- typeof args["base64Body"] === "string" ? args["base64Body"] : undefined;
1176
- const rawCount = Number(textBody !== undefined) + Number(base64Body !== undefined);
1177
- if ((hasMultipart && rawCount > 0) || (!hasMultipart && rawCount !== 1)) {
1178
- throw new ConnectorCallError(
1179
- "invalid_args",
1180
- "cloudflare_api_upload needs exactly one body shape: textBody, base64Body, or multipart fields/files.",
1181
- );
1182
- }
1183
- if (hasMultipart) {
1184
- const form = new FormData();
1185
- for (const value of fields) {
1186
- const field = asRecord(value);
1187
- const name = String(field["name"]);
1188
- const contentType = optionalString(field, "contentType");
1189
- if (contentType) {
1190
- form.append(
1191
- name,
1192
- new Blob([String(field["value"])], { type: contentType }),
1193
- optionalString(field, "fileName") ?? name,
1194
- );
1195
- } else {
1196
- form.append(name, String(field["value"]));
1197
- }
1198
- }
1199
- for (const value of files) {
1200
- const file = asRecord(value);
1201
- const text = typeof file["text"] === "string" ? file["text"] : undefined;
1202
- const base64 =
1203
- typeof file["base64"] === "string" ? file["base64"] : undefined;
1204
- if (Number(text !== undefined) + Number(base64 !== undefined) !== 1) {
1205
- throw new ConnectorCallError(
1206
- "invalid_args",
1207
- "Each multipart file needs exactly one of text or base64.",
1208
- );
1209
- }
1210
- const blob = new Blob(
1211
- [text ?? bytesFromBase64(base64!)],
1212
- { type: String(file["contentType"] ?? "application/octet-stream") },
1213
- );
1214
- form.append(String(file["name"]), blob, String(file["fileName"]));
1215
- }
1216
- return { rawBody: form };
1217
- }
1218
- return {
1219
- rawBody: textBody ?? bytesFromBase64(base64Body!),
1220
- headers: {
1221
- "Content-Type":
1222
- optionalString(args, "contentType") ??
1223
- (textBody !== undefined ? "text/plain; charset=utf-8" : "application/octet-stream"),
1224
- },
1225
- };
1226
- }
1227
-
1228
- const OPEN_OBJECT_OUTPUT_SCHEMA: JsonSchema = {
1229
- type: "object",
1230
- description: "Cloudflare's result object. Its fields depend on the endpoint.",
1231
- additionalProperties: true,
1232
- };
1233
-
1234
- const QUERY_INPUT_PROPERTY: JsonSchema = {
1235
- type: "array",
1236
- description:
1237
- "Optional query parameters as name/value pairs. Each parameter name may appear once.",
1238
- items: {
1239
- type: "object",
1240
- properties: {
1241
- name: { type: "string", minLength: 1 },
1242
- value: { type: ["string", "number", "boolean"] },
1243
- },
1244
- required: ["name", "value"],
1245
- additionalProperties: false,
1246
- },
1247
- };
1248
-
1249
- const HEADERS_INPUT_PROPERTY: JsonSchema = {
1250
- type: "array",
1251
- description:
1252
- "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.",
1253
- items: {
1254
- type: "object",
1255
- properties: {
1256
- name: { type: "string", minLength: 1 },
1257
- value: { type: "string" },
1258
- },
1259
- required: ["name", "value"],
1260
- additionalProperties: false,
1261
- },
1262
- };
1263
-
1264
- const R2_JURISDICTION_PROPERTY: JsonSchema = {
1265
- type: "string",
1266
- enum: ["default", "eu", "fedramp"],
1267
- description:
1268
- "Bucket jurisdiction. Omit for ordinary buckets; set eu or fedramp for jurisdictional buckets.",
1269
- };
1270
-
1271
- const R2_BUCKET_NAME_PROPERTY: JsonSchema = {
1272
- type: "string",
1273
- minLength: 3,
1274
- maxLength: 64,
1275
- description: "R2 bucket name.",
1276
- };
1277
-
1278
- const R2_BUCKET_SCHEMA: JsonSchema = {
1279
- type: "object",
1280
- properties: {
1281
- name: { type: "string" },
1282
- location: { type: "string" },
1283
- storageClass: { type: "string" },
1284
- jurisdiction: { type: "string" },
1285
- creationDate: { type: "string" },
1286
- },
1287
- required: ["name"],
1288
- };
1289
-
1290
- const R2_OBJECT_SCHEMA: JsonSchema = {
1291
- type: "object",
1292
- properties: {
1293
- key: { type: "string" },
1294
- size: { type: "number" },
1295
- etag: { type: "string" },
1296
- lastModified: { type: "string" },
1297
- storageClass: { type: "string" },
1298
- httpMetadata: { type: "object" },
1299
- customMetadata: { type: "object" },
1300
- },
1301
- required: ["key"],
1302
- };
1303
-
1304
- function buildTools(
1305
- scope: Scoping,
1306
- authentication: CloudflareAuthentication,
1307
- ): ApiTool[] {
1308
- const { base } = scope;
1309
- const zoneArg = (args: JsonRecord): string =>
1310
- requireScope(args["zoneId"], scope.zoneId, "zoneId");
1311
- const accountArg = (args: JsonRecord): string =>
1312
- requireScope(args["accountId"], scope.accountId, "accountId");
1313
-
1314
- const readOnly = { readOnlyHint: true, destructiveHint: false } as const;
1315
-
1316
- const tools: ApiTool[] = [
1317
- authentication === "apiToken"
1318
- ? {
1319
- name: "verify_api_token",
1320
- description:
1321
- "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.",
1322
- annotations: readOnly,
1323
- inputSchema: {
1324
- type: "object",
1325
- properties: {},
1326
- required: [],
1327
- additionalProperties: false,
1328
- },
1329
- outputSchema: {
1330
- type: "object",
1331
- properties: {
1332
- id: { type: "string" },
1333
- status: {
1334
- type: "string",
1335
- description: "\"active\" for a usable token.",
1336
- },
1337
- notBefore: { type: "string" },
1338
- expiresOn: { type: "string" },
1339
- },
1340
- required: ["status"],
1341
- },
1342
- handler: async (_args, ctx) => {
1343
- const { result } = await callCloudflare(
1344
- base,
1345
- { method: "GET", path: "/user/tokens/verify" },
1346
- ctx,
1347
- );
1348
- const token = asRecord(result);
1349
- return {
1350
- id: token["id"],
1351
- status: token["status"],
1352
- ...(token["not_before"] !== undefined
1353
- ? { notBefore: token["not_before"] }
1354
- : {}),
1355
- ...(token["expires_on"] !== undefined
1356
- ? { expiresOn: token["expires_on"] }
1357
- : {}),
1358
- };
1359
- },
1360
- }
1361
- : {
1362
- name: "verify_global_api_key",
1363
- description:
1364
- "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.",
1365
- annotations: readOnly,
1366
- inputSchema: {
1367
- type: "object",
1368
- properties: {},
1369
- required: [],
1370
- additionalProperties: false,
1371
- },
1372
- outputSchema: {
1373
- type: "object",
1374
- properties: {
1375
- id: { type: "string" },
1376
- email: { type: "string" },
1377
- status: {
1378
- type: "string",
1379
- description: "\"active\" when Cloudflare accepts the email and key.",
1380
- },
1381
- },
1382
- required: ["email", "status"],
1383
- },
1384
- handler: async (_args, ctx) => {
1385
- const { result } = await callCloudflare(
1386
- base,
1387
- { method: "GET", path: "/user" },
1388
- ctx,
1389
- );
1390
- const user = asRecord(result);
1391
- return { id: user["id"], email: user["email"], status: "active" };
1392
- },
1393
- },
1394
- {
1395
- name: "cloudflare_api_get",
1396
- description:
1397
- "Call any GET endpoint under Cloudflare's v4 API with this connector's credential. 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.",
1398
- annotations: readOnly,
1399
- inputSchema: {
1400
- type: "object",
1401
- properties: {
1402
- path: {
1403
- type: "string",
1404
- minLength: 1,
1405
- description:
1406
- "Relative path below /client/v4, beginning with '/', for example /accounts/<id>/images/v1 or /zones/<id>/email/routing/rules. Do not include a query string.",
1407
- },
1408
- query: QUERY_INPUT_PROPERTY,
1409
- headers: HEADERS_INPUT_PROPERTY,
1410
- responseType: {
1411
- type: "string",
1412
- enum: ["json", "text", "base64"],
1413
- description:
1414
- "How to read a successful response. Defaults to json; use text or base64 for object, log, script, and media downloads.",
1415
- },
1416
- },
1417
- required: ["path"],
1418
- additionalProperties: false,
1419
- },
1420
- outputSchema: {
1421
- type: "object",
1422
- properties: {
1423
- result: {
1424
- description: "Cloudflare's unprojected result for the endpoint.",
1425
- },
1426
- resultInfo: {
1427
- type: "object",
1428
- description:
1429
- "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1430
- },
1431
- text: { type: "string", description: "Text response body when responseType is text." },
1432
- base64: { type: "string", description: "Base64 response bytes when responseType is base64." },
1433
- contentType: { type: "string", description: "Response Content-Type for text/base64 reads." },
1434
- etag: { type: "string", description: "Response ETag when Cloudflare supplies one." },
1435
- },
1436
- required: [],
1437
- },
1438
- handler: async (args: JsonRecord, ctx) => {
1439
- const query = queryFromArgs(args["query"]);
1440
- const headers = headersFromArgs(args["headers"]);
1441
- const responseType = optionalString(args, "responseType") ?? "json";
1442
- const spec: RequestSpec = {
1443
- method: "GET",
1444
- path: cloudflareApiPath(args["path"]),
1445
- ...(query !== undefined ? { query } : {}),
1446
- ...(headers !== undefined ? { headers } : {}),
1447
- };
1448
- if (responseType === "text" || responseType === "base64") {
1449
- return await callCloudflareContent(base, spec, ctx, responseType);
1450
- }
1451
- const { result, resultInfo } = await callCloudflare(
1452
- base,
1453
- spec,
1454
- ctx,
1455
- );
1456
- return {
1457
- result,
1458
- ...(resultInfo !== undefined ? { resultInfo } : {}),
1459
- };
1460
- },
1461
- },
1462
- {
1463
- name: "cloudflare_api_mutate",
1464
- description:
1465
- "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's credential. 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.",
1466
- annotations: { readOnlyHint: false, destructiveHint: true },
1467
- inputSchema: {
1468
- type: "object",
1469
- properties: {
1470
- method: {
1471
- type: "string",
1472
- enum: ["POST", "PUT", "PATCH", "DELETE"],
1473
- description: "HTTP mutation method required by the Cloudflare endpoint.",
1474
- },
1475
- path: {
1476
- type: "string",
1477
- minLength: 1,
1478
- description:
1479
- "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
1480
- },
1481
- query: QUERY_INPUT_PROPERTY,
1482
- headers: HEADERS_INPUT_PROPERTY,
1483
- body: {
1484
- type: ["object", "array", "string", "number", "boolean", "null"],
1485
- description:
1486
- "JSON request body exactly as documented by Cloudflare. Omit for endpoints with no body.",
1487
- },
1488
- },
1489
- required: ["method", "path"],
1490
- additionalProperties: false,
1491
- },
1492
- outputSchema: {
1493
- type: "object",
1494
- properties: {
1495
- result: {
1496
- description: "Cloudflare's unprojected result for the endpoint.",
1497
- },
1498
- resultInfo: {
1499
- type: "object",
1500
- description:
1501
- "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1502
- },
1503
- },
1504
- required: ["result"],
1505
- },
1506
- handler: async (args: JsonRecord, ctx) => {
1507
- const method = String(args["method"]) as RequestSpec["method"];
1508
- const query = queryFromArgs(args["query"]);
1509
- const headers = headersFromArgs(args["headers"]);
1510
- const { result, resultInfo } = await callCloudflare(
1511
- base,
1512
- {
1513
- method,
1514
- path: cloudflareApiPath(args["path"]),
1515
- ...(query !== undefined ? { query } : {}),
1516
- ...(headers !== undefined ? { headers } : {}),
1517
- ...(args["body"] !== undefined ? { body: args["body"] } : {}),
1518
- },
1519
- ctx,
1520
- );
1521
- return {
1522
- result,
1523
- ...(resultInfo !== undefined ? { resultInfo } : {}),
1524
- };
1525
- },
1526
- },
1527
- {
1528
- name: "cloudflare_api_upload",
1529
- description:
1530
- "Upload raw text, base64 bytes, or multipart form data to a Cloudflare v4 POST or PUT endpoint. Covers Worker modules, R2/KV objects, Images, Stream, and Pages upload endpoints. Reads no local files; content must be supplied explicitly.",
1531
- annotations: { readOnlyHint: false, destructiveHint: true },
1532
- inputSchema: {
1533
- type: "object",
1534
- properties: {
1535
- method: {
1536
- type: "string",
1537
- enum: ["POST", "PUT"],
1538
- description: "HTTP upload method required by the Cloudflare endpoint.",
1539
- },
1540
- path: {
1541
- type: "string",
1542
- minLength: 1,
1543
- description:
1544
- "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
1545
- },
1546
- query: QUERY_INPUT_PROPERTY,
1547
- headers: HEADERS_INPUT_PROPERTY,
1548
- contentType: {
1549
- type: "string",
1550
- minLength: 1,
1551
- description:
1552
- "Content-Type for a raw text/base64 body. Omit for multipart because fetch supplies the boundary.",
1553
- },
1554
- textBody: {
1555
- type: "string",
1556
- description: "Raw UTF-8 request body. Mutually exclusive with base64Body and multipart fields/files.",
1557
- },
1558
- base64Body: {
1559
- type: "string",
1560
- description: "Base64-encoded request bytes. Mutually exclusive with textBody and multipart fields/files.",
1561
- },
1562
- fields: {
1563
- type: "array",
1564
- description: "String fields for a multipart/form-data request.",
1565
- items: {
1566
- type: "object",
1567
- properties: {
1568
- name: { type: "string", minLength: 1 },
1569
- value: { type: "string" },
1570
- contentType: { type: "string", minLength: 1 },
1571
- fileName: { type: "string", minLength: 1 },
1572
- },
1573
- required: ["name", "value"],
1574
- additionalProperties: false,
1575
- },
1576
- },
1577
- files: {
1578
- type: "array",
1579
- description:
1580
- "File parts for multipart/form-data. Each file needs exactly one of text or base64.",
1581
- items: {
1582
- type: "object",
1583
- properties: {
1584
- name: { type: "string", minLength: 1 },
1585
- fileName: { type: "string", minLength: 1 },
1586
- contentType: { type: "string", minLength: 1 },
1587
- text: { type: "string" },
1588
- base64: { type: "string" },
1589
- },
1590
- required: ["name", "fileName", "contentType"],
1591
- additionalProperties: false,
1592
- },
1593
- },
1594
- },
1595
- required: ["method", "path"],
1596
- additionalProperties: false,
1597
- },
1598
- outputSchema: {
1599
- type: "object",
1600
- properties: {
1601
- result: {
1602
- description: "Cloudflare's unprojected upload result.",
1603
- },
1604
- },
1605
- required: ["result"],
1606
- },
1607
- handler: async (args: JsonRecord, ctx) => {
1608
- const query = queryFromArgs(args["query"]);
1609
- const headers = headersFromArgs(args["headers"]);
1610
- const upload = uploadBody(args);
1611
- const { result } = await callCloudflare(
1612
- base,
1613
- {
1614
- method: String(args["method"]) as "POST" | "PUT",
1615
- path: cloudflareApiPath(args["path"]),
1616
- ...(query !== undefined ? { query } : {}),
1617
- ...(headers !== undefined || upload.headers !== undefined
1618
- ? { headers: { ...headers, ...upload.headers } }
1619
- : {}),
1620
- rawBody: upload.rawBody,
1621
- },
1622
- ctx,
1623
- );
1624
- return { result };
1625
- },
1626
- },
1627
- {
1628
- name: "list_accounts",
1629
- description:
1630
- "List Cloudflare accounts this token can see. Supplies the accountId that the Workers, KV, R2, and Pages tools need.",
1631
- annotations: readOnly,
1632
- inputSchema: {
1633
- type: "object",
1634
- properties: {
1635
- name: {
1636
- type: "string",
1637
- description: "Filter by exact account name.",
1638
- },
1639
- ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1640
- raw: RAW_INPUT_PROPERTY,
1641
- },
1642
- required: [],
1643
- additionalProperties: false,
1644
- },
1645
- outputSchema: listOutputSchema("accounts", ACCOUNT_SCHEMA),
1646
- handler: async (args: JsonRecord, ctx) => {
1647
- const { result, resultInfo } = await callCloudflare(
1648
- base,
1649
- {
1650
- method: "GET",
1651
- path: "/accounts",
1652
- query: {
1653
- name: optionalString(args, "name"),
1654
- page: optionalNumber(args, "page"),
1655
- per_page: optionalNumber(args, "perPage"),
1656
- },
1657
- },
1658
- ctx,
1659
- );
1660
- if (args["raw"] === true) return { accounts: result, page: pageInfo(resultInfo) };
1661
- return {
1662
- accounts: asArray(result).map(projectAccount),
1663
- page: pageInfo(resultInfo),
1664
- };
1665
- },
1666
- },
1667
- {
1668
- name: "list_zones",
1669
- description:
1670
- "List zones (domains) this token can see, with their ids and status. This is the zoneId discovery step for every DNS and cache tool.",
1671
- annotations: readOnly,
1672
- inputSchema: {
1673
- type: "object",
1674
- properties: {
1675
- name: {
1676
- type: "string",
1677
- description: "Filter by zone name, e.g. example.com.",
1678
- },
1679
- accountId: {
1680
- type: "string",
1681
- description:
1682
- "Restrict to one account. Defaults to every account the token can see.",
1683
- },
1684
- status: {
1685
- type: "string",
1686
- enum: ["initializing", "pending", "active", "moved"],
1687
- description: "Filter by zone status.",
1688
- },
1689
- ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1690
- raw: RAW_INPUT_PROPERTY,
1691
- },
1692
- required: [],
1693
- additionalProperties: false,
1694
- },
1695
- outputSchema: listOutputSchema("zones", ZONE_SCHEMA),
1696
- handler: async (args: JsonRecord, ctx) => {
1697
- const { result, resultInfo } = await callCloudflare(
1698
- base,
1699
- {
1700
- method: "GET",
1701
- path: "/zones",
1702
- query: {
1703
- name: optionalString(args, "name"),
1704
- // Deliberately not defaulted to `scope.accountId`. This is the
1705
- // discovery tool: a deployment default that silently narrowed
1706
- // what an agent can see would contradict the property's own
1707
- // description, and there would be no argument that escapes it.
1708
- "account.id": optionalString(args, "accountId"),
1709
- status: optionalString(args, "status"),
1710
- page: optionalNumber(args, "page"),
1711
- per_page: optionalNumber(args, "perPage"),
1712
- },
1713
- },
1714
- ctx,
1715
- );
1716
- if (args["raw"] === true) return { zones: result, page: pageInfo(resultInfo) };
1717
- return {
1718
- zones: asArray(result).map(projectZone),
1719
- page: pageInfo(resultInfo),
1720
- };
1721
- },
1722
- },
1723
- {
1724
- name: "get_zone",
1725
- description:
1726
- "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.",
1727
- annotations: readOnly,
1728
- inputSchema: {
1729
- type: "object",
1730
- properties: {
1731
- zoneId: scopeProperty("zoneId", scope.zoneId),
1732
- raw: RAW_INPUT_PROPERTY,
1733
- },
1734
- required: scopeRequired("zoneId", scope.zoneId),
1735
- additionalProperties: false,
1736
- },
1737
- outputSchema: ZONE_SCHEMA,
1738
- handler: async (args: JsonRecord, ctx) => {
1739
- const { result } = await callCloudflare(
1740
- base,
1741
- { method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` },
1742
- ctx,
1743
- );
1744
- return args["raw"] === true ? result : projectZone(result);
1745
- },
1746
- },
1747
- {
1748
- name: "list_zone_settings",
1749
- description:
1750
- "List the effective settings for a zone, including each setting's current value and whether the plan allows editing it.",
1751
- annotations: readOnly,
1752
- inputSchema: {
1753
- type: "object",
1754
- properties: {
1755
- zoneId: scopeProperty("zoneId", scope.zoneId),
1756
- },
1757
- required: scopeRequired("zoneId", scope.zoneId),
1758
- additionalProperties: false,
1759
- },
1760
- outputSchema: listOutputSchema("settings", OPEN_OBJECT_OUTPUT_SCHEMA),
1761
- handler: async (args: JsonRecord, ctx) => {
1762
- const { result, resultInfo } = await callCloudflare(
1763
- base,
1764
- {
1765
- method: "GET",
1766
- path: `/zones/${encodePathSegment(zoneArg(args))}/settings`,
1767
- },
1768
- ctx,
1769
- );
1770
- return { settings: asArray(result), page: pageInfo(resultInfo) };
1771
- },
1772
- },
1773
- {
1774
- name: "get_zone_setting",
1775
- description:
1776
- "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.",
1777
- annotations: readOnly,
1778
- inputSchema: {
1779
- type: "object",
1780
- properties: {
1781
- zoneId: scopeProperty("zoneId", scope.zoneId),
1782
- settingId: {
1783
- type: "string",
1784
- minLength: 1,
1785
- description: "Cloudflare zone setting id from list_zone_settings.",
1786
- },
1787
- },
1788
- required: [...scopeRequired("zoneId", scope.zoneId), "settingId"],
1789
- additionalProperties: false,
1790
- },
1791
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1792
- handler: async (args: JsonRecord, ctx) => {
1793
- const { result } = await callCloudflare(
1794
- base,
1795
- {
1796
- method: "GET",
1797
- path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1798
- },
1799
- ctx,
1800
- );
1801
- return result;
1802
- },
1803
- },
1804
- {
1805
- name: "update_zone_setting",
1806
- description:
1807
- "Set one editable zone setting. Read it first: allowed value types and plan restrictions differ by setting.",
1808
- annotations: { readOnlyHint: false, destructiveHint: true },
1809
- inputSchema: {
1810
- type: "object",
1811
- properties: {
1812
- zoneId: scopeProperty("zoneId", scope.zoneId),
1813
- settingId: {
1814
- type: "string",
1815
- minLength: 1,
1816
- description: "Cloudflare zone setting id from list_zone_settings.",
1817
- },
1818
- value: {
1819
- type: ["string", "number", "boolean", "array"],
1820
- description:
1821
- "New setting value in the type returned by get_zone_setting. Arrays must contain strings.",
1822
- items: { type: "string" },
1823
- },
1824
- },
1825
- required: [...scopeRequired("zoneId", scope.zoneId), "settingId", "value"],
1826
- additionalProperties: false,
1827
- },
1828
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1829
- handler: async (args: JsonRecord, ctx) => {
1830
- const { result } = await callCloudflare(
1831
- base,
1832
- {
1833
- method: "PATCH",
1834
- path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1835
- body: { value: args["value"] },
1836
- },
1837
- ctx,
1838
- );
1839
- return result;
1840
- },
1841
- },
1842
- {
1843
- name: "list_zone_rulesets",
1844
- description:
1845
- "List zone rulesets for WAF, redirects, transforms, cache rules, configuration rules, and other Ruleset Engine phases.",
1846
- annotations: readOnly,
1847
- inputSchema: {
1848
- type: "object",
1849
- properties: {
1850
- zoneId: scopeProperty("zoneId", scope.zoneId),
1851
- perPage: {
1852
- type: "integer",
1853
- minimum: 1,
1854
- maximum: 50,
1855
- description: "Rulesets per request, 1 to 50.",
1856
- },
1857
- cursor: {
1858
- type: "string",
1859
- description: "Opaque cursor returned as nextCursor by the previous call.",
1860
- },
1861
- },
1862
- required: scopeRequired("zoneId", scope.zoneId),
1863
- additionalProperties: false,
1864
- },
1865
- outputSchema: {
1866
- type: "object",
1867
- properties: {
1868
- rulesets: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1869
- nextCursor: { type: "string" },
1870
- },
1871
- required: ["rulesets"],
1872
- },
1873
- handler: async (args: JsonRecord, ctx) => {
1874
- const { result, resultInfo } = await callCloudflare(
1875
- base,
1876
- {
1877
- method: "GET",
1878
- path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets`,
1879
- query: {
1880
- per_page: optionalNumber(args, "perPage"),
1881
- cursor: optionalString(args, "cursor"),
1882
- },
1883
- },
1884
- ctx,
1885
- );
1886
- const cursor = resultInfo?.cursors?.after;
1887
- return {
1888
- rulesets: asArray(result).map(projectRuleset),
1889
- ...(typeof cursor === "string" && cursor !== ""
1890
- ? { nextCursor: cursor }
1891
- : {}),
1892
- };
1893
- },
1894
- },
1895
- {
1896
- name: "get_zone_ruleset",
1897
- description:
1898
- "Get one zone ruleset including its ordered rules, expressions, actions, parameters, and enabled state.",
1899
- annotations: readOnly,
1900
- inputSchema: {
1901
- type: "object",
1902
- properties: {
1903
- zoneId: scopeProperty("zoneId", scope.zoneId),
1904
- rulesetId: {
1905
- type: "string",
1906
- minLength: 1,
1907
- description: "Ruleset id from list_zone_rulesets.",
1908
- },
1909
- },
1910
- required: [...scopeRequired("zoneId", scope.zoneId), "rulesetId"],
1911
- additionalProperties: false,
1912
- },
1913
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1914
- handler: async (args: JsonRecord, ctx) => {
1915
- const { result } = await callCloudflare(
1916
- base,
1917
- {
1918
- method: "GET",
1919
- path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets/${encodePathSegment(requireString(args, "rulesetId"))}`,
1920
- },
1921
- ctx,
1922
- );
1923
- return projectRuleset(result);
1924
- },
1925
- },
1926
- {
1927
- name: "list_dns_records",
1928
- description:
1929
- "List DNS records in a zone, filtered by name, type, or content. Returns record ids, which update_dns_record and delete_dns_record require.",
1930
- annotations: readOnly,
1931
- inputSchema: {
1932
- type: "object",
1933
- properties: {
1934
- zoneId: scopeProperty("zoneId", scope.zoneId),
1935
- name: {
1936
- type: "string",
1937
- description:
1938
- "Exact record name, fully qualified, e.g. www.example.com.",
1939
- },
1940
- type: {
1941
- type: "string",
1942
- enum: [...CLOUDFLARE_DNS_RECORD_TYPES],
1943
- description: "Filter by record type.",
1944
- },
1945
- content: {
1946
- type: "string",
1947
- description: "Exact record content, e.g. an IP address.",
1948
- },
1949
- order: {
1950
- type: "string",
1951
- enum: ["type", "name", "content", "ttl", "proxied"],
1952
- description: "Sort field.",
1953
- },
1954
- direction: {
1955
- type: "string",
1956
- enum: ["asc", "desc"],
1957
- description: "Sort direction for `order`. Defaults to asc.",
1958
- },
1959
- // Cloudflare documents 1 to 5,000,000 here with a default of 100; the
1960
- // ceiling is nominal, so this connection caps it at a page size that
1961
- // actually returns.
1962
- ...pagingInputProperties(1, 1000, {
1963
- defaultPerPage: 100,
1964
- bounds: "clamped",
1965
- }),
1966
- raw: RAW_INPUT_PROPERTY,
1967
- },
1968
- required: scopeRequired("zoneId", scope.zoneId),
1969
- additionalProperties: false,
1970
- },
1971
- outputSchema: listOutputSchema("records", DNS_RECORD_SCHEMA),
1972
- handler: async (args: JsonRecord, ctx) => {
1973
- const { result, resultInfo } = await callCloudflare(
1974
- base,
1975
- {
1976
- method: "GET",
1977
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
1978
- query: {
1979
- name: optionalString(args, "name"),
1980
- type: optionalString(args, "type"),
1981
- content: optionalString(args, "content"),
1982
- order: optionalString(args, "order"),
1983
- direction: optionalString(args, "direction"),
1984
- page: optionalNumber(args, "page"),
1985
- per_page: optionalNumber(args, "perPage"),
1986
- },
1987
- },
1988
- ctx,
1989
- );
1990
- if (args["raw"] === true)
1991
- return { records: result, page: pageInfo(resultInfo) };
1992
- return {
1993
- records: asArray(result).map(projectDnsRecord),
1994
- page: pageInfo(resultInfo),
1995
- };
1996
- },
1997
- },
1998
- {
1999
- name: "get_dns_record",
2000
- description: "Fetch one DNS record by its record id.",
2001
- annotations: readOnly,
2002
- inputSchema: {
2003
- type: "object",
2004
- properties: {
2005
- zoneId: scopeProperty("zoneId", scope.zoneId),
2006
- recordId: {
2007
- type: "string",
2008
- description: "DNS record id, from list_dns_records.",
2009
- },
2010
- raw: RAW_INPUT_PROPERTY,
2011
- },
2012
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
2013
- additionalProperties: false,
2014
- },
2015
- outputSchema: DNS_RECORD_SCHEMA,
2016
- handler: async (args: JsonRecord, ctx) => {
2017
- const { result } = await callCloudflare(
2018
- base,
2019
- {
2020
- method: "GET",
2021
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(
2022
- String(args["recordId"]),
2023
- )}`,
2024
- },
2025
- ctx,
2026
- );
2027
- return args["raw"] === true ? result : projectDnsRecord(result);
2028
- },
2029
- },
2030
- {
2031
- name: "list_worker_scripts",
2032
- description:
2033
- "List Workers scripts deployed in an account, with their last-modified times.",
2034
- annotations: readOnly,
2035
- inputSchema: {
2036
- type: "object",
2037
- properties: {
2038
- accountId: scopeProperty("accountId", scope.accountId),
2039
- raw: RAW_INPUT_PROPERTY,
2040
- },
2041
- required: scopeRequired("accountId", scope.accountId),
2042
- additionalProperties: false,
2043
- },
2044
- outputSchema: listOutputSchema("scripts", {
2045
- type: "object",
2046
- properties: {
2047
- id: { type: "string", description: "Script name." },
2048
- createdOn: { type: "string" },
2049
- modifiedOn: { type: "string" },
2050
- usageModel: { type: "string" },
2051
- },
2052
- required: ["id"],
2053
- }),
2054
- handler: async (args: JsonRecord, ctx) => {
2055
- const { result, resultInfo } = await callCloudflare(
2056
- base,
2057
- {
2058
- method: "GET",
2059
- path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
2060
- },
2061
- ctx,
2062
- );
2063
- if (args["raw"] === true)
2064
- return { scripts: result, page: pageInfo(resultInfo) };
2065
- return {
2066
- scripts: asArray(result).map(projectWorkerScript),
2067
- page: pageInfo(resultInfo),
2068
- };
2069
- },
2070
- },
2071
- {
2072
- name: "get_worker_settings",
2073
- description:
2074
- "Get a Worker's compatibility date and flags, bindings, limits, observability, placement, usage model, and other script settings.",
2075
- annotations: readOnly,
2076
- inputSchema: {
2077
- type: "object",
2078
- properties: {
2079
- accountId: scopeProperty("accountId", scope.accountId),
2080
- scriptName: {
2081
- type: "string",
2082
- minLength: 1,
2083
- description: "Worker script name from list_worker_scripts.",
2084
- },
2085
- },
2086
- required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
2087
- additionalProperties: false,
2088
- },
2089
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2090
- handler: async (args: JsonRecord, ctx) => {
2091
- const { result } = await callCloudflare(
2092
- base,
2093
- {
2094
- method: "GET",
2095
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/settings`,
2096
- },
2097
- ctx,
2098
- );
2099
- return result;
2100
- },
2101
- },
2102
- {
2103
- name: "list_worker_deployments",
2104
- description:
2105
- "List deployments of a Worker script, including version traffic allocations and deployment strategy.",
2106
- annotations: readOnly,
2107
- inputSchema: {
2108
- type: "object",
2109
- properties: {
2110
- accountId: scopeProperty("accountId", scope.accountId),
2111
- scriptName: {
2112
- type: "string",
2113
- minLength: 1,
2114
- description: "Worker script name from list_worker_scripts.",
2115
- },
2116
- },
2117
- required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
2118
- additionalProperties: false,
2119
- },
2120
- outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
2121
- handler: async (args: JsonRecord, ctx) => {
2122
- const { result } = await callCloudflare(
2123
- base,
2124
- {
2125
- method: "GET",
2126
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments`,
2127
- },
2128
- ctx,
2129
- );
2130
- const record = asRecord(result);
2131
- const deployments = Array.isArray(result)
2132
- ? result
2133
- : asArray(record["deployments"]);
2134
- return { deployments: deployments.map(projectWorkerDeployment) };
2135
- },
2136
- },
2137
- {
2138
- name: "get_worker_deployment",
2139
- description: "Get one Worker deployment and its version traffic allocations.",
2140
- annotations: readOnly,
2141
- inputSchema: {
2142
- type: "object",
2143
- properties: {
2144
- accountId: scopeProperty("accountId", scope.accountId),
2145
- scriptName: {
2146
- type: "string",
2147
- minLength: 1,
2148
- description: "Worker script name from list_worker_scripts.",
2149
- },
2150
- deploymentId: {
2151
- type: "string",
2152
- minLength: 1,
2153
- description: "Deployment id from list_worker_deployments.",
2154
- },
2155
- },
2156
- required: [
2157
- ...scopeRequired("accountId", scope.accountId),
2158
- "scriptName",
2159
- "deploymentId",
2160
- ],
2161
- additionalProperties: false,
2162
- },
2163
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2164
- handler: async (args: JsonRecord, ctx) => {
2165
- const { result } = await callCloudflare(
2166
- base,
2167
- {
2168
- method: "GET",
2169
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
2170
- },
2171
- ctx,
2172
- );
2173
- return projectWorkerDeployment(result);
2174
- },
2175
- },
2176
- {
2177
- name: "delete_worker_script",
2178
- description:
2179
- "Delete a Worker script and stop traffic served by that script. This cannot be undone from the API.",
2180
- annotations: { readOnlyHint: false, destructiveHint: true },
2181
- inputSchema: {
2182
- type: "object",
2183
- properties: {
2184
- accountId: scopeProperty("accountId", scope.accountId),
2185
- scriptName: {
2186
- type: "string",
2187
- minLength: 1,
2188
- description: "Worker script name from list_worker_scripts.",
2189
- },
2190
- force: {
2191
- type: "boolean",
2192
- description:
2193
- "Pass Cloudflare's force=true option when the script has dependencies that permit forced removal.",
2194
- },
2195
- },
2196
- required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
2197
- additionalProperties: false,
2198
- },
2199
- outputSchema: {
2200
- type: "object",
2201
- properties: {
2202
- deleted: { type: "boolean" },
2203
- scriptName: { type: "string" },
2204
- },
2205
- required: ["deleted", "scriptName"],
2206
- },
2207
- handler: async (args: JsonRecord, ctx) => {
2208
- const scriptName = requireString(args, "scriptName");
2209
- await callCloudflare(
2210
- base,
2211
- {
2212
- method: "DELETE",
2213
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(scriptName)}`,
2214
- query: { force: optionalBoolean(args, "force") },
2215
- },
2216
- ctx,
2217
- );
2218
- return { deleted: true, scriptName };
2219
- },
2220
- },
2221
- {
2222
- name: "list_kv_namespaces",
2223
- description:
2224
- "List Workers KV namespaces in an account, with the namespace ids bindings refer to.",
2225
- annotations: readOnly,
2226
- inputSchema: {
2227
- type: "object",
2228
- properties: {
2229
- accountId: scopeProperty("accountId", scope.accountId),
2230
- ...pagingInputProperties(1, 1000, { defaultPerPage: 20 }),
2231
- raw: RAW_INPUT_PROPERTY,
2232
- },
2233
- required: scopeRequired("accountId", scope.accountId),
2234
- additionalProperties: false,
2235
- },
2236
- outputSchema: listOutputSchema("namespaces", {
2237
- type: "object",
2238
- properties: {
2239
- id: { type: "string" },
2240
- title: { type: "string" },
2241
- supportsUrlEncoding: { type: "boolean" },
2242
- },
2243
- required: ["id", "title"],
2244
- }),
2245
- handler: async (args: JsonRecord, ctx) => {
2246
- const { result, resultInfo } = await callCloudflare(
2247
- base,
2248
- {
2249
- method: "GET",
2250
- path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
2251
- query: {
2252
- page: optionalNumber(args, "page"),
2253
- per_page: optionalNumber(args, "perPage"),
2254
- },
2255
- },
2256
- ctx,
2257
- );
2258
- if (args["raw"] === true)
2259
- return { namespaces: result, page: pageInfo(resultInfo) };
2260
- return {
2261
- namespaces: asArray(result).map(projectKvNamespace),
2262
- page: pageInfo(resultInfo),
2263
- };
2264
- },
2265
- },
2266
- {
2267
- name: "get_kv_namespace",
2268
- description: "Get one Workers KV namespace by id.",
2269
- annotations: readOnly,
2270
- inputSchema: {
2271
- type: "object",
2272
- properties: {
2273
- accountId: scopeProperty("accountId", scope.accountId),
2274
- namespaceId: {
2275
- type: "string",
2276
- minLength: 1,
2277
- description: "KV namespace id from list_kv_namespaces.",
2278
- },
2279
- },
2280
- required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
2281
- additionalProperties: false,
2282
- },
2283
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2284
- handler: async (args: JsonRecord, ctx) => {
2285
- const { result } = await callCloudflare(
2286
- base,
2287
- {
2288
- method: "GET",
2289
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
2290
- },
2291
- ctx,
2292
- );
2293
- return projectKvNamespace(result);
2294
- },
2295
- },
2296
- {
2297
- name: "create_kv_namespace",
2298
- description: "Create a Workers KV namespace.",
2299
- annotations: { readOnlyHint: false },
2300
- inputSchema: {
2301
- type: "object",
2302
- properties: {
2303
- accountId: scopeProperty("accountId", scope.accountId),
2304
- title: {
2305
- type: "string",
2306
- minLength: 1,
2307
- maxLength: 512,
2308
- description: "Human-readable namespace title.",
2309
- },
2310
- },
2311
- required: [...scopeRequired("accountId", scope.accountId), "title"],
2312
- additionalProperties: false,
2313
- },
2314
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2315
- handler: async (args: JsonRecord, ctx) => {
2316
- const { result } = await callCloudflare(
2317
- base,
2318
- {
2319
- method: "POST",
2320
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces`,
2321
- body: { title: requireString(args, "title") },
2322
- },
2323
- ctx,
2324
- );
2325
- return projectKvNamespace(result);
2326
- },
2327
- },
2328
- {
2329
- name: "rename_kv_namespace",
2330
- description: "Rename an existing Workers KV namespace without changing its id or keys.",
2331
- annotations: { readOnlyHint: false, destructiveHint: true },
2332
- inputSchema: {
2333
- type: "object",
2334
- properties: {
2335
- accountId: scopeProperty("accountId", scope.accountId),
2336
- namespaceId: {
2337
- type: "string",
2338
- minLength: 1,
2339
- description: "KV namespace id from list_kv_namespaces.",
2340
- },
2341
- title: {
2342
- type: "string",
2343
- minLength: 1,
2344
- maxLength: 512,
2345
- description: "Replacement namespace title.",
2346
- },
2347
- },
2348
- required: [
2349
- ...scopeRequired("accountId", scope.accountId),
2350
- "namespaceId",
2351
- "title",
2352
- ],
2353
- additionalProperties: false,
2354
- },
2355
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2356
- handler: async (args: JsonRecord, ctx) => {
2357
- const { result } = await callCloudflare(
2358
- base,
2359
- {
2360
- method: "PUT",
2361
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
2362
- body: { title: requireString(args, "title") },
2363
- },
2364
- ctx,
2365
- );
2366
- return result ?? { renamed: true, namespaceId: args["namespaceId"] };
2367
- },
2368
- },
2369
- {
2370
- name: "delete_kv_namespace",
2371
- description: "Permanently delete a Workers KV namespace and every key stored in it.",
2372
- annotations: { readOnlyHint: false, destructiveHint: true },
2373
- inputSchema: {
2374
- type: "object",
2375
- properties: {
2376
- accountId: scopeProperty("accountId", scope.accountId),
2377
- namespaceId: {
2378
- type: "string",
2379
- minLength: 1,
2380
- description: "KV namespace id from list_kv_namespaces.",
2381
- },
2382
- },
2383
- required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
2384
- additionalProperties: false,
2385
- },
2386
- outputSchema: {
2387
- type: "object",
2388
- properties: {
2389
- deleted: { type: "boolean" },
2390
- namespaceId: { type: "string" },
2391
- },
2392
- required: ["deleted", "namespaceId"],
2393
- },
2394
- handler: async (args: JsonRecord, ctx) => {
2395
- const namespaceId = requireString(args, "namespaceId");
2396
- await callCloudflare(
2397
- base,
2398
- {
2399
- method: "DELETE",
2400
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(namespaceId)}`,
2401
- },
2402
- ctx,
2403
- );
2404
- return { deleted: true, namespaceId };
2405
- },
2406
- },
2407
- {
2408
- name: "list_kv_keys",
2409
- description:
2410
- "List keys and metadata in a Workers KV namespace by prefix, using cursor pagination.",
2411
- annotations: readOnly,
2412
- inputSchema: {
2413
- type: "object",
2414
- properties: {
2415
- accountId: scopeProperty("accountId", scope.accountId),
2416
- namespaceId: {
2417
- type: "string",
2418
- minLength: 1,
2419
- description: "KV namespace id from list_kv_namespaces.",
2420
- },
2421
- prefix: {
2422
- type: "string",
2423
- description: "Return only keys beginning with this prefix.",
2424
- },
2425
- limit: {
2426
- type: "integer",
2427
- minimum: 10,
2428
- maximum: 1000,
2429
- description: "Keys per request, 10 to 1000. Defaults to 1000.",
2430
- },
2431
- cursor: {
2432
- type: "string",
2433
- description: "Opaque cursor returned as nextCursor by the previous call.",
2434
- },
2435
- },
2436
- required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
2437
- additionalProperties: false,
2438
- },
2439
- outputSchema: {
2440
- type: "object",
2441
- properties: {
2442
- keys: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
2443
- nextCursor: { type: "string" },
2444
- },
2445
- required: ["keys"],
2446
- },
2447
- handler: async (args: JsonRecord, ctx) => {
2448
- const { result, resultInfo } = await callCloudflare(
2449
- base,
2450
- {
2451
- method: "GET",
2452
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/keys`,
2453
- query: {
2454
- prefix: optionalString(args, "prefix"),
2455
- limit: optionalNumber(args, "limit"),
2456
- cursor: optionalString(args, "cursor"),
2457
- },
2458
- },
2459
- ctx,
2460
- );
2461
- const cursor = resultInfo?.cursor;
2462
- return {
2463
- keys: asArray(result).map(projectKvKey),
2464
- ...(typeof cursor === "string" && cursor !== ""
2465
- ? { nextCursor: cursor }
2466
- : {}),
2467
- };
2468
- },
2469
- },
2470
- {
2471
- name: "bulk_get_kv_values",
2472
- description:
2473
- "Read up to 100 Workers KV values in one request. This JSON endpoint is suitable for text and JSON values; use the raw API for specialized response types.",
2474
- annotations: readOnly,
2475
- inputSchema: {
2476
- type: "object",
2477
- properties: {
2478
- accountId: scopeProperty("accountId", scope.accountId),
2479
- namespaceId: {
2480
- type: "string",
2481
- minLength: 1,
2482
- description: "KV namespace id from list_kv_namespaces.",
2483
- },
2484
- keys: {
2485
- type: "array",
2486
- minItems: 1,
2487
- maxItems: 100,
2488
- items: { type: "string", minLength: 1, maxLength: 512 },
2489
- description: "Key names to retrieve, up to 100.",
2490
- },
2491
- withMetadata: {
2492
- type: "boolean",
2493
- description: "Include each key's metadata and expiration when true.",
2494
- },
2495
- type: {
2496
- type: "string",
2497
- enum: ["text", "json"],
2498
- description: "Return strings as stored, or parse JSON values before returning them.",
2499
- },
2500
- },
2501
- required: [
2502
- ...scopeRequired("accountId", scope.accountId),
2503
- "namespaceId",
2504
- "keys",
2505
- ],
2506
- additionalProperties: false,
2507
- },
2508
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2509
- handler: async (args: JsonRecord, ctx) => {
2510
- const { result } = await callCloudflare(
2511
- base,
2512
- {
2513
- method: "POST",
2514
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/get`,
2515
- body: {
2516
- keys: args["keys"],
2517
- ...(args["withMetadata"] !== undefined
2518
- ? { withMetadata: args["withMetadata"] }
2519
- : {}),
2520
- ...(args["type"] !== undefined ? { type: args["type"] } : {}),
2521
- },
2522
- },
2523
- ctx,
2524
- );
2525
- return asRecord(result);
2526
- },
2527
- },
2528
- {
2529
- name: "bulk_write_kv_values",
2530
- description:
2531
- "Create or replace multiple Workers KV values, with optional expirations and JSON metadata.",
2532
- annotations: { readOnlyHint: false, destructiveHint: true },
2533
- inputSchema: {
2534
- type: "object",
2535
- properties: {
2536
- accountId: scopeProperty("accountId", scope.accountId),
2537
- namespaceId: {
2538
- type: "string",
2539
- minLength: 1,
2540
- description: "KV namespace id from list_kv_namespaces.",
2541
- },
2542
- entries: {
2543
- type: "array",
2544
- minItems: 1,
2545
- maxItems: 10_000,
2546
- description: "Key/value entries to write, up to Cloudflare's 10,000-key bulk limit.",
2547
- items: {
2548
- type: "object",
2549
- properties: {
2550
- key: { type: "string", minLength: 1, maxLength: 512 },
2551
- value: { type: "string", maxLength: 26_214_400 },
2552
- expiration: { type: "number" },
2553
- expiration_ttl: { type: "number", minimum: 60 },
2554
- metadata: { type: ["object", "array", "string", "number", "boolean", "null"] },
2555
- base64: { type: "boolean" },
2556
- },
2557
- required: ["key", "value"],
2558
- additionalProperties: false,
2559
- },
2560
- },
2561
- },
2562
- required: [
2563
- ...scopeRequired("accountId", scope.accountId),
2564
- "namespaceId",
2565
- "entries",
2566
- ],
2567
- additionalProperties: false,
2568
- },
2569
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2570
- handler: async (args: JsonRecord, ctx) => {
2571
- const { result } = await callCloudflare(
2572
- base,
2573
- {
2574
- method: "PUT",
2575
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk`,
2576
- body: args["entries"],
2577
- },
2578
- ctx,
2579
- );
2580
- return asRecord(result);
2581
- },
2582
- },
2583
- {
2584
- name: "bulk_delete_kv_values",
2585
- description: "Permanently delete multiple keys from a Workers KV namespace.",
2586
- annotations: { readOnlyHint: false, destructiveHint: true },
2587
- inputSchema: {
2588
- type: "object",
2589
- properties: {
2590
- accountId: scopeProperty("accountId", scope.accountId),
2591
- namespaceId: {
2592
- type: "string",
2593
- minLength: 1,
2594
- description: "KV namespace id from list_kv_namespaces.",
2595
- },
2596
- keys: {
2597
- type: "array",
2598
- minItems: 1,
2599
- maxItems: 10_000,
2600
- items: { type: "string", minLength: 1, maxLength: 512 },
2601
- description: "Key names to delete, up to Cloudflare's 10,000-key bulk limit.",
2602
- },
2603
- },
2604
- required: [
2605
- ...scopeRequired("accountId", scope.accountId),
2606
- "namespaceId",
2607
- "keys",
2608
- ],
2609
- additionalProperties: false,
2610
- },
2611
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2612
- handler: async (args: JsonRecord, ctx) => {
2613
- const { result } = await callCloudflare(
2614
- base,
2615
- {
2616
- method: "POST",
2617
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/delete`,
2618
- body: args["keys"],
2619
- },
2620
- ctx,
2621
- );
2622
- return asRecord(result);
2623
- },
2624
- },
2625
- {
2626
- name: "list_r2_buckets",
2627
- description:
2628
- "List R2 buckets in an account, with location and storage class.",
2629
- annotations: readOnly,
2630
- inputSchema: {
2631
- type: "object",
2632
- properties: {
2633
- accountId: scopeProperty("accountId", scope.accountId),
2634
- nameContains: {
2635
- type: "string",
2636
- description: "Filter to buckets whose name contains this string.",
2637
- },
2638
- perPage: {
2639
- type: "integer",
2640
- minimum: 1,
2641
- maximum: 1000,
2642
- description: "Buckets per request, 1 to 1000. Defaults to 20.",
2643
- },
2644
- cursor: {
2645
- type: "string",
2646
- description:
2647
- "Opaque cursor from a previous call's nextCursor. R2 paginates by cursor, not page number.",
2648
- },
2649
- jurisdiction: R2_JURISDICTION_PROPERTY,
2650
- raw: RAW_INPUT_PROPERTY,
2651
- },
2652
- required: scopeRequired("accountId", scope.accountId),
2653
- additionalProperties: false,
2654
- },
2655
- outputSchema: {
2656
- type: "object",
2657
- properties: {
2658
- buckets: {
2659
- type: "array",
2660
- items: {
2661
- type: "object",
2662
- properties: {
2663
- name: { type: "string" },
2664
- location: { type: "string" },
2665
- storageClass: { type: "string" },
2666
- jurisdiction: { type: "string" },
2667
- creationDate: { type: "string" },
2668
- },
2669
- required: ["name"],
2670
- },
2671
- },
2672
- nextCursor: {
2673
- type: "string",
2674
- description:
2675
- "Pass back as `cursor` to continue. Absent when the listing is complete.",
2676
- },
2677
- },
2678
- required: ["buckets"],
2679
- },
2680
- handler: async (args: JsonRecord, ctx) => {
2681
- const { result, resultInfo } = await callCloudflare(
2682
- base,
2683
- {
2684
- method: "GET",
2685
- path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
2686
- query: {
2687
- name_contains: optionalString(args, "nameContains"),
2688
- per_page: optionalNumber(args, "perPage"),
2689
- cursor: optionalString(args, "cursor"),
2690
- },
2691
- headers: r2Headers(args),
2692
- },
2693
- ctx,
2694
- );
2695
- // R2 nests its list under `buckets` rather than returning a bare array,
2696
- // and its result_info carries a cursor instead of page counters.
2697
- const cursor = resultInfo?.cursor;
2698
- const next =
2699
- typeof cursor === "string" && cursor !== ""
2700
- ? { nextCursor: cursor }
2701
- : {};
2702
- if (args["raw"] === true) return { buckets: result, ...next };
2703
- return {
2704
- buckets: asArray(asRecord(result)["buckets"]).map(projectR2Bucket),
2705
- ...next,
2706
- };
2707
- },
2708
- },
2709
- {
2710
- name: "get_r2_bucket",
2711
- description: "Get one R2 bucket's location, jurisdiction, storage class, and creation time.",
2712
- annotations: readOnly,
2713
- inputSchema: {
2714
- type: "object",
2715
- properties: {
2716
- accountId: scopeProperty("accountId", scope.accountId),
2717
- bucketName: R2_BUCKET_NAME_PROPERTY,
2718
- jurisdiction: R2_JURISDICTION_PROPERTY,
2719
- },
2720
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2721
- additionalProperties: false,
2722
- },
2723
- outputSchema: R2_BUCKET_SCHEMA,
2724
- handler: async (args: JsonRecord, ctx) => {
2725
- const { result } = await callCloudflare(
2726
- base,
2727
- {
2728
- method: "GET",
2729
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2730
- headers: r2Headers(args),
2731
- },
2732
- ctx,
2733
- );
2734
- return projectR2Bucket(result);
2735
- },
2736
- },
2737
- {
2738
- name: "create_r2_bucket",
2739
- description: "Create an R2 bucket with an optional location hint and default storage class.",
2740
- annotations: { readOnlyHint: false },
2741
- inputSchema: {
2742
- type: "object",
2743
- properties: {
2744
- accountId: scopeProperty("accountId", scope.accountId),
2745
- bucketName: R2_BUCKET_NAME_PROPERTY,
2746
- jurisdiction: R2_JURISDICTION_PROPERTY,
2747
- locationHint: {
2748
- type: "string",
2749
- enum: ["apac", "eeur", "enam", "weur", "wnam", "oc"],
2750
- description: "Optional placement hint for the new bucket.",
2751
- },
2752
- storageClass: {
2753
- type: "string",
2754
- enum: ["Standard", "InfrequentAccess"],
2755
- description: "Default storage class for new objects.",
2756
- },
2757
- },
2758
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2759
- additionalProperties: false,
2760
- },
2761
- outputSchema: R2_BUCKET_SCHEMA,
2762
- handler: async (args: JsonRecord, ctx) => {
2763
- const { result } = await callCloudflare(
2764
- base,
2765
- {
2766
- method: "POST",
2767
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets`,
2768
- headers: r2Headers(args),
2769
- body: {
2770
- name: requireString(args, "bucketName"),
2771
- ...(args["locationHint"] !== undefined
2772
- ? { locationHint: args["locationHint"] }
2773
- : {}),
2774
- ...(args["storageClass"] !== undefined
2775
- ? { storageClass: args["storageClass"] }
2776
- : {}),
2777
- },
2778
- },
2779
- ctx,
2780
- );
2781
- return projectR2Bucket(result);
2782
- },
2783
- },
2784
- {
2785
- name: "update_r2_bucket",
2786
- description: "Change the default storage class used for newly uploaded objects in an R2 bucket.",
2787
- annotations: { readOnlyHint: false, destructiveHint: true },
2788
- inputSchema: {
2789
- type: "object",
2790
- properties: {
2791
- accountId: scopeProperty("accountId", scope.accountId),
2792
- bucketName: R2_BUCKET_NAME_PROPERTY,
2793
- jurisdiction: R2_JURISDICTION_PROPERTY,
2794
- storageClass: {
2795
- type: "string",
2796
- enum: ["Standard", "InfrequentAccess"],
2797
- description: "New default storage class for future uploads.",
2798
- },
2799
- },
2800
- required: [
2801
- ...scopeRequired("accountId", scope.accountId),
2802
- "bucketName",
2803
- "storageClass",
2804
- ],
2805
- additionalProperties: false,
2806
- },
2807
- outputSchema: R2_BUCKET_SCHEMA,
2808
- handler: async (args: JsonRecord, ctx) => {
2809
- const { result } = await callCloudflare(
2810
- base,
2811
- {
2812
- method: "PATCH",
2813
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2814
- headers: {
2815
- ...r2Headers(args),
2816
- "cf-r2-storage-class": String(args["storageClass"]),
2817
- },
2818
- },
2819
- ctx,
2820
- );
2821
- return projectR2Bucket(result);
2822
- },
2823
- },
2824
- {
2825
- name: "delete_r2_bucket",
2826
- description:
2827
- "Permanently delete an empty R2 bucket and all of its configuration. Cloudflare refuses non-empty buckets.",
2828
- annotations: { readOnlyHint: false, destructiveHint: true },
2829
- inputSchema: {
2830
- type: "object",
2831
- properties: {
2832
- accountId: scopeProperty("accountId", scope.accountId),
2833
- bucketName: R2_BUCKET_NAME_PROPERTY,
2834
- jurisdiction: R2_JURISDICTION_PROPERTY,
2835
- },
2836
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2837
- additionalProperties: false,
2838
- },
2839
- outputSchema: {
2840
- type: "object",
2841
- properties: {
2842
- deleted: { type: "boolean" },
2843
- bucketName: { type: "string" },
2844
- },
2845
- required: ["deleted", "bucketName"],
2846
- },
2847
- handler: async (args: JsonRecord, ctx) => {
2848
- const bucketName = requireString(args, "bucketName");
2849
- await callCloudflare(
2850
- base,
2851
- {
2852
- method: "DELETE",
2853
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(bucketName)}`,
2854
- headers: r2Headers(args),
2855
- },
2856
- ctx,
2857
- );
2858
- return { deleted: true, bucketName };
2859
- },
2860
- },
2861
- {
2862
- name: "list_r2_objects",
2863
- description:
2864
- "List object keys and metadata in an R2 bucket by prefix, with delimiter grouping and cursor pagination.",
2865
- annotations: readOnly,
2866
- inputSchema: {
2867
- type: "object",
2868
- properties: {
2869
- accountId: scopeProperty("accountId", scope.accountId),
2870
- bucketName: R2_BUCKET_NAME_PROPERTY,
2871
- jurisdiction: R2_JURISDICTION_PROPERTY,
2872
- prefix: {
2873
- type: "string",
2874
- description: "Return only object keys beginning with this prefix.",
2875
- },
2876
- delimiter: {
2877
- type: "string",
2878
- minLength: 1,
2879
- maxLength: 1,
2880
- description: "One character used to group path-like keys, usually '/'.",
2881
- },
2882
- startAfter: {
2883
- type: "string",
2884
- description: "Begin after this key in lexicographic order.",
2885
- },
2886
- perPage: {
2887
- type: "integer",
2888
- minimum: 1,
2889
- maximum: 1000,
2890
- description: "Objects per request, 1 to 1000.",
2891
- },
2892
- cursor: {
2893
- type: "string",
2894
- description: "Opaque cursor returned as nextCursor by the previous call.",
2895
- },
2896
- },
2897
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2898
- additionalProperties: false,
2899
- },
2900
- outputSchema: {
2901
- type: "object",
2902
- properties: {
2903
- objects: { type: "array", items: R2_OBJECT_SCHEMA },
2904
- commonPrefixes: { type: "array", items: { type: "string" } },
2905
- nextCursor: { type: "string" },
2906
- truncated: { type: "boolean" },
2907
- },
2908
- required: ["objects", "truncated"],
2909
- },
2910
- handler: async (args: JsonRecord, ctx) => {
2911
- const { result, resultInfo } = await callCloudflare(
2912
- base,
2913
- {
2914
- method: "GET",
2915
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects`,
2916
- headers: r2Headers(args),
2917
- query: {
2918
- prefix: optionalString(args, "prefix"),
2919
- delimiter: optionalString(args, "delimiter"),
2920
- start_after: optionalString(args, "startAfter"),
2921
- per_page: optionalNumber(args, "perPage"),
2922
- cursor: optionalString(args, "cursor"),
2923
- },
2924
- },
2925
- ctx,
2926
- );
2927
- const cursor = resultInfo?.cursor;
2928
- return {
2929
- objects: asArray(result).map(projectR2Object),
2930
- ...(Array.isArray(resultInfo?.delimited)
2931
- ? { commonPrefixes: resultInfo.delimited }
2932
- : {}),
2933
- ...(typeof cursor === "string" && cursor !== ""
2934
- ? { nextCursor: cursor }
2935
- : {}),
2936
- truncated: resultInfo?.is_truncated === true,
2937
- };
2938
- },
2939
- },
2940
- {
2941
- name: "delete_r2_object",
2942
- description: "Permanently delete one object from an R2 bucket by key.",
2943
- annotations: { readOnlyHint: false, destructiveHint: true },
2944
- inputSchema: {
2945
- type: "object",
2946
- properties: {
2947
- accountId: scopeProperty("accountId", scope.accountId),
2948
- bucketName: R2_BUCKET_NAME_PROPERTY,
2949
- jurisdiction: R2_JURISDICTION_PROPERTY,
2950
- objectKey: {
2951
- type: "string",
2952
- minLength: 1,
2953
- description: "Exact object key. Slashes are preserved as path separators.",
2954
- },
2955
- },
2956
- required: [
2957
- ...scopeRequired("accountId", scope.accountId),
2958
- "bucketName",
2959
- "objectKey",
2960
- ],
2961
- additionalProperties: false,
2962
- },
2963
- outputSchema: {
2964
- type: "object",
2965
- properties: {
2966
- deleted: { type: "boolean" },
2967
- objectKey: { type: "string" },
2968
- },
2969
- required: ["deleted", "objectKey"],
2970
- },
2971
- handler: async (args: JsonRecord, ctx) => {
2972
- const objectKey = requireString(args, "objectKey");
2973
- await callCloudflare(
2974
- base,
2975
- {
2976
- method: "DELETE",
2977
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects/${encodeObjectKey(objectKey)}`,
2978
- headers: r2Headers(args),
2979
- },
2980
- ctx,
2981
- );
2982
- return { deleted: true, objectKey };
2983
- },
2984
- },
2985
- {
2986
- name: "get_r2_metrics",
2987
- description:
2988
- "Get account-level R2 object-count and storage-size metrics split by storage class and publication state.",
2989
- annotations: readOnly,
2990
- inputSchema: {
2991
- type: "object",
2992
- properties: { accountId: scopeProperty("accountId", scope.accountId) },
2993
- required: scopeRequired("accountId", scope.accountId),
2994
- additionalProperties: false,
2995
- },
2996
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2997
- handler: async (args: JsonRecord, ctx) => {
2998
- const { result } = await callCloudflare(
2999
- base,
3000
- {
3001
- method: "GET",
3002
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/metrics`,
3003
- },
3004
- ctx,
3005
- );
3006
- return asRecord(result);
3007
- },
3008
- },
3009
- {
3010
- name: "get_r2_cors",
3011
- description: "Get the browser CORS rules configured on an R2 bucket.",
3012
- annotations: readOnly,
3013
- inputSchema: {
3014
- type: "object",
3015
- properties: {
3016
- accountId: scopeProperty("accountId", scope.accountId),
3017
- bucketName: R2_BUCKET_NAME_PROPERTY,
3018
- jurisdiction: R2_JURISDICTION_PROPERTY,
3019
- },
3020
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
3021
- additionalProperties: false,
3022
- },
3023
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3024
- handler: async (args: JsonRecord, ctx) => {
3025
- const { result } = await callCloudflare(
3026
- base,
3027
- {
3028
- method: "GET",
3029
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
3030
- headers: r2Headers(args),
3031
- },
3032
- ctx,
3033
- );
3034
- return asRecord(result);
3035
- },
3036
- },
3037
- {
3038
- name: "set_r2_cors",
3039
- description:
3040
- "Replace an R2 bucket's CORS policy. Supply the complete desired rule list; omitted existing rules are removed.",
3041
- annotations: { readOnlyHint: false, destructiveHint: true },
3042
- inputSchema: {
3043
- type: "object",
3044
- properties: {
3045
- accountId: scopeProperty("accountId", scope.accountId),
3046
- bucketName: R2_BUCKET_NAME_PROPERTY,
3047
- jurisdiction: R2_JURISDICTION_PROPERTY,
3048
- rules: {
3049
- type: "array",
3050
- maxItems: 100,
3051
- description:
3052
- "Complete CORS rule list using Cloudflare fields: allowed.methods, allowed.origins, optional allowed.headers, id, exposeHeaders, and maxAgeSeconds.",
3053
- items: { type: "object", additionalProperties: true },
3054
- },
3055
- },
3056
- required: [...scopeRequired("accountId", scope.accountId), "bucketName", "rules"],
3057
- additionalProperties: false,
3058
- },
3059
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3060
- handler: async (args: JsonRecord, ctx) => {
3061
- const { result } = await callCloudflare(
3062
- base,
3063
- {
3064
- method: "PUT",
3065
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
3066
- headers: r2Headers(args),
3067
- body: { rules: args["rules"] },
3068
- },
3069
- ctx,
3070
- );
3071
- return asRecord(result);
3072
- },
3073
- },
3074
- {
3075
- name: "delete_r2_cors",
3076
- description: "Remove the complete CORS policy from an R2 bucket.",
3077
- annotations: { readOnlyHint: false, destructiveHint: true },
3078
- inputSchema: {
3079
- type: "object",
3080
- properties: {
3081
- accountId: scopeProperty("accountId", scope.accountId),
3082
- bucketName: R2_BUCKET_NAME_PROPERTY,
3083
- jurisdiction: R2_JURISDICTION_PROPERTY,
3084
- },
3085
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
3086
- additionalProperties: false,
3087
- },
3088
- outputSchema: {
3089
- type: "object",
3090
- properties: { deleted: { type: "boolean" } },
3091
- required: ["deleted"],
3092
- },
3093
- handler: async (args: JsonRecord, ctx) => {
3094
- await callCloudflare(
3095
- base,
3096
- {
3097
- method: "DELETE",
3098
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
3099
- headers: r2Headers(args),
3100
- },
3101
- ctx,
3102
- );
3103
- return { deleted: true };
3104
- },
3105
- },
3106
- {
3107
- name: "list_pages_projects",
3108
- description:
3109
- "List Cloudflare Pages projects in an account, with their production branch and latest deployment.",
3110
- annotations: readOnly,
3111
- inputSchema: {
3112
- type: "object",
3113
- properties: {
3114
- accountId: scopeProperty("accountId", scope.accountId),
3115
- ...pagingInputProperties(1, 100, { bounds: "undocumented" }),
3116
- raw: RAW_INPUT_PROPERTY,
3117
- },
3118
- required: scopeRequired("accountId", scope.accountId),
3119
- additionalProperties: false,
3120
- },
3121
- outputSchema: listOutputSchema("projects", {
3122
- type: "object",
3123
- properties: {
3124
- name: { type: "string" },
3125
- subdomain: { type: "string" },
3126
- domains: { type: "array", items: { type: "string" } },
3127
- productionBranch: { type: "string" },
3128
- createdOn: { type: "string" },
3129
- latestDeployment: {
3130
- type: "object",
3131
- properties: {
3132
- id: { type: "string" },
3133
- environment: { type: "string" },
3134
- url: { type: "string" },
3135
- createdOn: { type: "string" },
3136
- },
3137
- },
3138
- },
3139
- required: ["name"],
3140
- }),
3141
- handler: async (args: JsonRecord, ctx) => {
3142
- const { result, resultInfo } = await callCloudflare(
3143
- base,
3144
- {
3145
- method: "GET",
3146
- path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects`,
3147
- query: {
3148
- page: optionalNumber(args, "page"),
3149
- per_page: optionalNumber(args, "perPage"),
3150
- },
3151
- },
3152
- ctx,
3153
- );
3154
- if (args["raw"] === true)
3155
- return { projects: result, page: pageInfo(resultInfo) };
3156
- return {
3157
- projects: asArray(result).map(projectPagesProject),
3158
- page: pageInfo(resultInfo),
3159
- };
3160
- },
3161
- },
3162
- {
3163
- name: "get_pages_project",
3164
- description: "Get one Pages project, including build configuration, deployment configuration, domains, and latest deployment.",
3165
- annotations: readOnly,
3166
- inputSchema: {
3167
- type: "object",
3168
- properties: {
3169
- accountId: scopeProperty("accountId", scope.accountId),
3170
- projectName: {
3171
- type: "string",
3172
- minLength: 1,
3173
- description: "Pages project name from list_pages_projects.",
3174
- },
3175
- raw: RAW_INPUT_PROPERTY,
3176
- },
3177
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3178
- additionalProperties: false,
3179
- },
3180
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3181
- handler: async (args: JsonRecord, ctx) => {
3182
- const { result } = await callCloudflare(
3183
- base,
3184
- {
3185
- method: "GET",
3186
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}`,
3187
- },
3188
- ctx,
3189
- );
3190
- return args["raw"] === true ? result : projectPagesProject(result);
3191
- },
3192
- },
3193
- {
3194
- name: "list_pages_deployments",
3195
- description: "List production and preview deployments for a Pages project.",
3196
- annotations: readOnly,
3197
- inputSchema: {
3198
- type: "object",
3199
- properties: {
3200
- accountId: scopeProperty("accountId", scope.accountId),
3201
- projectName: {
3202
- type: "string",
3203
- minLength: 1,
3204
- description: "Pages project name from list_pages_projects.",
3205
- },
3206
- env: {
3207
- type: "string",
3208
- enum: ["production", "preview"],
3209
- description: "Optional deployment environment filter.",
3210
- },
3211
- ...pagingInputProperties(1, 100, { bounds: "undocumented" }),
3212
- },
3213
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3214
- additionalProperties: false,
3215
- },
3216
- outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
3217
- handler: async (args: JsonRecord, ctx) => {
3218
- const { result, resultInfo } = await callCloudflare(
3219
- base,
3220
- {
3221
- method: "GET",
3222
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments`,
3223
- query: {
3224
- env: optionalString(args, "env"),
3225
- page: optionalNumber(args, "page"),
3226
- per_page: optionalNumber(args, "perPage"),
3227
- },
3228
- },
3229
- ctx,
3230
- );
3231
- return {
3232
- deployments: asArray(result).map(projectPagesDeployment),
3233
- page: pageInfo(resultInfo),
3234
- };
3235
- },
3236
- },
3237
- {
3238
- name: "get_pages_deployment",
3239
- description: "Get one Pages deployment including its environment, URLs, stages, source, and build configuration.",
3240
- annotations: readOnly,
3241
- inputSchema: {
3242
- type: "object",
3243
- properties: {
3244
- accountId: scopeProperty("accountId", scope.accountId),
3245
- projectName: {
3246
- type: "string",
3247
- minLength: 1,
3248
- description: "Pages project name from list_pages_projects.",
3249
- },
3250
- deploymentId: {
3251
- type: "string",
3252
- minLength: 1,
3253
- description: "Deployment id from list_pages_deployments.",
3254
- },
3255
- raw: RAW_INPUT_PROPERTY,
3256
- },
3257
- required: [
3258
- ...scopeRequired("accountId", scope.accountId),
3259
- "projectName",
3260
- "deploymentId",
3261
- ],
3262
- additionalProperties: false,
3263
- },
3264
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3265
- handler: async (args: JsonRecord, ctx) => {
3266
- const { result } = await callCloudflare(
3267
- base,
3268
- {
3269
- method: "GET",
3270
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
3271
- },
3272
- ctx,
3273
- );
3274
- return args["raw"] === true ? result : projectPagesDeployment(result);
3275
- },
3276
- },
3277
- {
3278
- name: "retry_pages_deployment",
3279
- description: "Retry a failed or cancelled Pages deployment using its existing source and build configuration.",
3280
- annotations: { readOnlyHint: false },
3281
- inputSchema: {
3282
- type: "object",
3283
- properties: {
3284
- accountId: scopeProperty("accountId", scope.accountId),
3285
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3286
- deploymentId: { type: "string", minLength: 1, description: "Deployment id to retry." },
3287
- },
3288
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
3289
- additionalProperties: false,
3290
- },
3291
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3292
- handler: async (args: JsonRecord, ctx) => {
3293
- const { result } = await callCloudflare(
3294
- base,
3295
- {
3296
- method: "POST",
3297
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/retry`,
3298
- },
3299
- ctx,
3300
- );
3301
- return projectPagesDeployment(result);
3302
- },
3303
- },
3304
- {
3305
- name: "rollback_pages_deployment",
3306
- description: "Promote a previous Pages deployment to production, replacing the currently served production deployment.",
3307
- annotations: { readOnlyHint: false, destructiveHint: true },
3308
- inputSchema: {
3309
- type: "object",
3310
- properties: {
3311
- accountId: scopeProperty("accountId", scope.accountId),
3312
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3313
- deploymentId: { type: "string", minLength: 1, description: "Previous deployment id to promote." },
3314
- },
3315
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
3316
- additionalProperties: false,
3317
- },
3318
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3319
- handler: async (args: JsonRecord, ctx) => {
3320
- const { result } = await callCloudflare(
3321
- base,
3322
- {
3323
- method: "POST",
3324
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/rollback`,
3325
- },
3326
- ctx,
3327
- );
3328
- return projectPagesDeployment(result);
3329
- },
3330
- },
3331
- {
3332
- name: "delete_pages_deployment",
3333
- description: "Permanently delete a Pages deployment and its immutable deployment URL.",
3334
- annotations: { readOnlyHint: false, destructiveHint: true },
3335
- inputSchema: {
3336
- type: "object",
3337
- properties: {
3338
- accountId: scopeProperty("accountId", scope.accountId),
3339
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3340
- deploymentId: { type: "string", minLength: 1, description: "Deployment id to delete." },
3341
- },
3342
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
3343
- additionalProperties: false,
3344
- },
3345
- outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["deleted", "deploymentId"] },
3346
- handler: async (args: JsonRecord, ctx) => {
3347
- const deploymentId = requireString(args, "deploymentId");
3348
- await callCloudflare(
3349
- base,
3350
- {
3351
- method: "DELETE",
3352
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(deploymentId)}`,
3353
- },
3354
- ctx,
3355
- );
3356
- return { deleted: true, deploymentId };
3357
- },
3358
- },
3359
- {
3360
- name: "list_pages_domains",
3361
- description: "List custom domains attached to a Pages project and their validation status.",
3362
- annotations: readOnly,
3363
- inputSchema: {
3364
- type: "object",
3365
- properties: {
3366
- accountId: scopeProperty("accountId", scope.accountId),
3367
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3368
- },
3369
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3370
- additionalProperties: false,
3371
- },
3372
- outputSchema: listOutputSchema("domains", OPEN_OBJECT_OUTPUT_SCHEMA),
3373
- handler: async (args: JsonRecord, ctx) => {
3374
- const { result } = await callCloudflare(
3375
- base,
3376
- {
3377
- method: "GET",
3378
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
3379
- },
3380
- ctx,
3381
- );
3382
- return { domains: asArray(result).map(projectPagesDomain) };
3383
- },
3384
- },
3385
- {
3386
- name: "add_pages_domain",
3387
- description: "Attach a custom domain to a Pages project. DNS ownership and validation still apply.",
3388
- annotations: { readOnlyHint: false },
3389
- inputSchema: {
3390
- type: "object",
3391
- properties: {
3392
- accountId: scopeProperty("accountId", scope.accountId),
3393
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3394
- domain: { type: "string", minLength: 1, description: "Fully qualified custom domain to attach." },
3395
- },
3396
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "domain"],
3397
- additionalProperties: false,
3398
- },
3399
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3400
- handler: async (args: JsonRecord, ctx) => {
3401
- const { result } = await callCloudflare(
3402
- base,
3403
- {
3404
- method: "POST",
3405
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
3406
- body: { name: requireString(args, "domain") },
3407
- },
3408
- ctx,
3409
- );
3410
- return projectPagesDomain(result);
3411
- },
3412
- },
3413
- {
3414
- name: "delete_pages_domain",
3415
- description: "Detach a custom domain from a Pages project.",
3416
- annotations: { readOnlyHint: false, destructiveHint: true },
3417
- inputSchema: {
3418
- type: "object",
3419
- properties: {
3420
- accountId: scopeProperty("accountId", scope.accountId),
3421
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3422
- domain: { type: "string", minLength: 1, description: "Custom domain to detach." },
3423
- },
3424
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "domain"],
3425
- additionalProperties: false,
3426
- },
3427
- outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, domain: { type: "string" } }, required: ["deleted", "domain"] },
3428
- handler: async (args: JsonRecord, ctx) => {
3429
- const domain = requireString(args, "domain");
3430
- await callCloudflare(
3431
- base,
3432
- {
3433
- method: "DELETE",
3434
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains/${encodePathSegment(domain)}`,
3435
- },
3436
- ctx,
3437
- );
3438
- return { deleted: true, domain };
3439
- },
3440
- },
3441
- {
3442
- name: "purge_pages_build_cache",
3443
- description: "Clear a Pages project's build cache so its next deployment rebuilds dependencies and artifacts from scratch.",
3444
- annotations: { readOnlyHint: false, destructiveHint: true },
3445
- inputSchema: {
3446
- type: "object",
3447
- properties: {
3448
- accountId: scopeProperty("accountId", scope.accountId),
3449
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
3450
- },
3451
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3452
- additionalProperties: false,
3453
- },
3454
- outputSchema: { type: "object", properties: { purged: { type: "boolean" } }, required: ["purged"] },
3455
- handler: async (args: JsonRecord, ctx) => {
3456
- await callCloudflare(
3457
- base,
3458
- {
3459
- method: "POST",
3460
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/purge_build_cache`,
3461
- },
3462
- ctx,
3463
- );
3464
- return { purged: true };
3465
- },
3466
- },
3467
- {
3468
- name: "delete_pages_project",
3469
- description: "Permanently delete a Pages project, its deployments, and project configuration.",
3470
- annotations: { readOnlyHint: false, destructiveHint: true },
3471
- inputSchema: {
3472
- type: "object",
3473
- properties: {
3474
- accountId: scopeProperty("accountId", scope.accountId),
3475
- projectName: { type: "string", minLength: 1, description: "Pages project name to delete." },
3476
- },
3477
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3478
- additionalProperties: false,
3479
- },
3480
- outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, projectName: { type: "string" } }, required: ["deleted", "projectName"] },
3481
- handler: async (args: JsonRecord, ctx) => {
3482
- const projectName = requireString(args, "projectName");
3483
- await callCloudflare(
3484
- base,
3485
- {
3486
- method: "DELETE",
3487
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(projectName)}`,
3488
- },
3489
- ctx,
3490
- );
3491
- return { deleted: true, projectName };
3492
- },
3493
- },
3494
- {
3495
- // Additive: brings a record into being and destroys nothing, so
3496
- // `destructiveHint` stays unset. `readOnlyHint: false` already routes it
3497
- // through call_destructive_tool.
3498
- name: "create_dns_record",
3499
- description:
3500
- "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.",
3501
- annotations: { readOnlyHint: false },
3502
- inputSchema: {
3503
- type: "object",
3504
- properties: {
3505
- zoneId: scopeProperty("zoneId", scope.zoneId),
3506
- type: {
3507
- type: "string",
3508
- enum: [...CLOUDFLARE_CONTENT_DNS_RECORD_TYPES],
3509
- description: "Record type.",
3510
- },
3511
- name: {
3512
- type: "string",
3513
- description:
3514
- "Record name. Use the apex domain for the root, or a fully qualified subdomain, e.g. www.example.com.",
3515
- },
3516
- content: {
3517
- type: "string",
3518
- description:
3519
- "Record value: an IPv4 address for A, IPv6 for AAAA, a hostname for CNAME/MX/NS, or the text body for TXT.",
3520
- },
3521
- ttl: {
3522
- type: "integer",
3523
- minimum: 1,
3524
- maximum: 86400,
3525
- description:
3526
- "Time to live in seconds. 1 means automatic, which is what a proxied record must use; any other value must be at least 60 (30 on Enterprise zones). Defaults to 1.",
3527
- },
3528
- proxied: {
3529
- type: "boolean",
3530
- description:
3531
- "Route through Cloudflare's proxy. Only A, AAAA, and CNAME records are proxiable. Defaults to false.",
3532
- },
3533
- priority: {
3534
- type: "integer",
3535
- minimum: 0,
3536
- maximum: 65535,
3537
- description: "Mail-server preference. MX records only.",
3538
- },
3539
- comment: {
3540
- type: "string",
3541
- description: "Operator-facing note stored with the record.",
3542
- },
3543
- tags: {
3544
- type: "array",
3545
- items: { type: "string" },
3546
- description: "Custom tags, available on paid plans.",
3547
- },
3548
- },
3549
- required: [
3550
- ...scopeRequired("zoneId", scope.zoneId),
3551
- "type",
3552
- "name",
3553
- "content",
3554
- ],
3555
- additionalProperties: false,
3556
- },
3557
- outputSchema: DNS_RECORD_SCHEMA,
3558
- handler: async (args: JsonRecord, ctx) => {
3559
- const { result } = await callCloudflare(
3560
- base,
3561
- {
3562
- method: "POST",
3563
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
3564
- body: {
3565
- type: args["type"],
3566
- name: args["name"],
3567
- content: args["content"],
3568
- ttl: optionalNumber(args, "ttl") ?? 1,
3569
- ...(args["proxied"] !== undefined
3570
- ? { proxied: args["proxied"] }
3571
- : {}),
3572
- ...(args["priority"] !== undefined
3573
- ? { priority: args["priority"] }
3574
- : {}),
3575
- ...(args["comment"] !== undefined
3576
- ? { comment: args["comment"] }
3577
- : {}),
3578
- ...(args["tags"] !== undefined ? { tags: args["tags"] } : {}),
3579
- },
3580
- },
3581
- ctx,
3582
- );
3583
- return projectDnsRecord(result);
3584
- },
3585
- },
3586
- {
3587
- // Destructive: it overwrites what a record already resolves to.
3588
- name: "update_dns_record",
3589
- description:
3590
- "Update fields on an existing DNS record. Only the supplied fields change; everything else keeps its current value. Changing content on a live record repoints traffic immediately.",
3591
- annotations: { readOnlyHint: false, destructiveHint: true },
3592
- inputSchema: {
3593
- type: "object",
3594
- properties: {
3595
- zoneId: scopeProperty("zoneId", scope.zoneId),
3596
- recordId: {
3597
- type: "string",
3598
- description: "DNS record id, from list_dns_records.",
3599
- },
3600
- type: {
3601
- type: "string",
3602
- enum: [...CLOUDFLARE_CONTENT_DNS_RECORD_TYPES],
3603
- description:
3604
- "Record type. Send it whenever content changes; Cloudflare treats type and content as a pair.",
3605
- },
3606
- name: { type: "string", description: "Fully qualified record name." },
3607
- content: { type: "string", description: "New record value." },
3608
- ttl: {
3609
- type: "integer",
3610
- minimum: 1,
3611
- maximum: 86400,
3612
- description:
3613
- "Seconds; 1 means automatic, otherwise at least 60 (30 on Enterprise zones).",
3614
- },
3615
- proxied: {
3616
- type: "boolean",
3617
- description:
3618
- "Route through Cloudflare's proxy. Only A, AAAA, and CNAME records are proxiable, and a proxied record must use ttl 1.",
3619
- },
3620
- priority: {
3621
- type: "integer",
3622
- minimum: 0,
3623
- maximum: 65535,
3624
- description: "Mail-server preference. MX records only.",
3625
- },
3626
- comment: {
3627
- type: "string",
3628
- description:
3629
- "Operator-facing note stored with the record. Replaces the existing note.",
3630
- },
3631
- tags: {
3632
- type: "array",
3633
- items: { type: "string" },
3634
- description:
3635
- "Custom tags, available on paid plans. Replaces the existing tag set rather than adding to it.",
3636
- },
3637
- },
3638
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
3639
- additionalProperties: false,
3640
- },
3641
- outputSchema: DNS_RECORD_SCHEMA,
3642
- handler: async (args: JsonRecord, ctx) => {
3643
- const body: JsonRecord = {};
3644
- for (const key of [
3645
- "type",
3646
- "name",
3647
- "content",
3648
- "ttl",
3649
- "proxied",
3650
- "priority",
3651
- "comment",
3652
- "tags",
3653
- ]) {
3654
- if (args[key] !== undefined) body[key] = args[key];
3655
- }
3656
- if (Object.keys(body).length === 0) {
3657
- throw new ConnectorCallError(
3658
- "invalid_args",
3659
- "update_dns_record needs at least one field to change besides zoneId and recordId.",
3660
- {
3661
- validation: {
3662
- issues: [
3663
- {
3664
- path: "/",
3665
- code: "anyOf",
3666
- expected: "at least one of type, name, content, ttl, proxied, priority, comment, tags",
3667
- },
3668
- ],
3669
- },
3670
- },
3671
- );
3672
- }
3673
- const { result } = await callCloudflare(
3674
- base,
3675
- {
3676
- method: "PATCH",
3677
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(
3678
- String(args["recordId"]),
3679
- )}`,
3680
- body,
3681
- },
3682
- ctx,
3683
- );
3684
- return projectDnsRecord(result);
3685
- },
3686
- },
3687
- {
3688
- name: "delete_dns_record",
3689
- description:
3690
- "Delete a DNS record by id. The record stops resolving immediately and Cloudflare keeps no undo.",
3691
- annotations: { readOnlyHint: false, destructiveHint: true },
3692
- inputSchema: {
3693
- type: "object",
3694
- properties: {
3695
- zoneId: scopeProperty("zoneId", scope.zoneId),
3696
- recordId: {
3697
- type: "string",
3698
- description: "DNS record id, from list_dns_records.",
3699
- },
3700
- },
3701
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
3702
- additionalProperties: false,
3703
- },
3704
- outputSchema: {
3705
- type: "object",
3706
- properties: {
3707
- deleted: { type: "boolean" },
3708
- recordId: { type: "string" },
3709
- },
3710
- required: ["deleted", "recordId"],
3711
- },
3712
- handler: async (args: JsonRecord, ctx) => {
3713
- const recordId = String(args["recordId"]);
3714
- await callCloudflare(
3715
- base,
3716
- {
3717
- method: "DELETE",
3718
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(recordId)}`,
3719
- },
3720
- ctx,
3721
- );
3722
- // Cloudflare answers a delete with `{ "result": { "id": ... } }` and
3723
- // nothing else; the useful acknowledgement is the boolean.
3724
- return { deleted: true, recordId };
3725
- },
3726
- },
3727
- {
3728
- name: "purge_cache",
3729
- description:
3730
- "Purge Cloudflare's edge cache for a zone. Prefer files, tags, hosts, or prefixes; everything discards the entire zone cache and sends every subsequent request to the origin until the cache refills.",
3731
- annotations: { readOnlyHint: false, destructiveHint: true },
3732
- inputSchema: {
3733
- type: "object",
3734
- properties: {
3735
- zoneId: scopeProperty("zoneId", scope.zoneId),
3736
- everything: {
3737
- type: "boolean",
3738
- description:
3739
- "Purge the entire zone cache. Mutually exclusive with the targeted options below, and a real load event for the origin.",
3740
- },
3741
- files: {
3742
- type: "array",
3743
- items: { type: "string" },
3744
- minItems: 1,
3745
- maxItems: 100,
3746
- description:
3747
- "Absolute URLs to purge, e.g. https://example.com/style.css. Up to 100 per request (500 on Enterprise).",
3748
- },
3749
- tags: {
3750
- type: "array",
3751
- items: { type: "string" },
3752
- minItems: 1,
3753
- maxItems: 100,
3754
- description:
3755
- "Cache-Tag values to purge. Up to 100 per request; available on every plan.",
3756
- },
3757
- hosts: {
3758
- type: "array",
3759
- items: { type: "string" },
3760
- minItems: 1,
3761
- maxItems: 100,
3762
- description:
3763
- "Hostnames to purge. Up to 100 per request; available on every plan.",
3764
- },
3765
- prefixes: {
3766
- type: "array",
3767
- items: { type: "string" },
3768
- minItems: 1,
3769
- maxItems: 100,
3770
- description:
3771
- "URL prefixes to purge, e.g. example.com/assets. Up to 100 per request; available on every plan.",
3772
- },
3773
- },
3774
- required: scopeRequired("zoneId", scope.zoneId),
3775
- additionalProperties: false,
3776
- },
3777
- outputSchema: {
3778
- type: "object",
3779
- properties: {
3780
- purged: { type: "boolean" },
3781
- zoneId: { type: "string" },
3782
- scope: {
3783
- type: "string",
3784
- description:
3785
- "Which variant ran: everything, files, tags, hosts, or prefixes.",
3786
- },
3787
- },
3788
- required: ["purged", "zoneId", "scope"],
3789
- },
3790
- handler: async (args: JsonRecord, ctx) => {
3791
- const zoneId = zoneArg(args);
3792
- const targeted = (["files", "tags", "hosts", "prefixes"] as const).filter(
3793
- (key) => Array.isArray(args[key]) && (args[key] as unknown[]).length > 0,
3794
- );
3795
- const everything = args["everything"] === true;
3796
- // Cloudflare's purge body accepts exactly one variant. Refusing here
3797
- // turns a confusing provider 400 into a schema-shaped failure.
3798
- if (everything && targeted.length > 0) {
3799
- throw new ConnectorCallError(
3800
- "invalid_args",
3801
- "purge_cache takes either everything: true or one targeted list, never both.",
3802
- {
3803
- validation: {
3804
- issues: [
3805
- {
3806
- path: "/everything",
3807
- code: "oneOf",
3808
- expected: "everything: true alone, or exactly one of files, tags, hosts, prefixes",
3809
- },
3810
- ],
3811
- },
3812
- },
3813
- );
3814
- }
3815
- if (!everything && targeted.length !== 1) {
3816
- throw new ConnectorCallError(
3817
- "invalid_args",
3818
- targeted.length === 0
3819
- ? "purge_cache needs everything: true or one of files, tags, hosts, or prefixes."
3820
- : `purge_cache takes exactly one targeted list; received ${targeted.join(", ")}.`,
3821
- {
3822
- validation: {
3823
- issues: [
3824
- {
3825
- path: "/",
3826
- code: "oneOf",
3827
- expected: "everything: true, or exactly one of files, tags, hosts, prefixes",
3828
- },
3829
- ],
3830
- },
3831
- },
3832
- );
3833
- }
3834
- const variant = everything ? "everything" : targeted[0]!;
3835
- await callCloudflare(
3836
- base,
3837
- {
3838
- method: "POST",
3839
- path: `/zones/${encodeURIComponent(zoneId)}/purge_cache`,
3840
- body: everything
3841
- ? { purge_everything: true }
3842
- : { [variant]: args[variant] },
3843
- },
3844
- ctx,
3845
- );
3846
- return { purged: true, zoneId, scope: variant };
3847
- },
3848
- },
3849
- ];
3850
- return tools.map((tool) => ({
3851
- ...tool,
3852
- handler: (args, ctx) =>
3853
- tool.handler(args, withAuthentication(ctx, authentication)),
3854
- }));
3855
- }
3856
-
3857
- function usageGuide(
3858
- purpose: string,
3859
- scope: Scoping,
3860
- instructions: string | undefined,
3861
- authentication: CloudflareAuthentication,
3862
- ): string {
3863
- const accountInstructions = instructions?.trim();
3864
- const zoneLine = scope.zoneId
3865
- ? `This connector defaults to zone \`${scope.zoneId}\`; omit \`zoneId\` unless the request names a different domain.`
3866
- : "This connector declares no default zone. Start with `list_zones` (filter by `name`) and carry the returned `id` into every zone-scoped call.";
3867
- const accountLine = scope.accountId
3868
- ? `It defaults to account \`${scope.accountId}\`; omit \`accountId\` unless the request names a different account.`
3869
- : "It declares no default account. `list_accounts` supplies the `accountId` the Workers, KV, R2, and Pages tools need.";
3870
- const authenticationLine =
3871
- authentication === "apiToken"
3872
- ? "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."
3873
- : "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.";
3874
- return `# Cloudflare usage
3875
-
3876
- Account purpose: ${purpose}
3877
-
3878
- - ${zoneLine}
3879
- - ${accountLine}
3880
- - 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.
3881
- - 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.
3882
- - 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.
3883
- - 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.
3884
- - 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.
3885
- - ${authenticationLine}
3886
- - 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.
3887
- - 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\`.
3888
- ${
3889
- accountInstructions
3890
- ? `\n## Account instructions\n\n${accountInstructions}\n`
3891
- : ""
3892
- }`;
3893
- }
3894
-
3895
- /** A maintained Cloudflare REST API connection. */
3896
- export function cloudflare(id: string, options: CloudflareOptions): Connector {
3897
- const purpose = options.purpose.trim();
3898
- if (!purpose) {
3899
- throw new Error("cloudflare() requires a non-empty account purpose.");
3900
- }
3901
- const maxConcurrency = options.maxConcurrency ?? 6;
3902
- if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
3903
- throw new Error("cloudflare() maxConcurrency must be a positive integer.");
3904
- }
3905
- const authentication = options.authentication ?? "apiToken";
3906
- if (authentication !== "apiToken" && authentication !== "globalApiKey") {
3907
- throw new Error(
3908
- 'cloudflare() authentication must be "apiToken" or "globalApiKey".',
3909
- );
3910
- }
3911
- const scope: Scoping = {
3912
- base: options.baseUrl?.trim() || CLOUDFLARE_API_BASE,
3913
- accountId: options.accountId?.trim() || undefined,
3914
- zoneId: options.zoneId?.trim() || undefined,
3915
- };
3916
- return api(id, {
3917
- title: options.title ?? "Cloudflare",
3918
- description: `Cloudflare control-plane access for zones, DNS, Workers, KV, R2, Pages, media, email, and other v4 APIs — ${purpose}`,
3919
- credential: credentialConfig(authentication, options.credential),
3920
- callAdmission: admissionPolicy(maxConcurrency),
3921
- usageGuide: usageGuide(
3922
- purpose,
3923
- scope,
3924
- options.instructions,
3925
- authentication,
3926
- ),
3927
- // The schemas are hand-written and closed; a schema that cannot be
3928
- // enforced is a bug in this file, not input to pass through.
3929
- strictValidation: true,
3930
- ...(options.maxResultBytes !== undefined
3931
- ? { maxResultBytes: options.maxResultBytes }
3932
- : {}),
3933
- tools: buildTools(scope, authentication),
3934
- ...(authentication === "apiToken"
3935
- ? {
3936
- async testCredential(value: string, ctx: ConnectorContext) {
3937
- try {
3938
- const { result } = await callCloudflare(
3939
- scope.base,
3940
- { method: "GET", path: "/user/tokens/verify" },
3941
- withAuthentication(
3942
- {
3943
- ...ctx,
3944
- credential: {
3945
- get: async () => value,
3946
- getAll: async () => ({ value }),
3947
- },
3948
- },
3949
- authentication,
3950
- ),
3951
- );
3952
- const status = asRecord(result)["status"];
3953
- return status === "active"
3954
- ? { ok: true, message: "Token verified: active." }
3955
- : { ok: false, message: `Token status is "${String(status)}".` };
3956
- } catch (error) {
3957
- return {
3958
- ok: false,
3959
- message: error instanceof Error ? error.message : String(error),
3960
- };
3961
- }
3962
- },
3963
- }
3964
- : {
3965
- async testCredentials(
3966
- values: Record<string, string>,
3967
- ctx: ConnectorContext,
3968
- ) {
3969
- try {
3970
- const { result } = await callCloudflare(
3971
- scope.base,
3972
- { method: "GET", path: "/user" },
3973
- withAuthentication(
3974
- {
3975
- ...ctx,
3976
- credential: {
3977
- get: async (field?: string) =>
3978
- field ? values[field] ?? null : null,
3979
- getAll: async () => values,
3980
- },
3981
- },
3982
- authentication,
3983
- ),
3984
- );
3985
- const email = asRecord(result)["email"];
3986
- return {
3987
- ok: true,
3988
- message: `Global API Key verified for ${String(email)}.`,
3989
- };
3990
- } catch (error) {
3991
- return {
3992
- ok: false,
3993
- message: error instanceof Error ? error.message : String(error),
3994
- };
3995
- }
3996
- },
3997
- }),
3998
- });
3999
- }