@zackbart/connecta 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +100 -4
  2. package/README.md +4 -0
  3. package/dist/catalog-service.d.ts +20 -13
  4. package/dist/catalog-service.js +123 -116
  5. package/dist/catalog.js +29 -46
  6. package/dist/connector-scope.js +2 -7
  7. package/dist/connectors/api.d.ts +4 -16
  8. package/dist/connectors/api.js +19 -46
  9. package/dist/connectors/guarded-fetch.d.ts +9 -23
  10. package/dist/connectors/guarded-fetch.js +38 -76
  11. package/dist/connectors/remote-mcp.js +36 -79
  12. package/dist/errors.d.ts +6 -27
  13. package/dist/errors.js +8 -5
  14. package/dist/execute.d.ts +24 -22
  15. package/dist/execute.js +98 -145
  16. package/dist/executor-result.d.ts +1 -0
  17. package/dist/executor-result.js +4 -11
  18. package/dist/executors/quickjs-child.js +1 -3
  19. package/dist/executors/quickjs-runtime.js +1 -3
  20. package/dist/executors/quickjs.js +1 -3
  21. package/dist/index.js +27 -57
  22. package/dist/invocation.js +114 -178
  23. package/dist/meta-tools.d.ts +15 -28
  24. package/dist/meta-tools.js +33 -89
  25. package/dist/providers/cloudflare.d.ts +2 -18
  26. package/dist/providers/cloudflare.js +1460 -2451
  27. package/dist/providers/linear.d.ts +4 -41
  28. package/dist/providers/linear.js +8 -39
  29. package/dist/providers/mixpanel.d.ts +3 -25
  30. package/dist/providers/mixpanel.js +7 -22
  31. package/dist/providers/notion.d.ts +1 -15
  32. package/dist/providers/notion.js +44 -173
  33. package/dist/providers/revenuecat.d.ts +4 -57
  34. package/dist/providers/revenuecat.js +10 -93
  35. package/dist/providers/stripe.d.ts +1 -12
  36. package/dist/providers/stripe.js +7 -45
  37. package/dist/registry.d.ts +16 -34
  38. package/dist/registry.js +18 -103
  39. package/dist/result-shapes.d.ts +13 -0
  40. package/dist/result-shapes.js +331 -0
  41. package/dist/routes/mcp.js +1 -1
  42. package/dist/routes/oauth.js +3 -3
  43. package/dist/routes/shared.d.ts +15 -15
  44. package/dist/routes/shared.js +1 -3
  45. package/dist/skills.js +3 -3
  46. package/dist/timeout.d.ts +8 -7
  47. package/dist/timeout.js +47 -38
  48. package/dist/types.d.ts +3 -3
  49. package/dist/ui.d.ts +1 -25
  50. package/dist/ui.js +18 -45
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/documentation/architecture.md +5 -2
  54. package/documentation/call-admission.md +1 -1
  55. package/documentation/cloudflare.md +1 -1
  56. package/documentation/code-mode.md +13 -13
  57. package/documentation/connectors.md +34 -1
  58. package/documentation/linear.md +1 -1
  59. package/documentation/meta-tools.md +17 -3
  60. package/documentation/mixpanel.md +1 -1
  61. package/documentation/notion.md +1 -1
  62. package/documentation/operations.md +20 -15
  63. package/documentation/provider-conventions.md +1 -1
  64. package/documentation/revenuecat.md +1 -1
  65. package/documentation/stripe.md +1 -1
  66. package/documentation/upgrading.md +24 -4
  67. package/ethos.md +74 -120
  68. package/package.json +3 -4
  69. package/templates/node/package.json +1 -1
  70. package/documentation/code-first-exploration.md +0 -292
  71. package/documentation/mcp-2026-07-28.md +0 -46
  72. package/documentation/mcp-ui-design.md +0 -382
  73. package/documentation/program-ui-read-calls.md +0 -213
  74. package/documentation/provider-audit.md +0 -198
  75. package/documentation/rich-output-design.md +0 -211
@@ -1,30 +1,10 @@
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
- */
1
+ /** See documentation/cloudflare.md#no-sdk-on-purpose. */
18
2
  import { api } from "../connectors/api.js";
19
- import { guardedFetch, } from "../connectors/guarded-fetch.js";
3
+ import { guardedFetch, retryAfterMs, } from "../connectors/guarded-fetch.js";
20
4
  import { ConnectorCallError } from "../errors.js";
21
5
  /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
22
6
  export const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
23
- /**
24
- * Every DNS record type the records API accepts, for filtering a list.
25
- * Enumerated in the schema so an agent picks a legal type without reading
26
- * Cloudflare's documentation.
27
- */
7
+ /** See documentation/cloudflare.md#dns-record-types. */
28
8
  export const CLOUDFLARE_DNS_RECORD_TYPES = [
29
9
  "A",
30
10
  "AAAA",
@@ -48,19 +28,7 @@ export const CLOUDFLARE_DNS_RECORD_TYPES = [
48
28
  "TXT",
49
29
  "URI",
50
30
  ];
51
- /**
52
- * The record types whose value is a single `content` string — the eight this
53
- * connection can create and update.
54
- *
55
- * The other thirteen (CAA, CERT, DNSKEY, DS, HTTPS, LOC, NAPTR, SMIMEA, SRV,
56
- * SSHFP, SVCB, TLSA, URI) carry a per-type structured `data` object instead,
57
- * each with its own field set. Accepting them here would mean either a
58
- * free-form `data` passthrough — exactly the untyped `{}` this connection
59
- * exists to avoid — or thirteen more hand-written schemas for record types
60
- * that are rare in the day-to-day work this surface is for. They remain fully
61
- * readable and filterable; only the named create/update tools omit them. The
62
- * guarded raw mutation tool remains available for their documented bodies.
63
- */
31
+ /** Content-valued types only; see documentation/cloudflare.md#dns-record-types. */
64
32
  export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
65
33
  "A",
66
34
  "AAAA",
@@ -71,13 +39,7 @@ export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
71
39
  "PTR",
72
40
  "TXT",
73
41
  ];
74
- /**
75
- * Cloudflare documents a global limit of 1,200 requests per five minutes per
76
- * user, counted cumulatively across the dashboard, API keys, and API tokens.
77
- * The budget mirrors that window; `maxConcurrency` is the part that actually
78
- * protects a shared token, because a single `execute_code` program can fan out
79
- * far faster than the window notices.
80
- */
42
+ /** See documentation/cloudflare.md#rate-limits. */
81
43
  function admissionPolicy(maxConcurrency) {
82
44
  return {
83
45
  rules: [
@@ -146,6 +108,9 @@ function asRecord(value) {
146
108
  function asArray(value) {
147
109
  return Array.isArray(value) ? value : [];
148
110
  }
111
+ function compact(value) {
112
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
113
+ }
149
114
  /** Flatten Cloudflare's error array (and any nested chain) into one line. */
150
115
  function describeErrors(errors) {
151
116
  const parts = [];
@@ -180,33 +145,15 @@ function errorCodes(errors) {
180
145
  * key/email headers. These arrive on HTTP 400, so status alone would misfile
181
146
  * them as an argument problem the agent could repair.
182
147
  *
183
- * Provenance matters here: Cloudflare publishes no official code table, so this
184
- * set comes from community reports and observed responses rather than
185
- * documentation. It is a narrow, well-attested list, not an authoritative one —
186
- * which is why status is the primary signal and these six only rescue the 400s
187
- * status gets wrong.
188
- *
189
148
  * Deliberately excludes 10000. Cloudflare returns 10000 for "Authentication
190
149
  * error" but has also been observed reusing it as a generic validation code
191
150
  * ("domain_name is required", "Invalid pagination cursor"), so routing on it
192
151
  * would risk telling an agent its token was broken when its arguments were.
193
152
  * Genuine 10000 auth failures arrive with 401 or 403 and are caught by status.
194
153
  *
195
- * All of these route to `auth_required`, whose recovery mode resolves to
196
- * `operator_config` because this connection declares an operator-managed
197
- * credential rather than an OAuth flow.
154
+ * See documentation/cloudflare.md#typed-failures for provenance and routing.
198
155
  */
199
156
  const AUTH_ERROR_CODES = new Set([1001, 6003, 6111, 9103, 9106, 9107]);
200
- /** Seconds in a `retry-after` header, converted to the milliseconds the core wants. */
201
- function retryAfterMs(headers) {
202
- const raw = headers.get("retry-after");
203
- if (!raw)
204
- return undefined;
205
- const seconds = Number(raw.trim());
206
- if (!Number.isFinite(seconds) || seconds < 0)
207
- return undefined;
208
- return Math.trunc(seconds * 1000);
209
- }
210
157
  /**
211
158
  * Turn a failed Cloudflare response into a typed connector failure.
212
159
  *
@@ -218,9 +165,7 @@ function retryAfterMs(headers) {
218
165
  function failureFor(status, headers, errors) {
219
166
  const detail = describeErrors(errors);
220
167
  const codes = errorCodes(errors);
221
- // 429 is checked before the auth codes on purpose: Cloudflare reuses the
222
- // generic 10000 code on throttled responses too, and reading a rate limit as
223
- // an auth failure would tell an agent to stop when it should wait.
168
+ // Ordering rationale: documentation/cloudflare.md#typed-failures.
224
169
  if (status === 429) {
225
170
  const wait = retryAfterMs(headers);
226
171
  return new ConnectorCallError("rate_limited", `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.`,
@@ -235,10 +180,7 @@ function failureFor(status, headers, errors) {
235
180
  if (status === 400 || status === 409 || status === 422) {
236
181
  return new ConnectorCallError("invalid_args", `Cloudflare rejected the request (HTTP ${status}). ${detail}`);
237
182
  }
238
- // Cloudflare's 404 is an honest absence: a token that may not touch a
239
- // resource is refused with 401 or 403 above, not hidden behind a miss, so
240
- // this is the unambiguous case `not_found` exists for — re-address, do not
241
- // retry and do not re-authorize.
183
+ // 404 rationale: documentation/cloudflare.md#typed-failures.
242
184
  if (status === 404) {
243
185
  return new ConnectorCallError("not_found", `Cloudflare found no such resource (HTTP 404). ${detail} Confirm the zone or account id with list_zones or list_accounts.`);
244
186
  }
@@ -257,12 +199,8 @@ function failureFor(status, headers, errors) {
257
199
  * the deployment set, so the caller was never going to see it whole.
258
200
  */
259
201
  const CLOUDFLARE_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
260
- const AUTHENTICATION_CONTEXT = Symbol("cloudflareAuthentication");
261
- function withAuthentication(ctx, authentication) {
262
- return { ...ctx, [AUTHENTICATION_CONTEXT]: authentication };
263
- }
264
- async function readAuthenticationHeaders(ctx) {
265
- if (ctx[AUTHENTICATION_CONTEXT] === "globalApiKey") {
202
+ async function readAuthenticationHeaders(ctx, authentication) {
203
+ if (authentication === "globalApiKey") {
266
204
  const values = await ctx.credential?.getAll();
267
205
  const email = values?.["email"];
268
206
  const apiKey = values?.["apiKey"];
@@ -285,37 +223,26 @@ async function readAuthenticationHeaders(ctx) {
285
223
  * What stays here is what only Cloudflare knows: which headers prove identity,
286
224
  * and what a status code means once it arrives.
287
225
  */
288
- function cloudflareTransport(baseUrl) {
226
+ function cloudflareTransport(baseUrl, authentication) {
289
227
  return guardedFetch({
290
228
  provider: "Cloudflare",
291
229
  baseUrl,
292
230
  headers: { Accept: "application/json" },
293
231
  maxResponseBytes: CLOUDFLARE_MAX_RESPONSE_BYTES,
294
- authenticate: (ctx) => readAuthenticationHeaders(ctx),
232
+ authenticate: (ctx) => readAuthenticationHeaders(ctx, authentication),
295
233
  });
296
234
  }
297
235
  async function callCloudflare(send, spec, ctx) {
298
236
  return await send(spec, ctx, async (response) => {
299
- let envelope;
300
- let parseFailure;
301
- try {
302
- envelope = (await response.json());
303
- }
304
- catch (cause) {
305
- // A transport failure is not a parse failure. The connector's byte
306
- // ceiling fires from inside this read and is deliberately non-retryable;
307
- // routing it through the branch below would relabel it as a retryable
308
- // `unavailable` and tell an agent to retry a response that will exceed
309
- // the ceiling every time.
310
- if (cause instanceof ConnectorCallError)
311
- throw cause;
312
- parseFailure = cause;
313
- }
237
+ const parsed = await response.jsonResult();
238
+ const envelope = "value" in parsed
239
+ ? parsed.value
240
+ : undefined;
314
241
  if (envelope === undefined) {
315
242
  // A gateway error page or an empty body, not an envelope: the status is
316
243
  // the only real signal left.
317
244
  throw response.ok
318
- ? new ConnectorCallError("unavailable", "Cloudflare returned a non-JSON body for a successful status.", parseFailure !== undefined ? { cause: parseFailure } : {})
245
+ ? new ConnectorCallError("unavailable", "Cloudflare returned a non-JSON body for a successful status.", "parseError" in parsed ? { cause: parsed.parseError } : {})
319
246
  : failureFor(response.status, response.headers, []);
320
247
  }
321
248
  const errors = Array.isArray(envelope.errors) ? envelope.errors : [];
@@ -335,6 +262,18 @@ async function callCloudflare(send, spec, ctx) {
335
262
  };
336
263
  });
337
264
  }
265
+ async function testCloudflareCredential(send, spec, ctx, success) {
266
+ try {
267
+ const { result } = await callCloudflare(send, spec, ctx);
268
+ return success(result);
269
+ }
270
+ catch (error) {
271
+ return {
272
+ ok: false,
273
+ message: error instanceof Error ? error.message : String(error),
274
+ };
275
+ }
276
+ }
338
277
  function base64FromBytes(bytes) {
339
278
  let binary = "";
340
279
  for (let offset = 0; offset < bytes.length; offset += 0x8000) {
@@ -345,26 +284,25 @@ function base64FromBytes(bytes) {
345
284
  async function callCloudflareContent(send, spec, ctx, responseType) {
346
285
  return await send(spec, ctx, async (response) => {
347
286
  if (!response.ok) {
348
- let errors = [];
287
+ let envelope;
349
288
  try {
350
- const envelope = (await response.json());
351
- if (envelope && Array.isArray(envelope.errors))
352
- errors = envelope.errors;
289
+ const parsed = await response.jsonResult();
290
+ envelope =
291
+ "value" in parsed
292
+ ? parsed.value
293
+ : undefined;
353
294
  }
354
295
  catch {
355
- // A raw or gateway error body has no structured detail to preserve,
356
- // and one past the byte ceiling has none worth reporting over the
357
- // status that already failed this call. Either way the throw below
358
- // classifies the status, so nothing is swallowed into a success.
296
+ // Content reads classify an already-failed status even when its error
297
+ // body exceeds the transport ceiling.
359
298
  }
299
+ const errors = envelope && Array.isArray(envelope.errors) ? envelope.errors : [];
360
300
  throw failureFor(response.status, response.headers, errors);
361
301
  }
362
- const common = {
302
+ const common = compact({
363
303
  contentType: response.headers.get("content-type") ?? "application/octet-stream",
364
- ...(response.headers.get("etag")
365
- ? { etag: response.headers.get("etag") }
366
- : {}),
367
- };
304
+ etag: response.headers.get("etag") ?? undefined,
305
+ });
368
306
  if (responseType === "text") {
369
307
  return { ...common, text: await response.text() };
370
308
  }
@@ -387,33 +325,43 @@ function pageInfo(info) {
387
325
  }
388
326
  const page = typeof info.page === "number" ? info.page : 1;
389
327
  const totalPages = typeof info.total_pages === "number" ? info.total_pages : undefined;
390
- return {
328
+ return compact({
391
329
  page,
392
330
  perPage: typeof info.per_page === "number" ? info.per_page : 0,
393
331
  count: typeof info.count === "number" ? info.count : 0,
394
- ...(typeof info.total_count === "number"
395
- ? { totalCount: info.total_count }
396
- : {}),
397
- ...(totalPages !== undefined ? { totalPages } : {}),
332
+ totalCount: typeof info.total_count === "number" ? info.total_count : undefined,
333
+ totalPages,
398
334
  hasMore: totalPages !== undefined ? page < totalPages : false,
399
- };
335
+ });
336
+ }
337
+ function pagedList(key, project) {
338
+ return (result, resultInfo, raw) => ({
339
+ [key]: raw ? result : asArray(result).map(project),
340
+ page: pageInfo(resultInfo),
341
+ });
342
+ }
343
+ function cursorResult(cursor) {
344
+ return typeof cursor === "string" && cursor !== ""
345
+ ? { nextCursor: cursor }
346
+ : {};
400
347
  }
401
348
  function projectAccount(value) {
402
349
  const account = asRecord(value);
403
- return {
350
+ return compact({
404
351
  id: account["id"],
405
352
  name: account["name"],
406
- ...(account["type"] !== undefined ? { type: account["type"] } : {}),
407
- ...(account["created_on"] !== undefined
408
- ? { createdOn: account["created_on"] }
409
- : {}),
410
- };
353
+ type: account["type"],
354
+ createdOn: account["created_on"],
355
+ });
411
356
  }
412
357
  function projectZone(value) {
413
358
  const zone = asRecord(value);
414
359
  const account = asRecord(zone["account"]);
415
360
  const plan = asRecord(zone["plan"]);
416
- return {
361
+ const nameServers = Array.isArray(zone["name_servers"])
362
+ ? zone["name_servers"]
363
+ : undefined;
364
+ return compact({
417
365
  id: zone["id"],
418
366
  name: zone["name"],
419
367
  status: zone["status"],
@@ -421,196 +369,148 @@ function projectZone(value) {
421
369
  type: zone["type"],
422
370
  accountId: account["id"],
423
371
  accountName: account["name"],
424
- ...(plan["name"] !== undefined ? { plan: plan["name"] } : {}),
425
- ...(Array.isArray(zone["name_servers"])
426
- ? { nameServers: zone["name_servers"] }
427
- : {}),
372
+ plan: plan["name"],
373
+ nameServers,
428
374
  createdOn: zone["created_on"],
429
375
  modifiedOn: zone["modified_on"],
430
- };
376
+ });
431
377
  }
432
378
  function projectDnsRecord(value) {
433
379
  const record = asRecord(value);
434
- return {
380
+ const comment = record["comment"] ? record["comment"] : undefined;
381
+ const tags = Array.isArray(record["tags"]) && record["tags"].length > 0
382
+ ? record["tags"]
383
+ : undefined;
384
+ return compact({
435
385
  id: record["id"],
436
386
  name: record["name"],
437
387
  type: record["type"],
438
388
  content: record["content"],
439
389
  ttl: record["ttl"],
440
- ...(record["proxied"] !== undefined
441
- ? { proxied: record["proxied"] }
442
- : {}),
443
- ...(record["priority"] !== undefined
444
- ? { priority: record["priority"] }
445
- : {}),
446
- ...(record["comment"] ? { comment: record["comment"] } : {}),
447
- ...(Array.isArray(record["tags"]) && record["tags"].length > 0
448
- ? { tags: record["tags"] }
449
- : {}),
390
+ proxied: record["proxied"],
391
+ priority: record["priority"],
392
+ comment,
393
+ tags,
450
394
  createdOn: record["created_on"],
451
395
  modifiedOn: record["modified_on"],
452
- };
396
+ });
453
397
  }
454
398
  function projectWorkerScript(value) {
455
399
  const script = asRecord(value);
456
- return {
400
+ return compact({
457
401
  id: script["id"],
458
402
  createdOn: script["created_on"],
459
403
  modifiedOn: script["modified_on"],
460
- ...(script["usage_model"] !== undefined
461
- ? { usageModel: script["usage_model"] }
462
- : {}),
463
- };
404
+ usageModel: script["usage_model"],
405
+ });
464
406
  }
465
407
  function projectKvNamespace(value) {
466
408
  const namespace = asRecord(value);
467
- return {
409
+ return compact({
468
410
  id: namespace["id"],
469
411
  title: namespace["title"],
470
- ...(namespace["supports_url_encoding"] !== undefined
471
- ? { supportsUrlEncoding: namespace["supports_url_encoding"] }
472
- : {}),
473
- };
412
+ supportsUrlEncoding: namespace["supports_url_encoding"],
413
+ });
474
414
  }
475
415
  function projectR2Bucket(value) {
476
416
  const bucket = asRecord(value);
477
- return {
417
+ return compact({
478
418
  name: bucket["name"],
479
- ...(bucket["location"] !== undefined
480
- ? { location: bucket["location"] }
481
- : {}),
482
- ...(bucket["storage_class"] !== undefined
483
- ? { storageClass: bucket["storage_class"] }
484
- : {}),
485
- ...(bucket["jurisdiction"] !== undefined
486
- ? { jurisdiction: bucket["jurisdiction"] }
487
- : {}),
488
- ...(bucket["creation_date"] !== undefined
489
- ? { creationDate: bucket["creation_date"] }
490
- : {}),
491
- };
419
+ location: bucket["location"],
420
+ storageClass: bucket["storage_class"],
421
+ jurisdiction: bucket["jurisdiction"],
422
+ creationDate: bucket["creation_date"],
423
+ });
492
424
  }
493
425
  function projectR2Object(value) {
494
426
  const object = asRecord(value);
495
- return {
427
+ return compact({
496
428
  key: object["key"],
497
429
  size: object["size"],
498
430
  etag: object["etag"],
499
431
  lastModified: object["last_modified"],
500
- ...(object["storage_class"] !== undefined
501
- ? { storageClass: object["storage_class"] }
502
- : {}),
503
- ...(object["http_metadata"] !== undefined
504
- ? { httpMetadata: object["http_metadata"] }
505
- : {}),
506
- ...(object["custom_metadata"] !== undefined
507
- ? { customMetadata: object["custom_metadata"] }
508
- : {}),
509
- };
432
+ storageClass: object["storage_class"],
433
+ httpMetadata: object["http_metadata"],
434
+ customMetadata: object["custom_metadata"],
435
+ });
510
436
  }
511
437
  function projectKvKey(value) {
512
438
  const key = asRecord(value);
513
- return {
439
+ return compact({
514
440
  name: key["name"],
515
- ...(key["expiration"] !== undefined ? { expiration: key["expiration"] } : {}),
516
- ...(key["metadata"] !== undefined ? { metadata: key["metadata"] } : {}),
517
- };
441
+ expiration: key["expiration"],
442
+ metadata: key["metadata"],
443
+ });
518
444
  }
519
445
  function projectWorkerDeployment(value) {
520
446
  const deployment = asRecord(value);
521
- return {
447
+ return compact({
522
448
  id: deployment["id"],
523
- ...(deployment["created_on"] !== undefined
524
- ? { createdOn: deployment["created_on"] }
525
- : {}),
526
- ...(deployment["source"] !== undefined ? { source: deployment["source"] } : {}),
527
- ...(deployment["strategy"] !== undefined
528
- ? { strategy: deployment["strategy"] }
529
- : {}),
530
- ...(deployment["versions"] !== undefined
531
- ? { versions: deployment["versions"] }
532
- : {}),
533
- };
449
+ createdOn: deployment["created_on"],
450
+ source: deployment["source"],
451
+ strategy: deployment["strategy"],
452
+ versions: deployment["versions"],
453
+ });
534
454
  }
535
455
  function projectPagesDeployment(value) {
536
456
  const deployment = asRecord(value);
537
- return {
457
+ return compact({
538
458
  id: deployment["id"],
539
- ...(deployment["project_name"] !== undefined
540
- ? { projectName: deployment["project_name"] }
541
- : {}),
542
- ...(deployment["environment"] !== undefined
543
- ? { environment: deployment["environment"] }
544
- : {}),
545
- ...(deployment["url"] !== undefined ? { url: deployment["url"] } : {}),
546
- ...(deployment["aliases"] !== undefined ? { aliases: deployment["aliases"] } : {}),
547
- ...(deployment["stage"] !== undefined ? { stage: deployment["stage"] } : {}),
548
- ...(deployment["latest_stage"] !== undefined
549
- ? { latestStage: deployment["latest_stage"] }
550
- : {}),
551
- ...(deployment["created_on"] !== undefined
552
- ? { createdOn: deployment["created_on"] }
553
- : {}),
554
- ...(deployment["modified_on"] !== undefined
555
- ? { modifiedOn: deployment["modified_on"] }
556
- : {}),
557
- };
459
+ projectName: deployment["project_name"],
460
+ environment: deployment["environment"],
461
+ url: deployment["url"],
462
+ aliases: deployment["aliases"],
463
+ stage: deployment["stage"],
464
+ latestStage: deployment["latest_stage"],
465
+ createdOn: deployment["created_on"],
466
+ modifiedOn: deployment["modified_on"],
467
+ });
558
468
  }
559
469
  function projectPagesDomain(value) {
560
470
  const domain = asRecord(value);
561
- return {
471
+ return compact({
562
472
  id: domain["id"],
563
473
  name: domain["name"],
564
- ...(domain["status"] !== undefined ? { status: domain["status"] } : {}),
565
- ...(domain["verification_data"] !== undefined
566
- ? { verificationData: domain["verification_data"] }
567
- : {}),
568
- ...(domain["created_on"] !== undefined
569
- ? { createdOn: domain["created_on"] }
570
- : {}),
571
- };
474
+ status: domain["status"],
475
+ verificationData: domain["verification_data"],
476
+ createdOn: domain["created_on"],
477
+ });
572
478
  }
573
479
  function projectRuleset(value) {
574
480
  const ruleset = asRecord(value);
575
- return {
481
+ return compact({
576
482
  id: ruleset["id"],
577
483
  name: ruleset["name"],
578
484
  kind: ruleset["kind"],
579
485
  phase: ruleset["phase"],
580
- ...(ruleset["description"] !== undefined
581
- ? { description: ruleset["description"] }
582
- : {}),
583
- ...(ruleset["version"] !== undefined ? { version: ruleset["version"] } : {}),
584
- ...(ruleset["last_updated"] !== undefined
585
- ? { lastUpdated: ruleset["last_updated"] }
586
- : {}),
587
- ...(ruleset["rules"] !== undefined ? { rules: ruleset["rules"] } : {}),
588
- };
486
+ description: ruleset["description"],
487
+ version: ruleset["version"],
488
+ lastUpdated: ruleset["last_updated"],
489
+ rules: ruleset["rules"],
490
+ });
589
491
  }
590
492
  function projectPagesProject(value) {
591
493
  const project = asRecord(value);
592
494
  const latest = asRecord(project["latest_deployment"]);
593
- return {
495
+ const domains = Array.isArray(project["domains"])
496
+ ? project["domains"]
497
+ : undefined;
498
+ const latestDeployment = latest["id"] === undefined
499
+ ? undefined
500
+ : compact({
501
+ id: latest["id"],
502
+ environment: latest["environment"],
503
+ url: latest["url"],
504
+ createdOn: latest["created_on"],
505
+ });
506
+ return compact({
594
507
  name: project["name"],
595
508
  subdomain: project["subdomain"],
596
- ...(Array.isArray(project["domains"])
597
- ? { domains: project["domains"] }
598
- : {}),
599
- ...(project["production_branch"] !== undefined
600
- ? { productionBranch: project["production_branch"] }
601
- : {}),
509
+ domains,
510
+ productionBranch: project["production_branch"],
602
511
  createdOn: project["created_on"],
603
- ...(latest["id"] !== undefined
604
- ? {
605
- latestDeployment: {
606
- id: latest["id"],
607
- environment: latest["environment"],
608
- url: latest["url"],
609
- createdOn: latest["created_on"],
610
- },
611
- }
612
- : {}),
613
- };
512
+ latestDeployment,
513
+ });
614
514
  }
615
515
  // --- Schema fragments --------------------------------------------------------
616
516
  const PAGE_OUTPUT_SCHEMA = {
@@ -633,19 +533,7 @@ const RAW_INPUT_PROPERTY = {
633
533
  type: "boolean",
634
534
  description: "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.",
635
535
  };
636
- /**
637
- * Cloudflare's per-page bounds and default differ per endpoint and it rejects
638
- * an out-of-range value with a 400, so each caller passes its own. Encoding
639
- * them in the schema turns a wasted round trip into a local repair — but only
640
- * where the bound is really Cloudflare's. `bounds` records who chose the
641
- * range, because `api()` refuses an out-of-range `perPage` locally and an
642
- * agent deserves to know whether the wall it hit is the API's or ours:
643
- *
644
- * - `"cloudflare"` — the schema's own documented minimum and maximum.
645
- * - `"clamped"` — Cloudflare accepts more; this connection caps it lower.
646
- * - `"undocumented"` — Cloudflare documents no bounds at all for the endpoint,
647
- * so the range is entirely this connection's choice.
648
- */
536
+ /** Bound provenance: documentation/cloudflare.md#where-the-perpage-bounds-come-from. */
649
537
  function pagingInputProperties(minPerPage, maxPerPage, options = {}) {
650
538
  const { defaultPerPage, bounds = "cloudflare" } = options;
651
539
  const defaultNote = defaultPerPage === undefined
@@ -670,16 +558,7 @@ function pagingInputProperties(minPerPage, maxPerPage, options = {}) {
670
558
  },
671
559
  };
672
560
  }
673
- /**
674
- * The second pagination convention, and the schemas say so.
675
- *
676
- * Rulesets, KV keys, and both R2 listings page by cursor rather than by page
677
- * number, because that is how Cloudflare built them. The guide names the four,
678
- * but an agent reading one tool's schema should not have to fetch the guide to
679
- * learn that the `page` object it expected is not coming: absence of
680
- * `nextCursor` is the loop condition here, and it is stated on both ends
681
- * ([#342](https://github.com/zackbart/connecta/issues/342)).
682
- */
561
+ /** Cursor convention: documentation/cloudflare.md#results. */
683
562
  const CURSOR_INPUT_PROPERTY = {
684
563
  type: "string",
685
564
  description: "Opaque cursor from a previous call's nextCursor. This endpoint pages by cursor, not page number.",
@@ -792,19 +671,12 @@ function optionalNumber(args, key) {
792
671
  const value = args[key];
793
672
  return typeof value === "number" ? value : undefined;
794
673
  }
795
- function optionalBoolean(args, key) {
796
- const value = args[key];
797
- return typeof value === "boolean" ? value : undefined;
798
- }
799
674
  function requireString(args, key) {
800
675
  const value = optionalString(args, key);
801
676
  if (value)
802
677
  return value;
803
678
  throw new ConnectorCallError("invalid_args", `${key} must not be blank.`);
804
679
  }
805
- function encodePathSegment(value) {
806
- return encodeURIComponent(value);
807
- }
808
680
  function encodeObjectKey(value) {
809
681
  return value
810
682
  .split("/")
@@ -891,6 +763,13 @@ function headersFromArgs(value) {
891
763
  }
892
764
  return headers;
893
765
  }
766
+ function rawSpec(args) {
767
+ return compact({
768
+ path: cloudflareApiPath(args["path"]),
769
+ query: queryFromArgs(args["query"]),
770
+ headers: headersFromArgs(args["headers"]),
771
+ });
772
+ }
894
773
  function r2Headers(args) {
895
774
  return { "cf-r2-jurisdiction": optionalString(args, "jurisdiction") };
896
775
  }
@@ -964,11 +843,7 @@ const QUERY_INPUT_PROPERTY = {
964
843
  additionalProperties: false,
965
844
  },
966
845
  };
967
- // Shared by all three escape hatches, and the compact renderer inlines it
968
- // three times over. The refused-header list is the connector's boundary rather
969
- // than a fact the caller composes with, so it lives in the usage guide; keeping
970
- // it here pushed cloudflare_api_upload's compact input past the 1,024-byte
971
- // budget ([#342](https://github.com/zackbart/connecta/issues/342)).
846
+ // Header boundary: documentation/cloudflare.md#the-whole-v4-escape-hatch.
972
847
  const HEADERS_INPUT_PROPERTY = {
973
848
  type: "array",
974
849
  description: "Endpoint headers as name/value pairs, e.g. cf-r2-jurisdiction or Range. Connector-owned headers are refused.",
@@ -993,6 +868,65 @@ const R2_BUCKET_NAME_PROPERTY = {
993
868
  maxLength: 64,
994
869
  description: "R2 bucket name.",
995
870
  };
871
+ const SCRIPT_NAME_PROPERTY = {
872
+ type: "string",
873
+ minLength: 1,
874
+ description: "Worker script name from list_worker_scripts.",
875
+ };
876
+ const NAMESPACE_ID_PROPERTY = {
877
+ type: "string",
878
+ minLength: 1,
879
+ description: "KV namespace id from list_kv_namespaces.",
880
+ };
881
+ const PROJECT_NAME_PROPERTY = {
882
+ type: "string",
883
+ minLength: 1,
884
+ description: "Pages project name from list_pages_projects.",
885
+ };
886
+ const PAGES_PROJECT_NAME_PROPERTY = {
887
+ type: "string",
888
+ minLength: 1,
889
+ description: "Pages project name.",
890
+ };
891
+ const DEPLOYMENT_ID_PROPERTY = {
892
+ type: "string",
893
+ minLength: 1,
894
+ description: "Deployment id from list_pages_deployments.",
895
+ };
896
+ const WORKER_DEPLOYMENT_ID_PROPERTY = {
897
+ type: "string",
898
+ minLength: 1,
899
+ description: "Deployment id from list_worker_deployments.",
900
+ };
901
+ const RETRY_DEPLOYMENT_ID_PROPERTY = {
902
+ type: "string",
903
+ minLength: 1,
904
+ description: "Deployment id to retry.",
905
+ };
906
+ const ROLLBACK_DEPLOYMENT_ID_PROPERTY = {
907
+ type: "string",
908
+ minLength: 1,
909
+ description: "Previous deployment id to promote.",
910
+ };
911
+ const DELETE_DEPLOYMENT_ID_PROPERTY = {
912
+ type: "string",
913
+ minLength: 1,
914
+ description: "Deployment id to delete.",
915
+ };
916
+ const DELETE_PROJECT_NAME_PROPERTY = {
917
+ type: "string",
918
+ minLength: 1,
919
+ description: "Pages project name to delete.",
920
+ };
921
+ const SETTING_ID_PROPERTY = {
922
+ type: "string",
923
+ minLength: 1,
924
+ description: "Cloudflare zone setting id, such as ssl, brotli, http3, or min_tls_version.",
925
+ };
926
+ const RECORD_ID_PROPERTY = {
927
+ type: "string",
928
+ description: "DNS record id, from list_dns_records.",
929
+ };
996
930
  const R2_BUCKET_SCHEMA = {
997
931
  type: "object",
998
932
  properties: {
@@ -1017,6 +951,41 @@ const R2_OBJECT_SCHEMA = {
1017
951
  },
1018
952
  required: ["key"],
1019
953
  };
954
+ function cfTool(name, description, annotations, scopeKind, scopeFallback, properties, required, outputSchema, handler) {
955
+ return {
956
+ name,
957
+ description,
958
+ annotations,
959
+ inputSchema: {
960
+ type: "object",
961
+ properties: scopeKind
962
+ ? {
963
+ [scopeKind]: scopeProperty(scopeKind, scopeFallback),
964
+ ...properties,
965
+ }
966
+ : properties,
967
+ required: scopeKind
968
+ ? [...scopeRequired(scopeKind, scopeFallback), ...required]
969
+ : required,
970
+ additionalProperties: false,
971
+ },
972
+ outputSchema,
973
+ handler,
974
+ };
975
+ }
976
+ function getResult(send, request, project = (result) => result) {
977
+ return async (args, ctx) => {
978
+ const { result } = await callCloudflare(send, request(args), ctx);
979
+ return project(result, args);
980
+ };
981
+ }
982
+ function deleteAck(send, key, value, request) {
983
+ return async (args, ctx) => {
984
+ const id = value(args);
985
+ await callCloudflare(send, request(args, id), ctx);
986
+ return { deleted: true, [key]: id };
987
+ };
988
+ }
1020
989
  function buildTools(scope, authentication) {
1021
990
  const { send } = scope;
1022
991
  const zoneArg = (args) => requireScope(args["zoneId"], scope.zoneId, "zoneId");
@@ -1024,2164 +993,1221 @@ function buildTools(scope, authentication) {
1024
993
  const readOnly = { readOnlyHint: true, destructiveHint: false };
1025
994
  const tools = [
1026
995
  authentication === "apiToken"
1027
- ? {
1028
- name: "verify_api_token",
1029
- description: "Verify the configured Cloudflare API token and report its status. Use this first when any other tool fails with an authentication error, to separate a bad token from a missing permission.",
1030
- annotations: readOnly,
1031
- inputSchema: {
1032
- type: "object",
1033
- properties: {},
1034
- required: [],
1035
- additionalProperties: false,
1036
- },
1037
- outputSchema: {
1038
- type: "object",
1039
- properties: {
1040
- id: { type: "string" },
1041
- status: {
1042
- type: "string",
1043
- description: "\"active\" for a usable token.",
1044
- },
1045
- notBefore: { type: "string" },
1046
- expiresOn: { type: "string" },
1047
- },
1048
- required: ["status"],
1049
- },
1050
- handler: async (_args, ctx) => {
1051
- const { result } = await callCloudflare(send, { method: "GET", path: "/user/tokens/verify" }, ctx);
1052
- const token = asRecord(result);
1053
- return {
1054
- id: token["id"],
1055
- status: token["status"],
1056
- ...(token["not_before"] !== undefined
1057
- ? { notBefore: token["not_before"] }
1058
- : {}),
1059
- ...(token["expires_on"] !== undefined
1060
- ? { expiresOn: token["expires_on"] }
1061
- : {}),
1062
- };
1063
- },
1064
- }
1065
- : {
1066
- name: "verify_global_api_key",
1067
- description: "Verify the configured Cloudflare Global API Key and account email by retrieving the authenticated user. Use this first when another tool fails with an authentication error.",
1068
- annotations: readOnly,
1069
- inputSchema: {
1070
- type: "object",
1071
- properties: {},
1072
- required: [],
1073
- additionalProperties: false,
1074
- },
1075
- outputSchema: {
1076
- type: "object",
1077
- properties: {
1078
- id: { type: "string" },
1079
- email: { type: "string" },
1080
- status: {
1081
- type: "string",
1082
- description: "\"active\" when Cloudflare accepts the email and key.",
1083
- },
1084
- },
1085
- required: ["email", "status"],
1086
- },
1087
- handler: async (_args, ctx) => {
1088
- const { result } = await callCloudflare(send, { method: "GET", path: "/user" }, ctx);
1089
- const user = asRecord(result);
1090
- return { id: user["id"], email: user["email"], status: "active" };
1091
- },
1092
- },
1093
- {
1094
- name: "cloudflare_api_get",
1095
- description: "Call any GET endpoint under Cloudflare's v4 API with this connector's credential. Prefer a named tool when one exists; this read-only hatch covers the products the named surface does not reach, such as Images, Stream, D1, and Queues.",
1096
- annotations: readOnly,
1097
- inputSchema: {
996
+ ? cfTool("verify_api_token", "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.", readOnly, undefined, undefined, {}, [], {
1098
997
  type: "object",
1099
998
  properties: {
1100
- path: {
1101
- type: "string",
1102
- minLength: 1,
1103
- description: "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.",
1104
- },
1105
- query: QUERY_INPUT_PROPERTY,
1106
- headers: HEADERS_INPUT_PROPERTY,
1107
- responseType: {
999
+ id: { type: "string" },
1000
+ status: {
1108
1001
  type: "string",
1109
- enum: ["json", "text", "base64"],
1110
- description: "How to read a successful response. Defaults to json; use text or base64 for object, log, script, and media downloads.",
1002
+ description: "\"active\" for a usable token.",
1111
1003
  },
1004
+ notBefore: { type: "string" },
1005
+ expiresOn: { type: "string" },
1112
1006
  },
1113
- required: ["path"],
1114
- additionalProperties: false,
1115
- },
1116
- outputSchema: {
1117
- type: "object",
1118
- properties: {
1119
- result: {
1120
- description: "Cloudflare's unprojected result for the endpoint.",
1121
- },
1122
- resultInfo: {
1123
- type: "object",
1124
- description: "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1125
- },
1126
- text: { type: "string", description: "Text response body when responseType is text." },
1127
- base64: { type: "string", description: "Base64 response bytes when responseType is base64." },
1128
- contentType: { type: "string", description: "Response Content-Type for text/base64 reads." },
1129
- etag: { type: "string", description: "Response ETag when Cloudflare supplies one." },
1130
- },
1131
- required: [],
1132
- },
1133
- handler: async (args, ctx) => {
1134
- const query = queryFromArgs(args["query"]);
1135
- const headers = headersFromArgs(args["headers"]);
1136
- const responseType = optionalString(args, "responseType") ?? "json";
1137
- const spec = {
1138
- method: "GET",
1139
- path: cloudflareApiPath(args["path"]),
1140
- ...(query !== undefined ? { query } : {}),
1141
- ...(headers !== undefined ? { headers } : {}),
1142
- };
1143
- if (responseType === "text" || responseType === "base64") {
1144
- return await callCloudflareContent(send, spec, ctx, responseType);
1145
- }
1146
- const { result, resultInfo } = await callCloudflare(send, spec, ctx);
1147
- return {
1148
- result,
1149
- ...(resultInfo !== undefined ? { resultInfo } : {}),
1150
- };
1151
- },
1152
- },
1153
- {
1154
- name: "cloudflare_api_mutate",
1155
- description: "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's credential. The approval-gated write hatch for products the named surface does not reach. No multipart or binary uploads.",
1156
- annotations: { readOnlyHint: false, destructiveHint: true },
1157
- inputSchema: {
1007
+ required: ["status"],
1008
+ }, async (_args, ctx) => {
1009
+ const { result } = await callCloudflare(send, { method: "GET", path: "/user/tokens/verify" }, ctx);
1010
+ const token = asRecord(result);
1011
+ return compact({
1012
+ id: token["id"],
1013
+ status: token["status"],
1014
+ notBefore: token["not_before"],
1015
+ expiresOn: token["expires_on"],
1016
+ });
1017
+ })
1018
+ : cfTool("verify_global_api_key", "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.", readOnly, undefined, undefined, {}, [], {
1158
1019
  type: "object",
1159
1020
  properties: {
1160
- method: {
1161
- type: "string",
1162
- enum: ["POST", "PUT", "PATCH", "DELETE"],
1163
- description: "HTTP mutation method required by the Cloudflare endpoint.",
1164
- },
1165
- path: {
1021
+ id: { type: "string" },
1022
+ email: { type: "string" },
1023
+ status: {
1166
1024
  type: "string",
1167
- minLength: 1,
1168
- description: "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
1169
- },
1170
- query: QUERY_INPUT_PROPERTY,
1171
- headers: HEADERS_INPUT_PROPERTY,
1172
- body: {
1173
- type: ["object", "array", "string", "number", "boolean", "null"],
1174
- description: "JSON request body exactly as documented by Cloudflare. Omit for endpoints with no body.",
1025
+ description: "\"active\" when Cloudflare accepts the email and key.",
1175
1026
  },
1176
1027
  },
1177
- required: ["method", "path"],
1178
- additionalProperties: false,
1179
- },
1180
- outputSchema: {
1181
- type: "object",
1182
- properties: {
1183
- result: {
1184
- description: "Cloudflare's unprojected result for the endpoint.",
1185
- },
1186
- resultInfo: {
1187
- type: "object",
1188
- description: "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1028
+ required: ["email", "status"],
1029
+ }, async (_args, ctx) => {
1030
+ const { result } = await callCloudflare(send, { method: "GET", path: "/user" }, ctx);
1031
+ const user = asRecord(result);
1032
+ return { id: user["id"], email: user["email"], status: "active" };
1033
+ }),
1034
+ cfTool("cloudflare_api_get", "Call any GET endpoint under Cloudflare's v4 API with this connector's credential. Prefer a named tool when one exists; this read-only hatch covers the products the named surface does not reach, such as Images, Stream, D1, and Queues.", readOnly, undefined, undefined, {
1035
+ path: {
1036
+ type: "string",
1037
+ minLength: 1,
1038
+ description: "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.",
1039
+ },
1040
+ query: QUERY_INPUT_PROPERTY,
1041
+ headers: HEADERS_INPUT_PROPERTY,
1042
+ responseType: {
1043
+ type: "string",
1044
+ enum: ["json", "text", "base64"],
1045
+ description: "How to read a successful response. Defaults to json; use text or base64 for object, log, script, and media downloads.",
1046
+ }
1047
+ }, ["path"], {
1048
+ type: "object",
1049
+ properties: {
1050
+ result: {
1051
+ description: "Cloudflare's unprojected result for the endpoint.",
1052
+ },
1053
+ resultInfo: {
1054
+ type: "object",
1055
+ description: "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1056
+ },
1057
+ text: { type: "string", description: "Text response body when responseType is text." },
1058
+ base64: { type: "string", description: "Base64 response bytes when responseType is base64." },
1059
+ contentType: { type: "string", description: "Response Content-Type for text/base64 reads." },
1060
+ etag: { type: "string", description: "Response ETag when Cloudflare supplies one." },
1061
+ },
1062
+ required: [],
1063
+ }, async (args, ctx) => {
1064
+ const raw = rawSpec(args);
1065
+ const responseType = optionalString(args, "responseType") ?? "json";
1066
+ const spec = compact({
1067
+ method: "GET",
1068
+ ...raw,
1069
+ });
1070
+ if (responseType === "text" || responseType === "base64") {
1071
+ return await callCloudflareContent(send, spec, ctx, responseType);
1072
+ }
1073
+ const { result, resultInfo } = await callCloudflare(send, spec, ctx);
1074
+ return compact({
1075
+ result,
1076
+ resultInfo,
1077
+ });
1078
+ }),
1079
+ cfTool("cloudflare_api_mutate", "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's credential. The approval-gated write hatch for products the named surface does not reach. No multipart or binary uploads.", { readOnlyHint: false, destructiveHint: true }, undefined, undefined, {
1080
+ method: {
1081
+ type: "string",
1082
+ enum: ["POST", "PUT", "PATCH", "DELETE"],
1083
+ description: "HTTP mutation method required by the Cloudflare endpoint.",
1084
+ },
1085
+ path: {
1086
+ type: "string",
1087
+ minLength: 1,
1088
+ description: "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
1089
+ },
1090
+ query: QUERY_INPUT_PROPERTY,
1091
+ headers: HEADERS_INPUT_PROPERTY,
1092
+ body: {
1093
+ type: ["object", "array", "string", "number", "boolean", "null"],
1094
+ description: "JSON request body exactly as documented by Cloudflare. Omit for endpoints with no body.",
1095
+ }
1096
+ }, ["method", "path"], {
1097
+ type: "object",
1098
+ properties: {
1099
+ result: {
1100
+ description: "Cloudflare's unprojected result for the endpoint.",
1101
+ },
1102
+ resultInfo: {
1103
+ type: "object",
1104
+ description: "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1105
+ },
1106
+ },
1107
+ required: ["result"],
1108
+ }, async (args, ctx) => {
1109
+ const method = String(args["method"]);
1110
+ const spec = rawSpec(args);
1111
+ const { result, resultInfo } = await callCloudflare(send, compact({
1112
+ method,
1113
+ ...spec,
1114
+ body: args["body"],
1115
+ }), ctx);
1116
+ return compact({
1117
+ result,
1118
+ resultInfo,
1119
+ });
1120
+ }),
1121
+ cfTool("cloudflare_api_upload", "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.", { readOnlyHint: false, destructiveHint: true }, undefined, undefined, {
1122
+ method: {
1123
+ type: "string",
1124
+ enum: ["POST", "PUT"],
1125
+ description: "Upload method the Cloudflare endpoint requires.",
1126
+ },
1127
+ path: {
1128
+ type: "string",
1129
+ minLength: 1,
1130
+ description: "Path below /client/v4, beginning with '/'. No query string.",
1131
+ },
1132
+ query: QUERY_INPUT_PROPERTY,
1133
+ headers: HEADERS_INPUT_PROPERTY,
1134
+ contentType: {
1135
+ type: "string",
1136
+ minLength: 1,
1137
+ description: "Content-Type for a raw text or base64 body. Omit for multipart.",
1138
+ },
1139
+ textBody: {
1140
+ type: "string",
1141
+ description: "Raw UTF-8 body. Exclusive with base64Body and fields/files.",
1142
+ },
1143
+ base64Body: {
1144
+ type: "string",
1145
+ description: "Base64-encoded body bytes. Exclusive with textBody and fields/files.",
1146
+ },
1147
+ fields: {
1148
+ type: "array",
1149
+ description: "String fields of a multipart/form-data request.",
1150
+ items: {
1151
+ type: "object",
1152
+ properties: {
1153
+ name: { type: "string", minLength: 1 },
1154
+ value: { type: "string" },
1155
+ contentType: { type: "string", minLength: 1 },
1156
+ fileName: { type: "string", minLength: 1 },
1189
1157
  },
1158
+ required: ["name", "value"],
1159
+ additionalProperties: false,
1190
1160
  },
1191
- required: ["result"],
1192
- },
1193
- handler: async (args, ctx) => {
1194
- const method = String(args["method"]);
1195
- const query = queryFromArgs(args["query"]);
1196
- const headers = headersFromArgs(args["headers"]);
1197
- const { result, resultInfo } = await callCloudflare(send, {
1198
- method,
1199
- path: cloudflareApiPath(args["path"]),
1200
- ...(query !== undefined ? { query } : {}),
1201
- ...(headers !== undefined ? { headers } : {}),
1202
- ...(args["body"] !== undefined ? { body: args["body"] } : {}),
1203
- }, ctx);
1204
- return {
1205
- result,
1206
- ...(resultInfo !== undefined ? { resultInfo } : {}),
1207
- };
1208
1161
  },
1209
- },
1210
- {
1211
- name: "cloudflare_api_upload",
1212
- description: "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.",
1213
- annotations: { readOnlyHint: false, destructiveHint: true },
1214
- inputSchema: {
1215
- type: "object",
1216
- properties: {
1217
- method: {
1218
- type: "string",
1219
- enum: ["POST", "PUT"],
1220
- description: "Upload method the Cloudflare endpoint requires.",
1221
- },
1222
- path: {
1223
- type: "string",
1224
- minLength: 1,
1225
- description: "Path below /client/v4, beginning with '/'. No query string.",
1226
- },
1227
- query: QUERY_INPUT_PROPERTY,
1228
- headers: HEADERS_INPUT_PROPERTY,
1229
- contentType: {
1230
- type: "string",
1231
- minLength: 1,
1232
- description: "Content-Type for a raw text or base64 body. Omit for multipart.",
1233
- },
1234
- textBody: {
1235
- type: "string",
1236
- description: "Raw UTF-8 body. Exclusive with base64Body and fields/files.",
1237
- },
1238
- base64Body: {
1239
- type: "string",
1240
- description: "Base64-encoded body bytes. Exclusive with textBody and fields/files.",
1162
+ files: {
1163
+ type: "array",
1164
+ description: "Multipart file parts. Each needs exactly one of text or base64.",
1165
+ items: {
1166
+ type: "object",
1167
+ properties: {
1168
+ name: { type: "string", minLength: 1 },
1169
+ fileName: { type: "string", minLength: 1 },
1170
+ contentType: { type: "string", minLength: 1 },
1171
+ text: { type: "string" },
1172
+ base64: { type: "string" },
1241
1173
  },
1242
- fields: {
1243
- type: "array",
1244
- description: "String fields of a multipart/form-data request.",
1245
- items: {
1246
- type: "object",
1247
- properties: {
1248
- name: { type: "string", minLength: 1 },
1249
- value: { type: "string" },
1250
- contentType: { type: "string", minLength: 1 },
1251
- fileName: { type: "string", minLength: 1 },
1252
- },
1253
- required: ["name", "value"],
1254
- additionalProperties: false,
1174
+ required: ["name", "fileName", "contentType"],
1175
+ additionalProperties: false,
1176
+ },
1177
+ }
1178
+ }, ["method", "path"], {
1179
+ type: "object",
1180
+ properties: {
1181
+ result: {
1182
+ description: "Cloudflare's unprojected upload result.",
1183
+ },
1184
+ },
1185
+ required: ["result"],
1186
+ }, async (args, ctx) => {
1187
+ const spec = rawSpec(args);
1188
+ const upload = uploadBody(args);
1189
+ const { result } = await callCloudflare(send, compact({
1190
+ method: String(args["method"]),
1191
+ ...spec,
1192
+ headers: spec.headers !== undefined || upload.headers !== undefined
1193
+ ? { ...spec.headers, ...upload.headers }
1194
+ : undefined,
1195
+ rawBody: upload.rawBody,
1196
+ }), ctx);
1197
+ return { result };
1198
+ }),
1199
+ cfTool("list_accounts", "List Cloudflare accounts this token can see. Supplies the accountId that the Workers, KV, R2, and Pages tools need.", readOnly, undefined, undefined, {
1200
+ name: {
1201
+ type: "string",
1202
+ description: "Filter by exact account name.",
1203
+ },
1204
+ ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1205
+ raw: RAW_INPUT_PROPERTY
1206
+ }, [], listOutputSchema("accounts", ACCOUNT_SCHEMA), async (args, ctx) => {
1207
+ const { result, resultInfo } = await callCloudflare(send, {
1208
+ method: "GET",
1209
+ path: "/accounts",
1210
+ query: {
1211
+ name: optionalString(args, "name"),
1212
+ page: optionalNumber(args, "page"),
1213
+ per_page: optionalNumber(args, "perPage"),
1214
+ },
1215
+ }, ctx);
1216
+ return pagedList("accounts", projectAccount)(result, resultInfo, args["raw"] === true);
1217
+ }),
1218
+ cfTool("list_zones", "List zones (domains) this token can see, with their ids and status. This is the zoneId discovery step for every DNS and cache tool.", readOnly, undefined, undefined, {
1219
+ name: {
1220
+ type: "string",
1221
+ description: "Filter by zone name, e.g. example.com.",
1222
+ },
1223
+ accountId: {
1224
+ type: "string",
1225
+ description: "Restrict to one account. Defaults to every account the token can see.",
1226
+ },
1227
+ status: {
1228
+ type: "string",
1229
+ enum: ["initializing", "pending", "active", "moved"],
1230
+ description: "Filter by zone status.",
1231
+ },
1232
+ ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1233
+ raw: RAW_INPUT_PROPERTY
1234
+ }, [], listOutputSchema("zones", ZONE_SCHEMA), async (args, ctx) => {
1235
+ const { result, resultInfo } = await callCloudflare(send, {
1236
+ method: "GET",
1237
+ path: "/zones",
1238
+ query: {
1239
+ name: optionalString(args, "name"),
1240
+ // Undefaulted on purpose — see documentation/cloudflare.md#scoping.
1241
+ "account.id": optionalString(args, "accountId"),
1242
+ status: optionalString(args, "status"),
1243
+ page: optionalNumber(args, "page"),
1244
+ per_page: optionalNumber(args, "perPage"),
1245
+ },
1246
+ }, ctx);
1247
+ return pagedList("zones", projectZone)(result, resultInfo, args["raw"] === true);
1248
+ }),
1249
+ cfTool("get_zone", "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.", readOnly, "zoneId", scope.zoneId, {
1250
+ raw: RAW_INPUT_PROPERTY
1251
+ }, [], ZONE_SCHEMA, getResult(send, (args) => ({ method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` }), (result, args) => args["raw"] === true ? result : projectZone(result))),
1252
+ // Removed tools: documentation/cloudflare.md#what-the-named-surface-deliberately-leaves-out.
1253
+ cfTool("get_zone_setting", "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.", readOnly, "zoneId", scope.zoneId, {
1254
+ settingId: SETTING_ID_PROPERTY
1255
+ }, ["settingId"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1256
+ method: "GET",
1257
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/settings/${encodeURIComponent(requireString(args, "settingId"))}`,
1258
+ }))),
1259
+ cfTool("update_zone_setting", "Set one editable zone setting. Read it first: allowed value types and plan restrictions differ by setting.", { readOnlyHint: false, destructiveHint: true }, "zoneId", scope.zoneId, {
1260
+ settingId: SETTING_ID_PROPERTY,
1261
+ value: {
1262
+ type: ["string", "number", "boolean", "array"],
1263
+ description: "New setting value in the type returned by get_zone_setting. Arrays must contain strings.",
1264
+ items: { type: "string" },
1265
+ }
1266
+ }, ["settingId", "value"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1267
+ const { result } = await callCloudflare(send, {
1268
+ method: "PATCH",
1269
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/settings/${encodeURIComponent(requireString(args, "settingId"))}`,
1270
+ body: { value: args["value"] },
1271
+ }, ctx);
1272
+ return result;
1273
+ }),
1274
+ cfTool("list_zone_rulesets", "List zone rulesets for WAF, redirects, transforms, cache rules, configuration rules, and other Ruleset Engine phases.", readOnly, "zoneId", scope.zoneId, {
1275
+ perPage: {
1276
+ type: "integer",
1277
+ minimum: 1,
1278
+ maximum: 50,
1279
+ description: "Rulesets per request, 1 to 50.",
1280
+ },
1281
+ cursor: CURSOR_INPUT_PROPERTY
1282
+ }, [], {
1283
+ type: "object",
1284
+ properties: {
1285
+ rulesets: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1286
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1287
+ },
1288
+ required: ["rulesets"],
1289
+ }, async (args, ctx) => {
1290
+ const { result, resultInfo } = await callCloudflare(send, {
1291
+ method: "GET",
1292
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/rulesets`,
1293
+ query: {
1294
+ per_page: optionalNumber(args, "perPage"),
1295
+ cursor: optionalString(args, "cursor"),
1296
+ },
1297
+ }, ctx);
1298
+ const cursor = resultInfo?.cursors?.after;
1299
+ return {
1300
+ rulesets: asArray(result).map(projectRuleset),
1301
+ ...cursorResult(cursor),
1302
+ };
1303
+ }),
1304
+ cfTool("get_zone_ruleset", "Get one zone ruleset including its ordered rules, expressions, actions, parameters, and enabled state.", readOnly, "zoneId", scope.zoneId, {
1305
+ rulesetId: {
1306
+ type: "string",
1307
+ minLength: 1,
1308
+ description: "Ruleset id from list_zone_rulesets.",
1309
+ }
1310
+ }, ["rulesetId"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1311
+ method: "GET",
1312
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/rulesets/${encodeURIComponent(requireString(args, "rulesetId"))}`,
1313
+ }), projectRuleset)),
1314
+ cfTool("list_dns_records", "List DNS records in a zone, filtered by name, type, or content. Returns record ids, which update_dns_record and delete_dns_record require.", readOnly, "zoneId", scope.zoneId, {
1315
+ name: {
1316
+ type: "string",
1317
+ description: "Exact record name, fully qualified, e.g. www.example.com.",
1318
+ },
1319
+ type: {
1320
+ type: "string",
1321
+ enum: [...CLOUDFLARE_DNS_RECORD_TYPES],
1322
+ description: "Filter by record type.",
1323
+ },
1324
+ content: {
1325
+ type: "string",
1326
+ description: "Exact record content, e.g. an IP address.",
1327
+ },
1328
+ order: {
1329
+ type: "string",
1330
+ enum: ["type", "name", "content", "ttl", "proxied"],
1331
+ description: "Sort field.",
1332
+ },
1333
+ direction: {
1334
+ type: "string",
1335
+ enum: ["asc", "desc"],
1336
+ description: "Sort direction for `order`. Defaults to asc.",
1337
+ },
1338
+ // Cloudflare documents 1 to 5,000,000 here with a default of 100; the
1339
+ // ceiling is nominal, so this connection caps it at a page size that
1340
+ // actually returns.
1341
+ ...pagingInputProperties(1, 1000, {
1342
+ defaultPerPage: 100,
1343
+ bounds: "clamped",
1344
+ }),
1345
+ raw: RAW_INPUT_PROPERTY
1346
+ }, [], listOutputSchema("records", DNS_RECORD_SCHEMA), async (args, ctx) => {
1347
+ const { result, resultInfo } = await callCloudflare(send, {
1348
+ method: "GET",
1349
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
1350
+ query: {
1351
+ name: optionalString(args, "name"),
1352
+ type: optionalString(args, "type"),
1353
+ content: optionalString(args, "content"),
1354
+ order: optionalString(args, "order"),
1355
+ direction: optionalString(args, "direction"),
1356
+ page: optionalNumber(args, "page"),
1357
+ per_page: optionalNumber(args, "perPage"),
1358
+ },
1359
+ }, ctx);
1360
+ return pagedList("records", projectDnsRecord)(result, resultInfo, args["raw"] === true);
1361
+ }),
1362
+ cfTool("get_dns_record", "Fetch one DNS record by its record id.", readOnly, "zoneId", scope.zoneId, {
1363
+ recordId: RECORD_ID_PROPERTY,
1364
+ raw: RAW_INPUT_PROPERTY
1365
+ }, ["recordId"], DNS_RECORD_SCHEMA, getResult(send, (args) => ({
1366
+ method: "GET",
1367
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
1368
+ }), (result, args) => args["raw"] === true ? result : projectDnsRecord(result))),
1369
+ cfTool("list_worker_scripts", "List Workers scripts deployed in an account, with their last-modified times.", readOnly, "accountId", scope.accountId, {
1370
+ raw: RAW_INPUT_PROPERTY
1371
+ }, [], listOutputSchema("scripts", {
1372
+ type: "object",
1373
+ properties: {
1374
+ id: { type: "string", description: "Script name." },
1375
+ createdOn: { type: "string" },
1376
+ modifiedOn: { type: "string" },
1377
+ usageModel: { type: "string" },
1378
+ },
1379
+ required: ["id"],
1380
+ }), async (args, ctx) => {
1381
+ const { result, resultInfo } = await callCloudflare(send, {
1382
+ method: "GET",
1383
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
1384
+ }, ctx);
1385
+ return pagedList("scripts", projectWorkerScript)(result, resultInfo, args["raw"] === true);
1386
+ }),
1387
+ cfTool("get_worker_settings", "Get a Worker's compatibility date and flags, bindings, limits, observability, placement, usage model, and other script settings.", readOnly, "accountId", scope.accountId, {
1388
+ scriptName: SCRIPT_NAME_PROPERTY
1389
+ }, ["scriptName"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1390
+ method: "GET",
1391
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(requireString(args, "scriptName"))}/settings`,
1392
+ }))),
1393
+ cfTool("list_worker_deployments", "List deployments of a Worker script, including version traffic allocations and deployment strategy.", readOnly, "accountId", scope.accountId, {
1394
+ scriptName: SCRIPT_NAME_PROPERTY
1395
+ }, ["scriptName"], listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA), async (args, ctx) => {
1396
+ const { result } = await callCloudflare(send, {
1397
+ method: "GET",
1398
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(requireString(args, "scriptName"))}/deployments`,
1399
+ }, ctx);
1400
+ const record = asRecord(result);
1401
+ const deployments = Array.isArray(result)
1402
+ ? result
1403
+ : asArray(record["deployments"]);
1404
+ return { deployments: deployments.map(projectWorkerDeployment) };
1405
+ }),
1406
+ cfTool("get_worker_deployment", "Get one Worker deployment and its version traffic allocations.", readOnly, "accountId", scope.accountId, {
1407
+ scriptName: SCRIPT_NAME_PROPERTY,
1408
+ deploymentId: WORKER_DEPLOYMENT_ID_PROPERTY
1409
+ }, ["scriptName", "deploymentId"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1410
+ method: "GET",
1411
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(requireString(args, "scriptName"))}/deployments/${encodeURIComponent(requireString(args, "deploymentId"))}`,
1412
+ }), projectWorkerDeployment)),
1413
+ cfTool("delete_worker_script", "Delete a Worker script and stop traffic served by that script. This cannot be undone from the API.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1414
+ scriptName: SCRIPT_NAME_PROPERTY,
1415
+ force: {
1416
+ type: "boolean",
1417
+ description: "Pass Cloudflare's force=true option when the script has dependencies that permit forced removal.",
1418
+ }
1419
+ }, ["scriptName"], {
1420
+ type: "object",
1421
+ properties: {
1422
+ deleted: { type: "boolean" },
1423
+ scriptName: { type: "string" },
1424
+ },
1425
+ required: ["deleted", "scriptName"],
1426
+ }, deleteAck(send, "scriptName", (args) => requireString(args, "scriptName"), (args, scriptName) => ({
1427
+ method: "DELETE",
1428
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(scriptName)}`,
1429
+ query: { force: typeof args["force"] === "boolean" ? args["force"] : undefined },
1430
+ }))),
1431
+ cfTool("list_kv_namespaces", "List Workers KV namespaces in an account, with the namespace ids bindings refer to.", readOnly, "accountId", scope.accountId, {
1432
+ ...pagingInputProperties(1, 1000, { defaultPerPage: 20 }),
1433
+ raw: RAW_INPUT_PROPERTY
1434
+ }, [], listOutputSchema("namespaces", {
1435
+ type: "object",
1436
+ properties: {
1437
+ id: { type: "string" },
1438
+ title: { type: "string" },
1439
+ supportsUrlEncoding: { type: "boolean" },
1440
+ },
1441
+ required: ["id", "title"],
1442
+ }), async (args, ctx) => {
1443
+ const { result, resultInfo } = await callCloudflare(send, {
1444
+ method: "GET",
1445
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
1446
+ query: {
1447
+ page: optionalNumber(args, "page"),
1448
+ per_page: optionalNumber(args, "perPage"),
1449
+ },
1450
+ }, ctx);
1451
+ return pagedList("namespaces", projectKvNamespace)(result, resultInfo, args["raw"] === true);
1452
+ }),
1453
+ cfTool("get_kv_namespace", "Get one Workers KV namespace by id.", readOnly, "accountId", scope.accountId, {
1454
+ namespaceId: NAMESPACE_ID_PROPERTY
1455
+ }, ["namespaceId"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1456
+ method: "GET",
1457
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}`,
1458
+ }), projectKvNamespace)),
1459
+ cfTool("create_kv_namespace", "Create a Workers KV namespace.", { readOnlyHint: false }, "accountId", scope.accountId, {
1460
+ title: {
1461
+ type: "string",
1462
+ minLength: 1,
1463
+ maxLength: 512,
1464
+ description: "Human-readable namespace title.",
1465
+ }
1466
+ }, ["title"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1467
+ const { result } = await callCloudflare(send, {
1468
+ method: "POST",
1469
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
1470
+ body: { title: requireString(args, "title") },
1471
+ }, ctx);
1472
+ return projectKvNamespace(result);
1473
+ }),
1474
+ cfTool("rename_kv_namespace", "Rename an existing Workers KV namespace without changing its id or keys.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1475
+ namespaceId: NAMESPACE_ID_PROPERTY,
1476
+ title: {
1477
+ type: "string",
1478
+ minLength: 1,
1479
+ maxLength: 512,
1480
+ description: "Replacement namespace title.",
1481
+ }
1482
+ }, ["namespaceId", "title"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1483
+ const { result } = await callCloudflare(send, {
1484
+ method: "PUT",
1485
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}`,
1486
+ body: { title: requireString(args, "title") },
1487
+ }, ctx);
1488
+ return result ?? { renamed: true, namespaceId: args["namespaceId"] };
1489
+ }),
1490
+ cfTool("delete_kv_namespace", "Permanently delete a Workers KV namespace and every key stored in it.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1491
+ namespaceId: NAMESPACE_ID_PROPERTY
1492
+ }, ["namespaceId"], {
1493
+ type: "object",
1494
+ properties: {
1495
+ deleted: { type: "boolean" },
1496
+ namespaceId: { type: "string" },
1497
+ },
1498
+ required: ["deleted", "namespaceId"],
1499
+ }, deleteAck(send, "namespaceId", (args) => requireString(args, "namespaceId"), (args, namespaceId) => ({
1500
+ method: "DELETE",
1501
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}`,
1502
+ }))),
1503
+ cfTool("list_kv_keys", "List keys and metadata in a Workers KV namespace by prefix, using cursor pagination.", readOnly, "accountId", scope.accountId, {
1504
+ namespaceId: NAMESPACE_ID_PROPERTY,
1505
+ prefix: {
1506
+ type: "string",
1507
+ description: "Return only keys beginning with this prefix.",
1508
+ },
1509
+ limit: {
1510
+ type: "integer",
1511
+ minimum: 10,
1512
+ maximum: 1000,
1513
+ description: "Keys per request, 10 to 1000. Defaults to 1000.",
1514
+ },
1515
+ cursor: CURSOR_INPUT_PROPERTY
1516
+ }, ["namespaceId"], {
1517
+ type: "object",
1518
+ properties: {
1519
+ keys: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1520
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1521
+ },
1522
+ required: ["keys"],
1523
+ }, async (args, ctx) => {
1524
+ const { result, resultInfo } = await callCloudflare(send, {
1525
+ method: "GET",
1526
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}/keys`,
1527
+ query: {
1528
+ prefix: optionalString(args, "prefix"),
1529
+ limit: optionalNumber(args, "limit"),
1530
+ cursor: optionalString(args, "cursor"),
1531
+ },
1532
+ }, ctx);
1533
+ const cursor = resultInfo?.cursor;
1534
+ return {
1535
+ keys: asArray(result).map(projectKvKey),
1536
+ ...cursorResult(cursor),
1537
+ };
1538
+ }),
1539
+ cfTool("bulk_get_kv_values", "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.", readOnly, "accountId", scope.accountId, {
1540
+ namespaceId: NAMESPACE_ID_PROPERTY,
1541
+ keys: {
1542
+ type: "array",
1543
+ minItems: 1,
1544
+ maxItems: 100,
1545
+ items: { type: "string", minLength: 1, maxLength: 512 },
1546
+ description: "Key names to retrieve, up to 100.",
1547
+ },
1548
+ withMetadata: {
1549
+ type: "boolean",
1550
+ description: "Include each key's metadata and expiration when true.",
1551
+ },
1552
+ type: {
1553
+ type: "string",
1554
+ enum: ["text", "json"],
1555
+ description: "Return strings as stored, or parse JSON values before returning them.",
1556
+ }
1557
+ }, ["namespaceId", "keys"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1558
+ const { result } = await callCloudflare(send, {
1559
+ method: "POST",
1560
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}/bulk/get`,
1561
+ body: compact({
1562
+ keys: args["keys"],
1563
+ withMetadata: args["withMetadata"],
1564
+ type: args["type"],
1565
+ }),
1566
+ }, ctx);
1567
+ return asRecord(result);
1568
+ }),
1569
+ cfTool("bulk_write_kv_values", "Create or replace multiple Workers KV values, with optional expirations and JSON metadata.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1570
+ namespaceId: NAMESPACE_ID_PROPERTY,
1571
+ entries: {
1572
+ type: "array",
1573
+ minItems: 1,
1574
+ maxItems: 10_000,
1575
+ description: "Key/value entries to write, up to Cloudflare's 10,000-key bulk limit.",
1576
+ items: {
1577
+ type: "object",
1578
+ properties: {
1579
+ key: {
1580
+ type: "string",
1581
+ minLength: 1,
1582
+ maxLength: 512,
1583
+ description: "Key name, up to Cloudflare's 512 bytes.",
1255
1584
  },
1256
- },
1257
- files: {
1258
- type: "array",
1259
- description: "Multipart file parts. Each needs exactly one of text or base64.",
1260
- items: {
1261
- type: "object",
1262
- properties: {
1263
- name: { type: "string", minLength: 1 },
1264
- fileName: { type: "string", minLength: 1 },
1265
- contentType: { type: "string", minLength: 1 },
1266
- text: { type: "string" },
1267
- base64: { type: "string" },
1268
- },
1269
- required: ["name", "fileName", "contentType"],
1270
- additionalProperties: false,
1585
+ value: {
1586
+ type: "string",
1587
+ maxLength: 26_214_400,
1588
+ description: "Value, up to Cloudflare's 25 MiB.",
1589
+ },
1590
+ expiration: {
1591
+ type: "number",
1592
+ description: "Absolute expiry as a Unix timestamp in seconds.",
1593
+ },
1594
+ expiration_ttl: {
1595
+ type: "number",
1596
+ minimum: 60,
1597
+ description: "Relative expiry in seconds; Cloudflare's floor is 60.",
1598
+ },
1599
+ metadata: {
1600
+ type: ["object", "array", "string", "number", "boolean", "null"],
1601
+ description: "JSON metadata returned beside the key by list_kv_keys.",
1602
+ },
1603
+ base64: {
1604
+ type: "boolean",
1605
+ description: "Treat value as base64 and store the decoded bytes.",
1271
1606
  },
1272
1607
  },
1608
+ required: ["key", "value"],
1609
+ additionalProperties: false,
1273
1610
  },
1274
- required: ["method", "path"],
1275
- additionalProperties: false,
1276
- },
1277
- outputSchema: {
1278
- type: "object",
1279
- properties: {
1280
- result: {
1281
- description: "Cloudflare's unprojected upload result.",
1282
- },
1283
- },
1284
- required: ["result"],
1285
- },
1286
- handler: async (args, ctx) => {
1287
- const query = queryFromArgs(args["query"]);
1288
- const headers = headersFromArgs(args["headers"]);
1289
- const upload = uploadBody(args);
1290
- const { result } = await callCloudflare(send, {
1291
- method: String(args["method"]),
1292
- path: cloudflareApiPath(args["path"]),
1293
- ...(query !== undefined ? { query } : {}),
1294
- ...(headers !== undefined || upload.headers !== undefined
1295
- ? { headers: { ...headers, ...upload.headers } }
1296
- : {}),
1297
- rawBody: upload.rawBody,
1298
- }, ctx);
1299
- return { result };
1300
- },
1301
- },
1302
- {
1303
- name: "list_accounts",
1304
- description: "List Cloudflare accounts this token can see. Supplies the accountId that the Workers, KV, R2, and Pages tools need.",
1305
- annotations: readOnly,
1306
- inputSchema: {
1307
- type: "object",
1308
- properties: {
1309
- name: {
1310
- type: "string",
1311
- description: "Filter by exact account name.",
1312
- },
1313
- ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1314
- raw: RAW_INPUT_PROPERTY,
1315
- },
1316
- required: [],
1317
- additionalProperties: false,
1318
- },
1319
- outputSchema: listOutputSchema("accounts", ACCOUNT_SCHEMA),
1320
- handler: async (args, ctx) => {
1321
- const { result, resultInfo } = await callCloudflare(send, {
1322
- method: "GET",
1323
- path: "/accounts",
1324
- query: {
1325
- name: optionalString(args, "name"),
1326
- page: optionalNumber(args, "page"),
1327
- per_page: optionalNumber(args, "perPage"),
1328
- },
1329
- }, ctx);
1330
- if (args["raw"] === true)
1331
- return { accounts: result, page: pageInfo(resultInfo) };
1332
- return {
1333
- accounts: asArray(result).map(projectAccount),
1334
- page: pageInfo(resultInfo),
1335
- };
1336
- },
1337
- },
1338
- {
1339
- name: "list_zones",
1340
- description: "List zones (domains) this token can see, with their ids and status. This is the zoneId discovery step for every DNS and cache tool.",
1341
- annotations: readOnly,
1342
- inputSchema: {
1343
- type: "object",
1344
- properties: {
1345
- name: {
1346
- type: "string",
1347
- description: "Filter by zone name, e.g. example.com.",
1348
- },
1349
- accountId: {
1350
- type: "string",
1351
- description: "Restrict to one account. Defaults to every account the token can see.",
1352
- },
1353
- status: {
1354
- type: "string",
1355
- enum: ["initializing", "pending", "active", "moved"],
1356
- description: "Filter by zone status.",
1357
- },
1358
- ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1359
- raw: RAW_INPUT_PROPERTY,
1360
- },
1361
- required: [],
1362
- additionalProperties: false,
1363
- },
1364
- outputSchema: listOutputSchema("zones", ZONE_SCHEMA),
1365
- handler: async (args, ctx) => {
1366
- const { result, resultInfo } = await callCloudflare(send, {
1367
- method: "GET",
1368
- path: "/zones",
1369
- query: {
1370
- name: optionalString(args, "name"),
1371
- // Deliberately not defaulted to `scope.accountId`. This is the
1372
- // discovery tool: a deployment default that silently narrowed
1373
- // what an agent can see would contradict the property's own
1374
- // description, and there would be no argument that escapes it.
1375
- "account.id": optionalString(args, "accountId"),
1376
- status: optionalString(args, "status"),
1377
- page: optionalNumber(args, "page"),
1378
- per_page: optionalNumber(args, "perPage"),
1379
- },
1380
- }, ctx);
1381
- if (args["raw"] === true)
1382
- return { zones: result, page: pageInfo(resultInfo) };
1383
- return {
1384
- zones: asArray(result).map(projectZone),
1385
- page: pageInfo(resultInfo),
1386
- };
1387
- },
1388
- },
1389
- {
1390
- name: "get_zone",
1391
- description: "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.",
1392
- annotations: readOnly,
1393
- inputSchema: {
1394
- type: "object",
1395
- properties: {
1396
- zoneId: scopeProperty("zoneId", scope.zoneId),
1397
- raw: RAW_INPUT_PROPERTY,
1398
- },
1399
- required: scopeRequired("zoneId", scope.zoneId),
1400
- additionalProperties: false,
1401
- },
1402
- outputSchema: ZONE_SCHEMA,
1403
- handler: async (args, ctx) => {
1404
- const { result } = await callCloudflare(send, { method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` }, ctx);
1405
- return args["raw"] === true ? result : projectZone(result);
1406
- },
1407
- },
1408
- // There is deliberately no bulk zone-settings read here. Cloudflare
1409
- // publishes `GET /zones/{zone_id}/settings` as deprecated (#361), the
1410
- // per-setting operations below are not, and the tool that wrapped the bulk
1411
- // read projected nothing. See the Cloudflare guide's "What the named
1412
- // surface deliberately leaves out".
1413
- {
1414
- name: "get_zone_setting",
1415
- description: "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.",
1416
- annotations: readOnly,
1417
- inputSchema: {
1418
- type: "object",
1419
- properties: {
1420
- zoneId: scopeProperty("zoneId", scope.zoneId),
1421
- settingId: {
1422
- type: "string",
1423
- minLength: 1,
1424
- description: "Cloudflare zone setting id, such as ssl, brotli, http3, or min_tls_version.",
1425
- },
1426
- },
1427
- required: [...scopeRequired("zoneId", scope.zoneId), "settingId"],
1428
- additionalProperties: false,
1429
- },
1430
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1431
- handler: async (args, ctx) => {
1432
- const { result } = await callCloudflare(send, {
1433
- method: "GET",
1434
- path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1435
- }, ctx);
1436
- return result;
1437
- },
1438
- },
1439
- {
1440
- name: "update_zone_setting",
1441
- description: "Set one editable zone setting. Read it first: allowed value types and plan restrictions differ by setting.",
1442
- annotations: { readOnlyHint: false, destructiveHint: true },
1443
- inputSchema: {
1444
- type: "object",
1445
- properties: {
1446
- zoneId: scopeProperty("zoneId", scope.zoneId),
1447
- settingId: {
1448
- type: "string",
1449
- minLength: 1,
1450
- description: "Cloudflare zone setting id, such as ssl, brotli, http3, or min_tls_version.",
1451
- },
1452
- value: {
1453
- type: ["string", "number", "boolean", "array"],
1454
- description: "New setting value in the type returned by get_zone_setting. Arrays must contain strings.",
1455
- items: { type: "string" },
1456
- },
1457
- },
1458
- required: [...scopeRequired("zoneId", scope.zoneId), "settingId", "value"],
1459
- additionalProperties: false,
1460
- },
1461
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1462
- handler: async (args, ctx) => {
1463
- const { result } = await callCloudflare(send, {
1464
- method: "PATCH",
1465
- path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1466
- body: { value: args["value"] },
1467
- }, ctx);
1468
- return result;
1469
- },
1470
- },
1471
- {
1472
- name: "list_zone_rulesets",
1473
- description: "List zone rulesets for WAF, redirects, transforms, cache rules, configuration rules, and other Ruleset Engine phases.",
1474
- annotations: readOnly,
1475
- inputSchema: {
1476
- type: "object",
1477
- properties: {
1478
- zoneId: scopeProperty("zoneId", scope.zoneId),
1479
- perPage: {
1480
- type: "integer",
1481
- minimum: 1,
1482
- maximum: 50,
1483
- description: "Rulesets per request, 1 to 50.",
1484
- },
1485
- cursor: CURSOR_INPUT_PROPERTY,
1486
- },
1487
- required: scopeRequired("zoneId", scope.zoneId),
1488
- additionalProperties: false,
1489
- },
1490
- outputSchema: {
1491
- type: "object",
1492
- properties: {
1493
- rulesets: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1494
- nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1495
- },
1496
- required: ["rulesets"],
1497
- },
1498
- handler: async (args, ctx) => {
1499
- const { result, resultInfo } = await callCloudflare(send, {
1500
- method: "GET",
1501
- path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets`,
1502
- query: {
1503
- per_page: optionalNumber(args, "perPage"),
1504
- cursor: optionalString(args, "cursor"),
1505
- },
1506
- }, ctx);
1507
- const cursor = resultInfo?.cursors?.after;
1508
- return {
1509
- rulesets: asArray(result).map(projectRuleset),
1510
- ...(typeof cursor === "string" && cursor !== ""
1511
- ? { nextCursor: cursor }
1512
- : {}),
1513
- };
1514
- },
1515
- },
1516
- {
1517
- name: "get_zone_ruleset",
1518
- description: "Get one zone ruleset including its ordered rules, expressions, actions, parameters, and enabled state.",
1519
- annotations: readOnly,
1520
- inputSchema: {
1521
- type: "object",
1522
- properties: {
1523
- zoneId: scopeProperty("zoneId", scope.zoneId),
1524
- rulesetId: {
1525
- type: "string",
1526
- minLength: 1,
1527
- description: "Ruleset id from list_zone_rulesets.",
1528
- },
1529
- },
1530
- required: [...scopeRequired("zoneId", scope.zoneId), "rulesetId"],
1531
- additionalProperties: false,
1532
- },
1533
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1534
- handler: async (args, ctx) => {
1535
- const { result } = await callCloudflare(send, {
1536
- method: "GET",
1537
- path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets/${encodePathSegment(requireString(args, "rulesetId"))}`,
1538
- }, ctx);
1539
- return projectRuleset(result);
1540
- },
1541
- },
1542
- {
1543
- name: "list_dns_records",
1544
- description: "List DNS records in a zone, filtered by name, type, or content. Returns record ids, which update_dns_record and delete_dns_record require.",
1545
- annotations: readOnly,
1546
- inputSchema: {
1547
- type: "object",
1548
- properties: {
1549
- zoneId: scopeProperty("zoneId", scope.zoneId),
1550
- name: {
1551
- type: "string",
1552
- description: "Exact record name, fully qualified, e.g. www.example.com.",
1553
- },
1554
- type: {
1555
- type: "string",
1556
- enum: [...CLOUDFLARE_DNS_RECORD_TYPES],
1557
- description: "Filter by record type.",
1558
- },
1559
- content: {
1560
- type: "string",
1561
- description: "Exact record content, e.g. an IP address.",
1562
- },
1563
- order: {
1564
- type: "string",
1565
- enum: ["type", "name", "content", "ttl", "proxied"],
1566
- description: "Sort field.",
1567
- },
1568
- direction: {
1569
- type: "string",
1570
- enum: ["asc", "desc"],
1571
- description: "Sort direction for `order`. Defaults to asc.",
1572
- },
1573
- // Cloudflare documents 1 to 5,000,000 here with a default of 100; the
1574
- // ceiling is nominal, so this connection caps it at a page size that
1575
- // actually returns.
1576
- ...pagingInputProperties(1, 1000, {
1577
- defaultPerPage: 100,
1578
- bounds: "clamped",
1579
- }),
1580
- raw: RAW_INPUT_PROPERTY,
1581
- },
1582
- required: scopeRequired("zoneId", scope.zoneId),
1583
- additionalProperties: false,
1584
- },
1585
- outputSchema: listOutputSchema("records", DNS_RECORD_SCHEMA),
1586
- handler: async (args, ctx) => {
1587
- const { result, resultInfo } = await callCloudflare(send, {
1588
- method: "GET",
1589
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
1590
- query: {
1591
- name: optionalString(args, "name"),
1592
- type: optionalString(args, "type"),
1593
- content: optionalString(args, "content"),
1594
- order: optionalString(args, "order"),
1595
- direction: optionalString(args, "direction"),
1596
- page: optionalNumber(args, "page"),
1597
- per_page: optionalNumber(args, "perPage"),
1598
- },
1599
- }, ctx);
1600
- if (args["raw"] === true)
1601
- return { records: result, page: pageInfo(resultInfo) };
1602
- return {
1603
- records: asArray(result).map(projectDnsRecord),
1604
- page: pageInfo(resultInfo),
1605
- };
1606
- },
1607
- },
1608
- {
1609
- name: "get_dns_record",
1610
- description: "Fetch one DNS record by its record id.",
1611
- annotations: readOnly,
1612
- inputSchema: {
1613
- type: "object",
1614
- properties: {
1615
- zoneId: scopeProperty("zoneId", scope.zoneId),
1616
- recordId: {
1617
- type: "string",
1618
- description: "DNS record id, from list_dns_records.",
1619
- },
1620
- raw: RAW_INPUT_PROPERTY,
1621
- },
1622
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
1623
- additionalProperties: false,
1624
- },
1625
- outputSchema: DNS_RECORD_SCHEMA,
1626
- handler: async (args, ctx) => {
1627
- const { result } = await callCloudflare(send, {
1628
- method: "GET",
1629
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
1630
- }, ctx);
1631
- return args["raw"] === true ? result : projectDnsRecord(result);
1632
- },
1633
- },
1634
- {
1635
- name: "list_worker_scripts",
1636
- description: "List Workers scripts deployed in an account, with their last-modified times.",
1637
- annotations: readOnly,
1638
- inputSchema: {
1639
- type: "object",
1640
- properties: {
1641
- accountId: scopeProperty("accountId", scope.accountId),
1642
- raw: RAW_INPUT_PROPERTY,
1643
- },
1644
- required: scopeRequired("accountId", scope.accountId),
1645
- additionalProperties: false,
1646
- },
1647
- outputSchema: listOutputSchema("scripts", {
1648
- type: "object",
1649
- properties: {
1650
- id: { type: "string", description: "Script name." },
1651
- createdOn: { type: "string" },
1652
- modifiedOn: { type: "string" },
1653
- usageModel: { type: "string" },
1654
- },
1655
- required: ["id"],
1656
- }),
1657
- handler: async (args, ctx) => {
1658
- const { result, resultInfo } = await callCloudflare(send, {
1659
- method: "GET",
1660
- path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
1661
- }, ctx);
1662
- if (args["raw"] === true)
1663
- return { scripts: result, page: pageInfo(resultInfo) };
1664
- return {
1665
- scripts: asArray(result).map(projectWorkerScript),
1666
- page: pageInfo(resultInfo),
1667
- };
1668
- },
1669
- },
1670
- {
1671
- name: "get_worker_settings",
1672
- description: "Get a Worker's compatibility date and flags, bindings, limits, observability, placement, usage model, and other script settings.",
1673
- annotations: readOnly,
1674
- inputSchema: {
1675
- type: "object",
1676
- properties: {
1677
- accountId: scopeProperty("accountId", scope.accountId),
1678
- scriptName: {
1679
- type: "string",
1680
- minLength: 1,
1681
- description: "Worker script name from list_worker_scripts.",
1682
- },
1683
- },
1684
- required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
1685
- additionalProperties: false,
1686
- },
1687
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1688
- handler: async (args, ctx) => {
1689
- const { result } = await callCloudflare(send, {
1690
- method: "GET",
1691
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/settings`,
1692
- }, ctx);
1693
- return result;
1694
- },
1695
- },
1696
- {
1697
- name: "list_worker_deployments",
1698
- description: "List deployments of a Worker script, including version traffic allocations and deployment strategy.",
1699
- annotations: readOnly,
1700
- inputSchema: {
1701
- type: "object",
1702
- properties: {
1703
- accountId: scopeProperty("accountId", scope.accountId),
1704
- scriptName: {
1705
- type: "string",
1706
- minLength: 1,
1707
- description: "Worker script name from list_worker_scripts.",
1708
- },
1709
- },
1710
- required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
1711
- additionalProperties: false,
1712
- },
1713
- outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
1714
- handler: async (args, ctx) => {
1715
- const { result } = await callCloudflare(send, {
1716
- method: "GET",
1717
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments`,
1718
- }, ctx);
1719
- const record = asRecord(result);
1720
- const deployments = Array.isArray(result)
1721
- ? result
1722
- : asArray(record["deployments"]);
1723
- return { deployments: deployments.map(projectWorkerDeployment) };
1724
- },
1725
- },
1726
- {
1727
- name: "get_worker_deployment",
1728
- description: "Get one Worker deployment and its version traffic allocations.",
1729
- annotations: readOnly,
1730
- inputSchema: {
1731
- type: "object",
1732
- properties: {
1733
- accountId: scopeProperty("accountId", scope.accountId),
1734
- scriptName: {
1735
- type: "string",
1736
- minLength: 1,
1737
- description: "Worker script name from list_worker_scripts.",
1738
- },
1739
- deploymentId: {
1740
- type: "string",
1741
- minLength: 1,
1742
- description: "Deployment id from list_worker_deployments.",
1743
- },
1744
- },
1745
- required: [
1746
- ...scopeRequired("accountId", scope.accountId),
1747
- "scriptName",
1748
- "deploymentId",
1749
- ],
1750
- additionalProperties: false,
1751
- },
1752
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1753
- handler: async (args, ctx) => {
1754
- const { result } = await callCloudflare(send, {
1755
- method: "GET",
1756
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
1757
- }, ctx);
1758
- return projectWorkerDeployment(result);
1759
- },
1760
- },
1761
- {
1762
- name: "delete_worker_script",
1763
- description: "Delete a Worker script and stop traffic served by that script. This cannot be undone from the API.",
1764
- annotations: { readOnlyHint: false, destructiveHint: true },
1765
- inputSchema: {
1766
- type: "object",
1767
- properties: {
1768
- accountId: scopeProperty("accountId", scope.accountId),
1769
- scriptName: {
1770
- type: "string",
1771
- minLength: 1,
1772
- description: "Worker script name from list_worker_scripts.",
1773
- },
1774
- force: {
1775
- type: "boolean",
1776
- description: "Pass Cloudflare's force=true option when the script has dependencies that permit forced removal.",
1777
- },
1778
- },
1779
- required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
1780
- additionalProperties: false,
1781
- },
1782
- outputSchema: {
1783
- type: "object",
1784
- properties: {
1785
- deleted: { type: "boolean" },
1786
- scriptName: { type: "string" },
1787
- },
1788
- required: ["deleted", "scriptName"],
1789
- },
1790
- handler: async (args, ctx) => {
1791
- const scriptName = requireString(args, "scriptName");
1792
- await callCloudflare(send, {
1793
- method: "DELETE",
1794
- path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(scriptName)}`,
1795
- query: { force: optionalBoolean(args, "force") },
1796
- }, ctx);
1797
- return { deleted: true, scriptName };
1798
- },
1799
- },
1800
- {
1801
- name: "list_kv_namespaces",
1802
- description: "List Workers KV namespaces in an account, with the namespace ids bindings refer to.",
1803
- annotations: readOnly,
1804
- inputSchema: {
1805
- type: "object",
1806
- properties: {
1807
- accountId: scopeProperty("accountId", scope.accountId),
1808
- ...pagingInputProperties(1, 1000, { defaultPerPage: 20 }),
1809
- raw: RAW_INPUT_PROPERTY,
1810
- },
1811
- required: scopeRequired("accountId", scope.accountId),
1812
- additionalProperties: false,
1813
- },
1814
- outputSchema: listOutputSchema("namespaces", {
1815
- type: "object",
1816
- properties: {
1817
- id: { type: "string" },
1818
- title: { type: "string" },
1819
- supportsUrlEncoding: { type: "boolean" },
1820
- },
1821
- required: ["id", "title"],
1822
- }),
1823
- handler: async (args, ctx) => {
1824
- const { result, resultInfo } = await callCloudflare(send, {
1825
- method: "GET",
1826
- path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
1827
- query: {
1828
- page: optionalNumber(args, "page"),
1829
- per_page: optionalNumber(args, "perPage"),
1830
- },
1831
- }, ctx);
1832
- if (args["raw"] === true)
1833
- return { namespaces: result, page: pageInfo(resultInfo) };
1834
- return {
1835
- namespaces: asArray(result).map(projectKvNamespace),
1836
- page: pageInfo(resultInfo),
1837
- };
1838
- },
1839
- },
1840
- {
1841
- name: "get_kv_namespace",
1842
- description: "Get one Workers KV namespace by id.",
1843
- annotations: readOnly,
1844
- inputSchema: {
1845
- type: "object",
1846
- properties: {
1847
- accountId: scopeProperty("accountId", scope.accountId),
1848
- namespaceId: {
1849
- type: "string",
1850
- minLength: 1,
1851
- description: "KV namespace id from list_kv_namespaces.",
1852
- },
1853
- },
1854
- required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
1855
- additionalProperties: false,
1856
- },
1857
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1858
- handler: async (args, ctx) => {
1859
- const { result } = await callCloudflare(send, {
1860
- method: "GET",
1861
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
1862
- }, ctx);
1863
- return projectKvNamespace(result);
1864
- },
1865
- },
1866
- {
1867
- name: "create_kv_namespace",
1868
- description: "Create a Workers KV namespace.",
1869
- annotations: { readOnlyHint: false },
1870
- inputSchema: {
1871
- type: "object",
1872
- properties: {
1873
- accountId: scopeProperty("accountId", scope.accountId),
1874
- title: {
1875
- type: "string",
1876
- minLength: 1,
1877
- maxLength: 512,
1878
- description: "Human-readable namespace title.",
1879
- },
1880
- },
1881
- required: [...scopeRequired("accountId", scope.accountId), "title"],
1882
- additionalProperties: false,
1883
- },
1884
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1885
- handler: async (args, ctx) => {
1886
- const { result } = await callCloudflare(send, {
1887
- method: "POST",
1888
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces`,
1889
- body: { title: requireString(args, "title") },
1890
- }, ctx);
1891
- return projectKvNamespace(result);
1892
- },
1893
- },
1894
- {
1895
- name: "rename_kv_namespace",
1896
- description: "Rename an existing Workers KV namespace without changing its id or keys.",
1897
- annotations: { readOnlyHint: false, destructiveHint: true },
1898
- inputSchema: {
1899
- type: "object",
1900
- properties: {
1901
- accountId: scopeProperty("accountId", scope.accountId),
1902
- namespaceId: {
1903
- type: "string",
1904
- minLength: 1,
1905
- description: "KV namespace id from list_kv_namespaces.",
1906
- },
1907
- title: {
1908
- type: "string",
1909
- minLength: 1,
1910
- maxLength: 512,
1911
- description: "Replacement namespace title.",
1912
- },
1913
- },
1914
- required: [
1915
- ...scopeRequired("accountId", scope.accountId),
1916
- "namespaceId",
1917
- "title",
1918
- ],
1919
- additionalProperties: false,
1920
- },
1921
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1922
- handler: async (args, ctx) => {
1923
- const { result } = await callCloudflare(send, {
1924
- method: "PUT",
1925
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
1926
- body: { title: requireString(args, "title") },
1927
- }, ctx);
1928
- return result ?? { renamed: true, namespaceId: args["namespaceId"] };
1929
- },
1930
- },
1931
- {
1932
- name: "delete_kv_namespace",
1933
- description: "Permanently delete a Workers KV namespace and every key stored in it.",
1934
- annotations: { readOnlyHint: false, destructiveHint: true },
1935
- inputSchema: {
1936
- type: "object",
1937
- properties: {
1938
- accountId: scopeProperty("accountId", scope.accountId),
1939
- namespaceId: {
1940
- type: "string",
1941
- minLength: 1,
1942
- description: "KV namespace id from list_kv_namespaces.",
1943
- },
1944
- },
1945
- required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
1946
- additionalProperties: false,
1947
- },
1948
- outputSchema: {
1949
- type: "object",
1950
- properties: {
1951
- deleted: { type: "boolean" },
1952
- namespaceId: { type: "string" },
1953
- },
1954
- required: ["deleted", "namespaceId"],
1955
- },
1956
- handler: async (args, ctx) => {
1957
- const namespaceId = requireString(args, "namespaceId");
1958
- await callCloudflare(send, {
1959
- method: "DELETE",
1960
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(namespaceId)}`,
1961
- }, ctx);
1962
- return { deleted: true, namespaceId };
1963
- },
1964
- },
1965
- {
1966
- name: "list_kv_keys",
1967
- description: "List keys and metadata in a Workers KV namespace by prefix, using cursor pagination.",
1968
- annotations: readOnly,
1969
- inputSchema: {
1970
- type: "object",
1971
- properties: {
1972
- accountId: scopeProperty("accountId", scope.accountId),
1973
- namespaceId: {
1974
- type: "string",
1975
- minLength: 1,
1976
- description: "KV namespace id from list_kv_namespaces.",
1977
- },
1978
- prefix: {
1979
- type: "string",
1980
- description: "Return only keys beginning with this prefix.",
1981
- },
1982
- limit: {
1983
- type: "integer",
1984
- minimum: 10,
1985
- maximum: 1000,
1986
- description: "Keys per request, 10 to 1000. Defaults to 1000.",
1987
- },
1988
- cursor: CURSOR_INPUT_PROPERTY,
1989
- },
1990
- required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
1991
- additionalProperties: false,
1992
- },
1993
- outputSchema: {
1994
- type: "object",
1995
- properties: {
1996
- keys: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1997
- nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1998
- },
1999
- required: ["keys"],
2000
- },
2001
- handler: async (args, ctx) => {
2002
- const { result, resultInfo } = await callCloudflare(send, {
2003
- method: "GET",
2004
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/keys`,
2005
- query: {
2006
- prefix: optionalString(args, "prefix"),
2007
- limit: optionalNumber(args, "limit"),
2008
- cursor: optionalString(args, "cursor"),
2009
- },
2010
- }, ctx);
2011
- const cursor = resultInfo?.cursor;
2012
- return {
2013
- keys: asArray(result).map(projectKvKey),
2014
- ...(typeof cursor === "string" && cursor !== ""
2015
- ? { nextCursor: cursor }
2016
- : {}),
2017
- };
2018
- },
2019
- },
2020
- {
2021
- name: "bulk_get_kv_values",
2022
- description: "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.",
2023
- annotations: readOnly,
2024
- inputSchema: {
2025
- type: "object",
2026
- properties: {
2027
- accountId: scopeProperty("accountId", scope.accountId),
2028
- namespaceId: {
2029
- type: "string",
2030
- minLength: 1,
2031
- description: "KV namespace id from list_kv_namespaces.",
2032
- },
2033
- keys: {
2034
- type: "array",
2035
- minItems: 1,
2036
- maxItems: 100,
2037
- items: { type: "string", minLength: 1, maxLength: 512 },
2038
- description: "Key names to retrieve, up to 100.",
2039
- },
2040
- withMetadata: {
2041
- type: "boolean",
2042
- description: "Include each key's metadata and expiration when true.",
2043
- },
2044
- type: {
2045
- type: "string",
2046
- enum: ["text", "json"],
2047
- description: "Return strings as stored, or parse JSON values before returning them.",
2048
- },
2049
- },
2050
- required: [
2051
- ...scopeRequired("accountId", scope.accountId),
2052
- "namespaceId",
2053
- "keys",
2054
- ],
2055
- additionalProperties: false,
2056
- },
2057
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2058
- handler: async (args, ctx) => {
2059
- const { result } = await callCloudflare(send, {
2060
- method: "POST",
2061
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/get`,
2062
- body: {
2063
- keys: args["keys"],
2064
- ...(args["withMetadata"] !== undefined
2065
- ? { withMetadata: args["withMetadata"] }
2066
- : {}),
2067
- ...(args["type"] !== undefined ? { type: args["type"] } : {}),
2068
- },
2069
- }, ctx);
2070
- return asRecord(result);
2071
- },
2072
- },
2073
- {
2074
- name: "bulk_write_kv_values",
2075
- description: "Create or replace multiple Workers KV values, with optional expirations and JSON metadata.",
2076
- annotations: { readOnlyHint: false, destructiveHint: true },
2077
- inputSchema: {
2078
- type: "object",
2079
- properties: {
2080
- accountId: scopeProperty("accountId", scope.accountId),
2081
- namespaceId: {
2082
- type: "string",
2083
- minLength: 1,
2084
- description: "KV namespace id from list_kv_namespaces.",
2085
- },
2086
- entries: {
2087
- type: "array",
2088
- minItems: 1,
2089
- maxItems: 10_000,
2090
- description: "Key/value entries to write, up to Cloudflare's 10,000-key bulk limit.",
2091
- items: {
2092
- type: "object",
2093
- properties: {
2094
- key: {
2095
- type: "string",
2096
- minLength: 1,
2097
- maxLength: 512,
2098
- description: "Key name, up to Cloudflare's 512 bytes.",
2099
- },
2100
- value: {
2101
- type: "string",
2102
- maxLength: 26_214_400,
2103
- description: "Value, up to Cloudflare's 25 MiB.",
2104
- },
2105
- expiration: {
2106
- type: "number",
2107
- description: "Absolute expiry as a Unix timestamp in seconds.",
2108
- },
2109
- expiration_ttl: {
2110
- type: "number",
2111
- minimum: 60,
2112
- description: "Relative expiry in seconds; Cloudflare's floor is 60.",
2113
- },
2114
- metadata: {
2115
- type: ["object", "array", "string", "number", "boolean", "null"],
2116
- description: "JSON metadata returned beside the key by list_kv_keys.",
2117
- },
2118
- base64: {
2119
- type: "boolean",
2120
- description: "Treat value as base64 and store the decoded bytes.",
2121
- },
2122
- },
2123
- required: ["key", "value"],
2124
- additionalProperties: false,
2125
- },
2126
- },
2127
- },
2128
- required: [
2129
- ...scopeRequired("accountId", scope.accountId),
2130
- "namespaceId",
2131
- "entries",
2132
- ],
2133
- additionalProperties: false,
2134
- },
2135
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2136
- handler: async (args, ctx) => {
2137
- const { result } = await callCloudflare(send, {
2138
- method: "PUT",
2139
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk`,
2140
- body: args["entries"],
2141
- }, ctx);
2142
- return asRecord(result);
2143
- },
2144
- },
2145
- {
2146
- name: "bulk_delete_kv_values",
2147
- description: "Permanently delete multiple keys from a Workers KV namespace.",
2148
- annotations: { readOnlyHint: false, destructiveHint: true },
2149
- inputSchema: {
2150
- type: "object",
2151
- properties: {
2152
- accountId: scopeProperty("accountId", scope.accountId),
2153
- namespaceId: {
2154
- type: "string",
2155
- minLength: 1,
2156
- description: "KV namespace id from list_kv_namespaces.",
2157
- },
2158
- keys: {
2159
- type: "array",
2160
- minItems: 1,
2161
- maxItems: 10_000,
2162
- items: { type: "string", minLength: 1, maxLength: 512 },
2163
- description: "Key names to delete, up to Cloudflare's 10,000-key bulk limit.",
2164
- },
2165
- },
2166
- required: [
2167
- ...scopeRequired("accountId", scope.accountId),
2168
- "namespaceId",
2169
- "keys",
2170
- ],
2171
- additionalProperties: false,
2172
- },
2173
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2174
- handler: async (args, ctx) => {
2175
- const { result } = await callCloudflare(send, {
2176
- method: "POST",
2177
- path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/delete`,
2178
- body: args["keys"],
2179
- }, ctx);
2180
- return asRecord(result);
2181
- },
2182
- },
2183
- {
2184
- name: "list_r2_buckets",
2185
- description: "List R2 buckets in an account, with location and storage class.",
2186
- annotations: readOnly,
2187
- inputSchema: {
2188
- type: "object",
2189
- properties: {
2190
- accountId: scopeProperty("accountId", scope.accountId),
2191
- nameContains: {
2192
- type: "string",
2193
- description: "Filter to buckets whose name contains this string.",
2194
- },
2195
- perPage: {
2196
- type: "integer",
2197
- minimum: 1,
2198
- maximum: 1000,
2199
- description: "Buckets per request, 1 to 1000. Defaults to 20.",
2200
- },
2201
- cursor: CURSOR_INPUT_PROPERTY,
2202
- jurisdiction: R2_JURISDICTION_PROPERTY,
2203
- raw: RAW_INPUT_PROPERTY,
2204
- },
2205
- required: scopeRequired("accountId", scope.accountId),
2206
- additionalProperties: false,
2207
- },
2208
- outputSchema: {
2209
- type: "object",
2210
- properties: {
2211
- buckets: {
2212
- type: "array",
2213
- items: {
2214
- type: "object",
2215
- properties: {
2216
- name: { type: "string" },
2217
- location: { type: "string" },
2218
- storageClass: { type: "string" },
2219
- jurisdiction: { type: "string" },
2220
- creationDate: { type: "string" },
2221
- },
2222
- required: ["name"],
2223
- },
2224
- },
2225
- nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
2226
- },
2227
- required: ["buckets"],
2228
- },
2229
- handler: async (args, ctx) => {
2230
- const { result, resultInfo } = await callCloudflare(send, {
2231
- method: "GET",
2232
- path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
2233
- query: {
2234
- name_contains: optionalString(args, "nameContains"),
2235
- per_page: optionalNumber(args, "perPage"),
2236
- cursor: optionalString(args, "cursor"),
2237
- },
2238
- headers: r2Headers(args),
2239
- }, ctx);
2240
- // R2 nests its list under `buckets` rather than returning a bare array,
2241
- // and its result_info carries a cursor instead of page counters.
2242
- const cursor = resultInfo?.cursor;
2243
- const next = typeof cursor === "string" && cursor !== ""
2244
- ? { nextCursor: cursor }
2245
- : {};
2246
- if (args["raw"] === true)
2247
- return { buckets: result, ...next };
2248
- return {
2249
- buckets: asArray(asRecord(result)["buckets"]).map(projectR2Bucket),
2250
- ...next,
2251
- };
2252
- },
2253
- },
2254
- {
2255
- name: "get_r2_bucket",
2256
- description: "Get one R2 bucket's location, jurisdiction, storage class, and creation time.",
2257
- annotations: readOnly,
2258
- inputSchema: {
2259
- type: "object",
2260
- properties: {
2261
- accountId: scopeProperty("accountId", scope.accountId),
2262
- bucketName: R2_BUCKET_NAME_PROPERTY,
2263
- jurisdiction: R2_JURISDICTION_PROPERTY,
2264
- },
2265
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2266
- additionalProperties: false,
2267
- },
2268
- outputSchema: R2_BUCKET_SCHEMA,
2269
- handler: async (args, ctx) => {
2270
- const { result } = await callCloudflare(send, {
2271
- method: "GET",
2272
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2273
- headers: r2Headers(args),
2274
- }, ctx);
2275
- return projectR2Bucket(result);
2276
- },
2277
- },
2278
- {
2279
- name: "create_r2_bucket",
2280
- description: "Create an R2 bucket with an optional location hint and default storage class.",
2281
- annotations: { readOnlyHint: false },
2282
- inputSchema: {
2283
- type: "object",
2284
- properties: {
2285
- accountId: scopeProperty("accountId", scope.accountId),
2286
- bucketName: R2_BUCKET_NAME_PROPERTY,
2287
- jurisdiction: R2_JURISDICTION_PROPERTY,
2288
- locationHint: {
2289
- type: "string",
2290
- enum: ["apac", "eeur", "enam", "weur", "wnam", "oc"],
2291
- description: "Optional placement hint for the new bucket.",
2292
- },
2293
- storageClass: {
2294
- type: "string",
2295
- enum: ["Standard", "InfrequentAccess"],
2296
- description: "Default storage class for new objects.",
2297
- },
2298
- },
2299
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2300
- additionalProperties: false,
2301
- },
2302
- outputSchema: R2_BUCKET_SCHEMA,
2303
- handler: async (args, ctx) => {
2304
- const { result } = await callCloudflare(send, {
2305
- method: "POST",
2306
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets`,
2307
- headers: r2Headers(args),
2308
- body: {
2309
- name: requireString(args, "bucketName"),
2310
- ...(args["locationHint"] !== undefined
2311
- ? { locationHint: args["locationHint"] }
2312
- : {}),
2313
- ...(args["storageClass"] !== undefined
2314
- ? { storageClass: args["storageClass"] }
2315
- : {}),
2316
- },
2317
- }, ctx);
2318
- return projectR2Bucket(result);
2319
- },
2320
- },
2321
- {
2322
- name: "update_r2_bucket",
2323
- description: "Change the default storage class used for newly uploaded objects in an R2 bucket.",
2324
- annotations: { readOnlyHint: false, destructiveHint: true },
2325
- inputSchema: {
2326
- type: "object",
2327
- properties: {
2328
- accountId: scopeProperty("accountId", scope.accountId),
2329
- bucketName: R2_BUCKET_NAME_PROPERTY,
2330
- jurisdiction: R2_JURISDICTION_PROPERTY,
2331
- storageClass: {
2332
- type: "string",
2333
- enum: ["Standard", "InfrequentAccess"],
2334
- description: "New default storage class for future uploads.",
2335
- },
2336
- },
2337
- required: [
2338
- ...scopeRequired("accountId", scope.accountId),
2339
- "bucketName",
2340
- "storageClass",
2341
- ],
2342
- additionalProperties: false,
2343
- },
2344
- outputSchema: R2_BUCKET_SCHEMA,
2345
- handler: async (args, ctx) => {
2346
- const { result } = await callCloudflare(send, {
2347
- method: "PATCH",
2348
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2349
- headers: {
2350
- ...r2Headers(args),
2351
- "cf-r2-storage-class": String(args["storageClass"]),
2352
- },
2353
- }, ctx);
2354
- return projectR2Bucket(result);
2355
- },
2356
- },
2357
- {
2358
- name: "delete_r2_bucket",
2359
- description: "Permanently delete an empty R2 bucket and all of its configuration. Cloudflare refuses non-empty buckets.",
2360
- annotations: { readOnlyHint: false, destructiveHint: true },
2361
- inputSchema: {
2362
- type: "object",
2363
- properties: {
2364
- accountId: scopeProperty("accountId", scope.accountId),
2365
- bucketName: R2_BUCKET_NAME_PROPERTY,
2366
- jurisdiction: R2_JURISDICTION_PROPERTY,
2367
- },
2368
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2369
- additionalProperties: false,
2370
- },
2371
- outputSchema: {
2372
- type: "object",
2373
- properties: {
2374
- deleted: { type: "boolean" },
2375
- bucketName: { type: "string" },
2376
- },
2377
- required: ["deleted", "bucketName"],
2378
- },
2379
- handler: async (args, ctx) => {
2380
- const bucketName = requireString(args, "bucketName");
2381
- await callCloudflare(send, {
2382
- method: "DELETE",
2383
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(bucketName)}`,
2384
- headers: r2Headers(args),
2385
- }, ctx);
2386
- return { deleted: true, bucketName };
2387
- },
2388
- },
2389
- {
2390
- name: "list_r2_objects",
2391
- description: "List object keys and metadata in an R2 bucket by prefix, with delimiter grouping and cursor pagination.",
2392
- annotations: readOnly,
2393
- inputSchema: {
2394
- type: "object",
2395
- properties: {
2396
- accountId: scopeProperty("accountId", scope.accountId),
2397
- bucketName: R2_BUCKET_NAME_PROPERTY,
2398
- jurisdiction: R2_JURISDICTION_PROPERTY,
2399
- prefix: {
2400
- type: "string",
2401
- description: "Return only object keys beginning with this prefix.",
2402
- },
2403
- delimiter: {
2404
- type: "string",
2405
- minLength: 1,
2406
- maxLength: 1,
2407
- description: "One character used to group path-like keys, usually '/'.",
2408
- },
2409
- startAfter: {
2410
- type: "string",
2411
- description: "Begin after this key in lexicographic order.",
2412
- },
2413
- perPage: {
2414
- type: "integer",
2415
- minimum: 1,
2416
- maximum: 1000,
2417
- description: "Objects per request, 1 to 1000.",
2418
- },
2419
- cursor: CURSOR_INPUT_PROPERTY,
2420
- },
2421
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2422
- additionalProperties: false,
2423
- },
2424
- outputSchema: {
2425
- type: "object",
2426
- properties: {
2427
- objects: { type: "array", items: R2_OBJECT_SCHEMA },
2428
- commonPrefixes: { type: "array", items: { type: "string" } },
2429
- nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
2430
- truncated: { type: "boolean" },
2431
- },
2432
- required: ["objects", "truncated"],
2433
- },
2434
- handler: async (args, ctx) => {
2435
- const { result, resultInfo } = await callCloudflare(send, {
2436
- method: "GET",
2437
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects`,
2438
- headers: r2Headers(args),
2439
- query: {
2440
- prefix: optionalString(args, "prefix"),
2441
- delimiter: optionalString(args, "delimiter"),
2442
- start_after: optionalString(args, "startAfter"),
2443
- per_page: optionalNumber(args, "perPage"),
2444
- cursor: optionalString(args, "cursor"),
2445
- },
2446
- }, ctx);
2447
- const cursor = resultInfo?.cursor;
2448
- return {
2449
- objects: asArray(result).map(projectR2Object),
2450
- ...(Array.isArray(resultInfo?.delimited)
2451
- ? { commonPrefixes: resultInfo.delimited }
2452
- : {}),
2453
- ...(typeof cursor === "string" && cursor !== ""
2454
- ? { nextCursor: cursor }
2455
- : {}),
2456
- truncated: resultInfo?.is_truncated === true,
2457
- };
2458
- },
2459
- },
2460
- {
2461
- name: "delete_r2_object",
2462
- description: "Permanently delete one object from an R2 bucket by key.",
2463
- annotations: { readOnlyHint: false, destructiveHint: true },
2464
- inputSchema: {
2465
- type: "object",
2466
- properties: {
2467
- accountId: scopeProperty("accountId", scope.accountId),
2468
- bucketName: R2_BUCKET_NAME_PROPERTY,
2469
- jurisdiction: R2_JURISDICTION_PROPERTY,
2470
- objectKey: {
2471
- type: "string",
2472
- minLength: 1,
2473
- description: "Exact object key. Slashes are preserved as path separators.",
2474
- },
2475
- },
2476
- required: [
2477
- ...scopeRequired("accountId", scope.accountId),
2478
- "bucketName",
2479
- "objectKey",
2480
- ],
2481
- additionalProperties: false,
2482
- },
2483
- outputSchema: {
2484
- type: "object",
2485
- properties: {
2486
- deleted: { type: "boolean" },
2487
- objectKey: { type: "string" },
2488
- },
2489
- required: ["deleted", "objectKey"],
2490
- },
2491
- handler: async (args, ctx) => {
2492
- const objectKey = requireString(args, "objectKey");
2493
- await callCloudflare(send, {
2494
- method: "DELETE",
2495
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects/${encodeObjectKey(objectKey)}`,
2496
- headers: r2Headers(args),
2497
- }, ctx);
2498
- return { deleted: true, objectKey };
2499
- },
2500
- },
2501
- // No `get_r2_metrics`, `set_r2_cors`, or `delete_r2_cors` here on purpose.
2502
- // The #350 measurement found `get_r2_metrics` an unprojected,
2503
- // output-schema-less wrapper around a path, and `set_r2_cors` carrying a
2504
- // free-form `additionalProperties: true` rule body, so its schema did not
2505
- // validate the part of the call that actually fails. `delete_r2_cors`
2506
- // measured clean — a fixed `{deleted}` confirmation behind a closed output
2507
- // schema — and went anyway, as the other half of one policy pair: with the
2508
- // write unnamed, a named delete would change a CORS policy through a
2509
- // different route than setting it does. Reading a policy is still named;
2510
- // changing one takes the approval-gated raw route, exactly as
2511
- // structured-data DNS records already do.
2512
- {
2513
- name: "get_r2_cors",
2514
- description: "Get the browser CORS rules configured on an R2 bucket.",
2515
- annotations: readOnly,
2516
- inputSchema: {
2517
- type: "object",
2518
- properties: {
2519
- accountId: scopeProperty("accountId", scope.accountId),
2520
- bucketName: R2_BUCKET_NAME_PROPERTY,
2521
- jurisdiction: R2_JURISDICTION_PROPERTY,
2522
- },
2523
- required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2524
- additionalProperties: false,
2525
- },
2526
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2527
- handler: async (args, ctx) => {
2528
- const { result } = await callCloudflare(send, {
2529
- method: "GET",
2530
- path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2531
- headers: r2Headers(args),
2532
- }, ctx);
2533
- return asRecord(result);
2534
- },
2535
- },
2536
- {
2537
- name: "list_pages_projects",
2538
- description: "List Cloudflare Pages projects in an account, with their production branch and latest deployment.",
2539
- annotations: readOnly,
2540
- inputSchema: {
2541
- type: "object",
2542
- properties: {
2543
- accountId: scopeProperty("accountId", scope.accountId),
2544
- ...pagingInputProperties(1, 100, { bounds: "undocumented" }),
2545
- raw: RAW_INPUT_PROPERTY,
2546
- },
2547
- required: scopeRequired("accountId", scope.accountId),
2548
- additionalProperties: false,
2549
- },
2550
- outputSchema: listOutputSchema("projects", {
2551
- type: "object",
2552
- properties: {
2553
- name: { type: "string" },
2554
- subdomain: { type: "string" },
2555
- domains: { type: "array", items: { type: "string" } },
2556
- productionBranch: { type: "string" },
2557
- createdOn: { type: "string" },
2558
- latestDeployment: {
2559
- type: "object",
2560
- properties: {
2561
- id: { type: "string" },
2562
- environment: { type: "string" },
2563
- url: { type: "string" },
2564
- createdOn: { type: "string" },
2565
- },
2566
- },
2567
- },
2568
- required: ["name"],
2569
- }),
2570
- handler: async (args, ctx) => {
2571
- const { result, resultInfo } = await callCloudflare(send, {
2572
- method: "GET",
2573
- path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects`,
2574
- query: {
2575
- page: optionalNumber(args, "page"),
2576
- per_page: optionalNumber(args, "perPage"),
2577
- },
2578
- }, ctx);
2579
- if (args["raw"] === true)
2580
- return { projects: result, page: pageInfo(resultInfo) };
2581
- return {
2582
- projects: asArray(result).map(projectPagesProject),
2583
- page: pageInfo(resultInfo),
2584
- };
2585
- },
2586
- },
2587
- {
2588
- name: "get_pages_project",
2589
- description: "Get one Pages project, including build configuration, deployment configuration, domains, and latest deployment.",
2590
- annotations: readOnly,
2591
- inputSchema: {
2592
- type: "object",
2593
- properties: {
2594
- accountId: scopeProperty("accountId", scope.accountId),
2595
- projectName: {
2596
- type: "string",
2597
- minLength: 1,
2598
- description: "Pages project name from list_pages_projects.",
2599
- },
2600
- raw: RAW_INPUT_PROPERTY,
2601
- },
2602
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
2603
- additionalProperties: false,
2604
- },
2605
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2606
- handler: async (args, ctx) => {
2607
- const { result } = await callCloudflare(send, {
2608
- method: "GET",
2609
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}`,
2610
- }, ctx);
2611
- return args["raw"] === true ? result : projectPagesProject(result);
2612
- },
2613
- },
2614
- {
2615
- name: "list_pages_deployments",
2616
- description: "List production and preview deployments for a Pages project.",
2617
- annotations: readOnly,
2618
- inputSchema: {
2619
- type: "object",
2620
- properties: {
2621
- accountId: scopeProperty("accountId", scope.accountId),
2622
- projectName: {
2623
- type: "string",
2624
- minLength: 1,
2625
- description: "Pages project name from list_pages_projects.",
2626
- },
2627
- env: {
2628
- type: "string",
2629
- enum: ["production", "preview"],
2630
- description: "Optional deployment environment filter.",
2631
- },
2632
- ...pagingInputProperties(1, 100, { bounds: "undocumented" }),
2633
- },
2634
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
2635
- additionalProperties: false,
2636
- },
2637
- outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
2638
- handler: async (args, ctx) => {
2639
- const { result, resultInfo } = await callCloudflare(send, {
2640
- method: "GET",
2641
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments`,
2642
- query: {
2643
- env: optionalString(args, "env"),
2644
- page: optionalNumber(args, "page"),
2645
- per_page: optionalNumber(args, "perPage"),
2646
- },
2647
- }, ctx);
2648
- return {
2649
- deployments: asArray(result).map(projectPagesDeployment),
2650
- page: pageInfo(resultInfo),
2651
- };
2652
- },
2653
- },
2654
- {
2655
- name: "get_pages_deployment",
2656
- description: "Get one Pages deployment including its environment, URLs, stages, source, and build configuration.",
2657
- annotations: readOnly,
2658
- inputSchema: {
2659
- type: "object",
2660
- properties: {
2661
- accountId: scopeProperty("accountId", scope.accountId),
2662
- projectName: {
2663
- type: "string",
2664
- minLength: 1,
2665
- description: "Pages project name from list_pages_projects.",
2666
- },
2667
- deploymentId: {
2668
- type: "string",
2669
- minLength: 1,
2670
- description: "Deployment id from list_pages_deployments.",
2671
- },
2672
- raw: RAW_INPUT_PROPERTY,
2673
- },
2674
- required: [
2675
- ...scopeRequired("accountId", scope.accountId),
2676
- "projectName",
2677
- "deploymentId",
2678
- ],
2679
- additionalProperties: false,
2680
- },
2681
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2682
- handler: async (args, ctx) => {
2683
- const { result } = await callCloudflare(send, {
2684
- method: "GET",
2685
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
2686
- }, ctx);
2687
- return args["raw"] === true ? result : projectPagesDeployment(result);
2688
- },
2689
- },
2690
- {
2691
- name: "retry_pages_deployment",
2692
- description: "Retry a failed or cancelled Pages deployment using its existing source and build configuration.",
2693
- annotations: { readOnlyHint: false },
2694
- inputSchema: {
2695
- type: "object",
2696
- properties: {
2697
- accountId: scopeProperty("accountId", scope.accountId),
2698
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2699
- deploymentId: { type: "string", minLength: 1, description: "Deployment id to retry." },
2700
- },
2701
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
2702
- additionalProperties: false,
2703
- },
2704
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2705
- handler: async (args, ctx) => {
2706
- const { result } = await callCloudflare(send, {
2707
- method: "POST",
2708
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/retry`,
2709
- }, ctx);
2710
- return projectPagesDeployment(result);
2711
- },
2712
- },
2713
- {
2714
- name: "rollback_pages_deployment",
2715
- description: "Promote a previous Pages deployment to production, replacing the currently served production deployment.",
2716
- annotations: { readOnlyHint: false, destructiveHint: true },
2717
- inputSchema: {
2718
- type: "object",
2719
- properties: {
2720
- accountId: scopeProperty("accountId", scope.accountId),
2721
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2722
- deploymentId: { type: "string", minLength: 1, description: "Previous deployment id to promote." },
2723
- },
2724
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
2725
- additionalProperties: false,
2726
- },
2727
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2728
- handler: async (args, ctx) => {
2729
- const { result } = await callCloudflare(send, {
2730
- method: "POST",
2731
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/rollback`,
2732
- }, ctx);
2733
- return projectPagesDeployment(result);
2734
- },
2735
- },
2736
- {
2737
- name: "delete_pages_deployment",
2738
- description: "Permanently delete a Pages deployment and its immutable deployment URL.",
2739
- annotations: { readOnlyHint: false, destructiveHint: true },
2740
- inputSchema: {
2741
- type: "object",
2742
- properties: {
2743
- accountId: scopeProperty("accountId", scope.accountId),
2744
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2745
- deploymentId: { type: "string", minLength: 1, description: "Deployment id to delete." },
2746
- },
2747
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
2748
- additionalProperties: false,
2749
- },
2750
- outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["deleted", "deploymentId"] },
2751
- handler: async (args, ctx) => {
2752
- const deploymentId = requireString(args, "deploymentId");
2753
- await callCloudflare(send, {
2754
- method: "DELETE",
2755
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(deploymentId)}`,
2756
- }, ctx);
2757
- return { deleted: true, deploymentId };
2758
- },
2759
- },
2760
- {
2761
- name: "list_pages_domains",
2762
- description: "List custom domains attached to a Pages project and their validation status.",
2763
- annotations: readOnly,
2764
- inputSchema: {
2765
- type: "object",
2766
- properties: {
2767
- accountId: scopeProperty("accountId", scope.accountId),
2768
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2769
- },
2770
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
2771
- additionalProperties: false,
2772
- },
2773
- outputSchema: listOutputSchema("domains", OPEN_OBJECT_OUTPUT_SCHEMA),
2774
- handler: async (args, ctx) => {
2775
- const { result } = await callCloudflare(send, {
2776
- method: "GET",
2777
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
2778
- }, ctx);
2779
- return { domains: asArray(result).map(projectPagesDomain) };
2780
- },
2781
- },
2782
- {
2783
- name: "add_pages_domain",
2784
- description: "Attach a custom domain to a Pages project. DNS ownership and validation still apply.",
2785
- annotations: { readOnlyHint: false },
2786
- inputSchema: {
2787
- type: "object",
2788
- properties: {
2789
- accountId: scopeProperty("accountId", scope.accountId),
2790
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2791
- domain: { type: "string", minLength: 1, description: "Fully qualified custom domain to attach." },
2792
- },
2793
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "domain"],
2794
- additionalProperties: false,
2795
- },
2796
- outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2797
- handler: async (args, ctx) => {
2798
- const { result } = await callCloudflare(send, {
2799
- method: "POST",
2800
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
2801
- body: { name: requireString(args, "domain") },
2802
- }, ctx);
2803
- return projectPagesDomain(result);
2804
- },
2805
- },
2806
- {
2807
- name: "delete_pages_domain",
2808
- description: "Detach a custom domain from a Pages project.",
2809
- annotations: { readOnlyHint: false, destructiveHint: true },
2810
- inputSchema: {
2811
- type: "object",
2812
- properties: {
2813
- accountId: scopeProperty("accountId", scope.accountId),
2814
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2815
- domain: { type: "string", minLength: 1, description: "Custom domain to detach." },
2816
- },
2817
- required: [...scopeRequired("accountId", scope.accountId), "projectName", "domain"],
2818
- additionalProperties: false,
2819
- },
2820
- outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, domain: { type: "string" } }, required: ["deleted", "domain"] },
2821
- handler: async (args, ctx) => {
2822
- const domain = requireString(args, "domain");
2823
- await callCloudflare(send, {
2824
- method: "DELETE",
2825
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains/${encodePathSegment(domain)}`,
2826
- }, ctx);
2827
- return { deleted: true, domain };
2828
- },
2829
- },
2830
- {
2831
- name: "purge_pages_build_cache",
2832
- description: "Clear a Pages project's build cache so its next deployment rebuilds dependencies and artifacts from scratch.",
2833
- annotations: { readOnlyHint: false, destructiveHint: true },
2834
- inputSchema: {
2835
- type: "object",
2836
- properties: {
2837
- accountId: scopeProperty("accountId", scope.accountId),
2838
- projectName: { type: "string", minLength: 1, description: "Pages project name." },
2839
- },
2840
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
2841
- additionalProperties: false,
2842
- },
2843
- outputSchema: { type: "object", properties: { purged: { type: "boolean" } }, required: ["purged"] },
2844
- handler: async (args, ctx) => {
2845
- await callCloudflare(send, {
2846
- method: "POST",
2847
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/purge_build_cache`,
2848
- }, ctx);
2849
- return { purged: true };
2850
- },
2851
- },
2852
- {
2853
- name: "delete_pages_project",
2854
- description: "Permanently delete a Pages project, its deployments, and project configuration.",
2855
- annotations: { readOnlyHint: false, destructiveHint: true },
2856
- inputSchema: {
2857
- type: "object",
2858
- properties: {
2859
- accountId: scopeProperty("accountId", scope.accountId),
2860
- projectName: { type: "string", minLength: 1, description: "Pages project name to delete." },
2861
- },
2862
- required: [...scopeRequired("accountId", scope.accountId), "projectName"],
2863
- additionalProperties: false,
2864
- },
2865
- outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, projectName: { type: "string" } }, required: ["deleted", "projectName"] },
2866
- handler: async (args, ctx) => {
2867
- const projectName = requireString(args, "projectName");
2868
- await callCloudflare(send, {
2869
- method: "DELETE",
2870
- path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(projectName)}`,
2871
- }, ctx);
2872
- return { deleted: true, projectName };
2873
- },
2874
- },
2875
- {
2876
- // Additive: brings a record into being and destroys nothing, so
2877
- // `destructiveHint` stays unset. `readOnlyHint: false` already routes it
2878
- // through call_destructive_tool.
2879
- name: "create_dns_record",
2880
- description: "Create a content-based DNS record in a zone; the type enum lists the creatable types. Check list_dns_records first — Cloudflare rejects a duplicate rather than replacing it. Structured types like SRV and CAA are readable but not creatable.",
2881
- annotations: { readOnlyHint: false },
2882
- inputSchema: {
2883
- type: "object",
2884
- properties: {
2885
- zoneId: scopeProperty("zoneId", scope.zoneId),
2886
- type: {
2887
- type: "string",
2888
- enum: [...CLOUDFLARE_CONTENT_DNS_RECORD_TYPES],
2889
- description: "Record type.",
2890
- },
2891
- name: {
2892
- type: "string",
2893
- description: "Record name. Use the apex domain for the root, or a fully qualified subdomain, e.g. www.example.com.",
2894
- },
2895
- content: {
2896
- type: "string",
2897
- description: "Record value: an IPv4 address for A, IPv6 for AAAA, a hostname for CNAME/MX/NS, or the text body for TXT.",
2898
- },
2899
- ttl: {
2900
- type: "integer",
2901
- minimum: 1,
2902
- maximum: 86400,
2903
- description: "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.",
2904
- },
2905
- proxied: {
2906
- type: "boolean",
2907
- description: "Route through Cloudflare's proxy. Only A, AAAA, and CNAME records are proxiable. Defaults to false.",
2908
- },
2909
- priority: {
2910
- type: "integer",
2911
- minimum: 0,
2912
- maximum: 65535,
2913
- description: "Mail-server preference. MX records only.",
2914
- },
2915
- comment: {
2916
- type: "string",
2917
- description: "Operator-facing note stored with the record.",
2918
- },
2919
- tags: {
2920
- type: "array",
2921
- items: { type: "string" },
2922
- description: "Custom tags, available on paid plans.",
2923
- },
2924
- },
2925
- required: [
2926
- ...scopeRequired("zoneId", scope.zoneId),
2927
- "type",
2928
- "name",
2929
- "content",
2930
- ],
2931
- additionalProperties: false,
2932
- },
2933
- outputSchema: DNS_RECORD_SCHEMA,
2934
- handler: async (args, ctx) => {
2935
- const { result } = await callCloudflare(send, {
2936
- method: "POST",
2937
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
2938
- body: {
2939
- type: args["type"],
2940
- name: args["name"],
2941
- content: args["content"],
2942
- ttl: optionalNumber(args, "ttl") ?? 1,
2943
- ...(args["proxied"] !== undefined
2944
- ? { proxied: args["proxied"] }
2945
- : {}),
2946
- ...(args["priority"] !== undefined
2947
- ? { priority: args["priority"] }
2948
- : {}),
2949
- ...(args["comment"] !== undefined
2950
- ? { comment: args["comment"] }
2951
- : {}),
2952
- ...(args["tags"] !== undefined ? { tags: args["tags"] } : {}),
2953
- },
2954
- }, ctx);
2955
- return projectDnsRecord(result);
2956
- },
2957
- },
2958
- {
2959
- // Destructive: it overwrites what a record already resolves to.
2960
- name: "update_dns_record",
2961
- description: "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.",
2962
- annotations: { readOnlyHint: false, destructiveHint: true },
2963
- inputSchema: {
2964
- type: "object",
2965
- properties: {
2966
- zoneId: scopeProperty("zoneId", scope.zoneId),
2967
- recordId: {
2968
- type: "string",
2969
- description: "DNS record id, from list_dns_records.",
2970
- },
2971
- type: {
2972
- type: "string",
2973
- enum: [...CLOUDFLARE_CONTENT_DNS_RECORD_TYPES],
2974
- description: "Record type. Send it whenever content changes; Cloudflare treats type and content as a pair.",
2975
- },
2976
- name: { type: "string", description: "Fully qualified record name." },
2977
- content: { type: "string", description: "New record value." },
2978
- ttl: {
2979
- type: "integer",
2980
- minimum: 1,
2981
- maximum: 86400,
2982
- description: "Seconds; 1 means automatic, otherwise at least 60 (30 on Enterprise zones).",
2983
- },
2984
- proxied: {
2985
- type: "boolean",
2986
- description: "Route through Cloudflare's proxy. Only A, AAAA, and CNAME records are proxiable, and a proxied record must use ttl 1.",
2987
- },
2988
- priority: {
2989
- type: "integer",
2990
- minimum: 0,
2991
- maximum: 65535,
2992
- description: "Mail-server preference. MX records only.",
2993
- },
2994
- comment: {
2995
- type: "string",
2996
- description: "Operator-facing note stored with the record. Replaces the existing note.",
2997
- },
2998
- tags: {
2999
- type: "array",
3000
- items: { type: "string" },
3001
- description: "Custom tags, available on paid plans. Replaces the existing tag set rather than adding to it.",
3002
- },
3003
- },
3004
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
3005
- additionalProperties: false,
3006
- },
3007
- outputSchema: DNS_RECORD_SCHEMA,
3008
- handler: async (args, ctx) => {
3009
- const body = {};
3010
- for (const key of [
3011
- "type",
3012
- "name",
3013
- "content",
3014
- "ttl",
3015
- "proxied",
3016
- "priority",
3017
- "comment",
3018
- "tags",
3019
- ]) {
3020
- if (args[key] !== undefined)
3021
- body[key] = args[key];
3022
- }
3023
- if (Object.keys(body).length === 0) {
3024
- throw new ConnectorCallError("invalid_args", "update_dns_record needs at least one field to change besides zoneId and recordId.", {
3025
- validation: {
3026
- issues: [
3027
- {
3028
- path: "/",
3029
- code: "anyOf",
3030
- expected: "at least one of type, name, content, ttl, proxied, priority, comment, tags",
3031
- },
3032
- ],
3033
- },
3034
- });
3035
- }
3036
- const { result } = await callCloudflare(send, {
3037
- method: "PATCH",
3038
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
3039
- body,
3040
- }, ctx);
3041
- return projectDnsRecord(result);
3042
- },
3043
- },
3044
- {
3045
- name: "delete_dns_record",
3046
- description: "Delete a DNS record by id. The record stops resolving immediately and Cloudflare keeps no undo.",
3047
- annotations: { readOnlyHint: false, destructiveHint: true },
3048
- inputSchema: {
3049
- type: "object",
3050
- properties: {
3051
- zoneId: scopeProperty("zoneId", scope.zoneId),
3052
- recordId: {
3053
- type: "string",
3054
- description: "DNS record id, from list_dns_records.",
3055
- },
3056
- },
3057
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
3058
- additionalProperties: false,
3059
- },
3060
- outputSchema: {
3061
- type: "object",
3062
- properties: {
3063
- deleted: { type: "boolean" },
3064
- recordId: { type: "string" },
3065
- },
3066
- required: ["deleted", "recordId"],
3067
- },
3068
- handler: async (args, ctx) => {
3069
- const recordId = String(args["recordId"]);
3070
- await callCloudflare(send, {
3071
- method: "DELETE",
3072
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(recordId)}`,
3073
- }, ctx);
3074
- // Cloudflare answers a delete with `{ "result": { "id": ... } }` and
3075
- // nothing else; the useful acknowledgement is the boolean.
3076
- return { deleted: true, recordId };
3077
- },
3078
- },
3079
- {
3080
- name: "purge_cache",
3081
- description: "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.",
3082
- annotations: { readOnlyHint: false, destructiveHint: true },
3083
- inputSchema: {
3084
- type: "object",
3085
- properties: {
3086
- zoneId: scopeProperty("zoneId", scope.zoneId),
3087
- everything: {
3088
- type: "boolean",
3089
- description: "Purge the entire zone cache. Mutually exclusive with the targeted options below, and a real load event for the origin.",
3090
- },
3091
- files: {
3092
- type: "array",
3093
- items: { type: "string" },
3094
- minItems: 1,
3095
- maxItems: 100,
3096
- description: "Absolute URLs to purge, e.g. https://example.com/style.css. Up to 100 per request (500 on Enterprise).",
3097
- },
3098
- tags: {
3099
- type: "array",
3100
- items: { type: "string" },
3101
- minItems: 1,
3102
- maxItems: 100,
3103
- description: "Cache-Tag values to purge. Up to 100 per request; available on every plan.",
3104
- },
3105
- hosts: {
3106
- type: "array",
3107
- items: { type: "string" },
3108
- minItems: 1,
3109
- maxItems: 100,
3110
- description: "Hostnames to purge. Up to 100 per request; available on every plan.",
1611
+ }
1612
+ }, ["namespaceId", "entries"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1613
+ const { result } = await callCloudflare(send, {
1614
+ method: "PUT",
1615
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}/bulk`,
1616
+ body: args["entries"],
1617
+ }, ctx);
1618
+ return asRecord(result);
1619
+ }),
1620
+ cfTool("bulk_delete_kv_values", "Permanently delete multiple keys from a Workers KV namespace.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1621
+ namespaceId: NAMESPACE_ID_PROPERTY,
1622
+ keys: {
1623
+ type: "array",
1624
+ minItems: 1,
1625
+ maxItems: 10_000,
1626
+ items: { type: "string", minLength: 1, maxLength: 512 },
1627
+ description: "Key names to delete, up to Cloudflare's 10,000-key bulk limit.",
1628
+ }
1629
+ }, ["namespaceId", "keys"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1630
+ const { result } = await callCloudflare(send, {
1631
+ method: "POST",
1632
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}/bulk/delete`,
1633
+ body: args["keys"],
1634
+ }, ctx);
1635
+ return asRecord(result);
1636
+ }),
1637
+ cfTool("list_r2_buckets", "List R2 buckets in an account, with location and storage class.", readOnly, "accountId", scope.accountId, {
1638
+ nameContains: {
1639
+ type: "string",
1640
+ description: "Filter to buckets whose name contains this string.",
1641
+ },
1642
+ perPage: {
1643
+ type: "integer",
1644
+ minimum: 1,
1645
+ maximum: 1000,
1646
+ description: "Buckets per request, 1 to 1000. Defaults to 20.",
1647
+ },
1648
+ cursor: CURSOR_INPUT_PROPERTY,
1649
+ jurisdiction: R2_JURISDICTION_PROPERTY,
1650
+ raw: RAW_INPUT_PROPERTY
1651
+ }, [], {
1652
+ type: "object",
1653
+ properties: {
1654
+ buckets: {
1655
+ type: "array",
1656
+ items: R2_BUCKET_SCHEMA,
1657
+ },
1658
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1659
+ },
1660
+ required: ["buckets"],
1661
+ }, async (args, ctx) => {
1662
+ const { result, resultInfo } = await callCloudflare(send, {
1663
+ method: "GET",
1664
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
1665
+ query: {
1666
+ name_contains: optionalString(args, "nameContains"),
1667
+ per_page: optionalNumber(args, "perPage"),
1668
+ cursor: optionalString(args, "cursor"),
1669
+ },
1670
+ headers: r2Headers(args),
1671
+ }, ctx);
1672
+ // R2 nests its list under `buckets` rather than returning a bare array,
1673
+ // and its result_info carries a cursor instead of page counters.
1674
+ const cursor = resultInfo?.cursor;
1675
+ const next = cursorResult(cursor);
1676
+ if (args["raw"] === true)
1677
+ return { buckets: result, ...next };
1678
+ return {
1679
+ buckets: asArray(asRecord(result)["buckets"]).map(projectR2Bucket),
1680
+ ...next,
1681
+ };
1682
+ }),
1683
+ cfTool("get_r2_bucket", "Get one R2 bucket's location, jurisdiction, storage class, and creation time.", readOnly, "accountId", scope.accountId, {
1684
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1685
+ jurisdiction: R2_JURISDICTION_PROPERTY
1686
+ }, ["bucketName"], R2_BUCKET_SCHEMA, getResult(send, (args) => ({
1687
+ method: "GET",
1688
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}`,
1689
+ headers: r2Headers(args),
1690
+ }), projectR2Bucket)),
1691
+ cfTool("create_r2_bucket", "Create an R2 bucket with an optional location hint and default storage class.", { readOnlyHint: false }, "accountId", scope.accountId, {
1692
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1693
+ jurisdiction: R2_JURISDICTION_PROPERTY,
1694
+ locationHint: {
1695
+ type: "string",
1696
+ enum: ["apac", "eeur", "enam", "weur", "wnam", "oc"],
1697
+ description: "Optional placement hint for the new bucket.",
1698
+ },
1699
+ storageClass: {
1700
+ type: "string",
1701
+ enum: ["Standard", "InfrequentAccess"],
1702
+ description: "Default storage class for new objects.",
1703
+ }
1704
+ }, ["bucketName"], R2_BUCKET_SCHEMA, async (args, ctx) => {
1705
+ const { result } = await callCloudflare(send, {
1706
+ method: "POST",
1707
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
1708
+ headers: r2Headers(args),
1709
+ body: compact({
1710
+ name: requireString(args, "bucketName"),
1711
+ locationHint: args["locationHint"],
1712
+ storageClass: args["storageClass"],
1713
+ }),
1714
+ }, ctx);
1715
+ return projectR2Bucket(result);
1716
+ }),
1717
+ cfTool("update_r2_bucket", "Change the default storage class used for newly uploaded objects in an R2 bucket.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1718
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1719
+ jurisdiction: R2_JURISDICTION_PROPERTY,
1720
+ storageClass: {
1721
+ type: "string",
1722
+ enum: ["Standard", "InfrequentAccess"],
1723
+ description: "New default storage class for future uploads.",
1724
+ }
1725
+ }, ["bucketName", "storageClass"], R2_BUCKET_SCHEMA, async (args, ctx) => {
1726
+ const { result } = await callCloudflare(send, {
1727
+ method: "PATCH",
1728
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}`,
1729
+ headers: {
1730
+ ...r2Headers(args),
1731
+ "cf-r2-storage-class": String(args["storageClass"]),
1732
+ },
1733
+ }, ctx);
1734
+ return projectR2Bucket(result);
1735
+ }),
1736
+ cfTool("delete_r2_bucket", "Permanently delete an empty R2 bucket and all of its configuration. Cloudflare refuses non-empty buckets.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1737
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1738
+ jurisdiction: R2_JURISDICTION_PROPERTY
1739
+ }, ["bucketName"], {
1740
+ type: "object",
1741
+ properties: {
1742
+ deleted: { type: "boolean" },
1743
+ bucketName: { type: "string" },
1744
+ },
1745
+ required: ["deleted", "bucketName"],
1746
+ }, deleteAck(send, "bucketName", (args) => requireString(args, "bucketName"), (args, bucketName) => ({
1747
+ method: "DELETE",
1748
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(bucketName)}`,
1749
+ headers: r2Headers(args),
1750
+ }))),
1751
+ cfTool("list_r2_objects", "List object keys and metadata in an R2 bucket by prefix, with delimiter grouping and cursor pagination.", readOnly, "accountId", scope.accountId, {
1752
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1753
+ jurisdiction: R2_JURISDICTION_PROPERTY,
1754
+ prefix: {
1755
+ type: "string",
1756
+ description: "Return only object keys beginning with this prefix.",
1757
+ },
1758
+ delimiter: {
1759
+ type: "string",
1760
+ minLength: 1,
1761
+ maxLength: 1,
1762
+ description: "One character used to group path-like keys, usually '/'.",
1763
+ },
1764
+ startAfter: {
1765
+ type: "string",
1766
+ description: "Begin after this key in lexicographic order.",
1767
+ },
1768
+ perPage: {
1769
+ type: "integer",
1770
+ minimum: 1,
1771
+ maximum: 1000,
1772
+ description: "Objects per request, 1 to 1000.",
1773
+ },
1774
+ cursor: CURSOR_INPUT_PROPERTY
1775
+ }, ["bucketName"], {
1776
+ type: "object",
1777
+ properties: {
1778
+ objects: { type: "array", items: R2_OBJECT_SCHEMA },
1779
+ commonPrefixes: { type: "array", items: { type: "string" } },
1780
+ nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
1781
+ truncated: { type: "boolean" },
1782
+ },
1783
+ required: ["objects", "truncated"],
1784
+ }, async (args, ctx) => {
1785
+ const { result, resultInfo } = await callCloudflare(send, {
1786
+ method: "GET",
1787
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}/objects`,
1788
+ headers: r2Headers(args),
1789
+ query: {
1790
+ prefix: optionalString(args, "prefix"),
1791
+ delimiter: optionalString(args, "delimiter"),
1792
+ start_after: optionalString(args, "startAfter"),
1793
+ per_page: optionalNumber(args, "perPage"),
1794
+ cursor: optionalString(args, "cursor"),
1795
+ },
1796
+ }, ctx);
1797
+ const cursor = resultInfo?.cursor;
1798
+ return {
1799
+ objects: asArray(result).map(projectR2Object),
1800
+ ...(Array.isArray(resultInfo?.delimited)
1801
+ ? { commonPrefixes: resultInfo.delimited }
1802
+ : {}),
1803
+ ...cursorResult(cursor),
1804
+ truncated: resultInfo?.is_truncated === true,
1805
+ };
1806
+ }),
1807
+ cfTool("delete_r2_object", "Permanently delete one object from an R2 bucket by key.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1808
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1809
+ jurisdiction: R2_JURISDICTION_PROPERTY,
1810
+ objectKey: {
1811
+ type: "string",
1812
+ minLength: 1,
1813
+ description: "Exact object key. Slashes are preserved as path separators.",
1814
+ }
1815
+ }, ["bucketName", "objectKey"], {
1816
+ type: "object",
1817
+ properties: {
1818
+ deleted: { type: "boolean" },
1819
+ objectKey: { type: "string" },
1820
+ },
1821
+ required: ["deleted", "objectKey"],
1822
+ }, deleteAck(send, "objectKey", (args) => requireString(args, "objectKey"), (args, objectKey) => ({
1823
+ method: "DELETE",
1824
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}/objects/${encodeObjectKey(objectKey)}`,
1825
+ headers: r2Headers(args),
1826
+ }))),
1827
+ // No `get_r2_metrics`, `set_r2_cors`, or `delete_r2_cors` on purpose (#350) —
1828
+ // see documentation/cloudflare.md#what-the-named-surface-deliberately-leaves-out.
1829
+ cfTool("get_r2_cors", "Get the browser CORS rules configured on an R2 bucket.", readOnly, "accountId", scope.accountId, {
1830
+ bucketName: R2_BUCKET_NAME_PROPERTY,
1831
+ jurisdiction: R2_JURISDICTION_PROPERTY
1832
+ }, ["bucketName"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1833
+ method: "GET",
1834
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}/cors`,
1835
+ headers: r2Headers(args),
1836
+ }), asRecord)),
1837
+ cfTool("list_pages_projects", "List Cloudflare Pages projects in an account, with their production branch and latest deployment.", readOnly, "accountId", scope.accountId, {
1838
+ ...pagingInputProperties(1, 100, { bounds: "undocumented" }),
1839
+ raw: RAW_INPUT_PROPERTY
1840
+ }, [], listOutputSchema("projects", {
1841
+ type: "object",
1842
+ properties: {
1843
+ name: { type: "string" },
1844
+ subdomain: { type: "string" },
1845
+ domains: { type: "array", items: { type: "string" } },
1846
+ productionBranch: { type: "string" },
1847
+ createdOn: { type: "string" },
1848
+ latestDeployment: {
1849
+ type: "object",
1850
+ properties: {
1851
+ id: { type: "string" },
1852
+ environment: { type: "string" },
1853
+ url: { type: "string" },
1854
+ createdOn: { type: "string" },
1855
+ },
1856
+ },
1857
+ },
1858
+ required: ["name"],
1859
+ }), async (args, ctx) => {
1860
+ const { result, resultInfo } = await callCloudflare(send, {
1861
+ method: "GET",
1862
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects`,
1863
+ query: {
1864
+ page: optionalNumber(args, "page"),
1865
+ per_page: optionalNumber(args, "perPage"),
1866
+ },
1867
+ }, ctx);
1868
+ return pagedList("projects", projectPagesProject)(result, resultInfo, args["raw"] === true);
1869
+ }),
1870
+ cfTool("get_pages_project", "Get one Pages project, including build configuration, deployment configuration, domains, and latest deployment.", readOnly, "accountId", scope.accountId, {
1871
+ projectName: PROJECT_NAME_PROPERTY,
1872
+ raw: RAW_INPUT_PROPERTY
1873
+ }, ["projectName"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1874
+ method: "GET",
1875
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}`,
1876
+ }), (result, args) => args["raw"] === true ? result : projectPagesProject(result))),
1877
+ cfTool("list_pages_deployments", "List production and preview deployments for a Pages project.", readOnly, "accountId", scope.accountId, {
1878
+ projectName: PROJECT_NAME_PROPERTY,
1879
+ env: {
1880
+ type: "string",
1881
+ enum: ["production", "preview"],
1882
+ description: "Optional deployment environment filter.",
1883
+ },
1884
+ ...pagingInputProperties(1, 100, { bounds: "undocumented" })
1885
+ }, ["projectName"], listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA), async (args, ctx) => {
1886
+ const { result, resultInfo } = await callCloudflare(send, {
1887
+ method: "GET",
1888
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments`,
1889
+ query: {
1890
+ env: optionalString(args, "env"),
1891
+ page: optionalNumber(args, "page"),
1892
+ per_page: optionalNumber(args, "perPage"),
1893
+ },
1894
+ }, ctx);
1895
+ return {
1896
+ deployments: asArray(result).map(projectPagesDeployment),
1897
+ page: pageInfo(resultInfo),
1898
+ };
1899
+ }),
1900
+ cfTool("get_pages_deployment", "Get one Pages deployment including its environment, URLs, stages, source, and build configuration.", readOnly, "accountId", scope.accountId, {
1901
+ projectName: PROJECT_NAME_PROPERTY,
1902
+ deploymentId: DEPLOYMENT_ID_PROPERTY,
1903
+ raw: RAW_INPUT_PROPERTY
1904
+ }, ["projectName", "deploymentId"], OPEN_OBJECT_OUTPUT_SCHEMA, getResult(send, (args) => ({
1905
+ method: "GET",
1906
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments/${encodeURIComponent(requireString(args, "deploymentId"))}`,
1907
+ }), (result, args) => args["raw"] === true ? result : projectPagesDeployment(result))),
1908
+ cfTool("retry_pages_deployment", "Retry a failed or cancelled Pages deployment using its existing source and build configuration.", { readOnlyHint: false }, "accountId", scope.accountId, {
1909
+ projectName: PAGES_PROJECT_NAME_PROPERTY,
1910
+ deploymentId: RETRY_DEPLOYMENT_ID_PROPERTY
1911
+ }, ["projectName", "deploymentId"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1912
+ const { result } = await callCloudflare(send, {
1913
+ method: "POST",
1914
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments/${encodeURIComponent(requireString(args, "deploymentId"))}/retry`,
1915
+ }, ctx);
1916
+ return projectPagesDeployment(result);
1917
+ }),
1918
+ cfTool("rollback_pages_deployment", "Promote a previous Pages deployment to production, replacing the currently served production deployment.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1919
+ projectName: PAGES_PROJECT_NAME_PROPERTY,
1920
+ deploymentId: ROLLBACK_DEPLOYMENT_ID_PROPERTY
1921
+ }, ["projectName", "deploymentId"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1922
+ const { result } = await callCloudflare(send, {
1923
+ method: "POST",
1924
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments/${encodeURIComponent(requireString(args, "deploymentId"))}/rollback`,
1925
+ }, ctx);
1926
+ return projectPagesDeployment(result);
1927
+ }),
1928
+ cfTool("delete_pages_deployment", "Permanently delete a Pages deployment and its immutable deployment URL.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1929
+ projectName: PAGES_PROJECT_NAME_PROPERTY,
1930
+ deploymentId: DELETE_DEPLOYMENT_ID_PROPERTY
1931
+ }, ["projectName", "deploymentId"], { type: "object", properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["deleted", "deploymentId"] }, deleteAck(send, "deploymentId", (args) => requireString(args, "deploymentId"), (args, deploymentId) => ({
1932
+ method: "DELETE",
1933
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments/${encodeURIComponent(deploymentId)}`,
1934
+ }))),
1935
+ cfTool("list_pages_domains", "List custom domains attached to a Pages project and their validation status.", readOnly, "accountId", scope.accountId, {
1936
+ projectName: PAGES_PROJECT_NAME_PROPERTY
1937
+ }, ["projectName"], listOutputSchema("domains", OPEN_OBJECT_OUTPUT_SCHEMA), async (args, ctx) => {
1938
+ const { result } = await callCloudflare(send, {
1939
+ method: "GET",
1940
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/domains`,
1941
+ }, ctx);
1942
+ return { domains: asArray(result).map(projectPagesDomain) };
1943
+ }),
1944
+ cfTool("add_pages_domain", "Attach a custom domain to a Pages project. DNS ownership and validation still apply.", { readOnlyHint: false }, "accountId", scope.accountId, {
1945
+ projectName: PAGES_PROJECT_NAME_PROPERTY,
1946
+ domain: { type: "string", minLength: 1, description: "Fully qualified custom domain to attach." }
1947
+ }, ["projectName", "domain"], OPEN_OBJECT_OUTPUT_SCHEMA, async (args, ctx) => {
1948
+ const { result } = await callCloudflare(send, {
1949
+ method: "POST",
1950
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/domains`,
1951
+ body: { name: requireString(args, "domain") },
1952
+ }, ctx);
1953
+ return projectPagesDomain(result);
1954
+ }),
1955
+ cfTool("delete_pages_domain", "Detach a custom domain from a Pages project.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1956
+ projectName: PAGES_PROJECT_NAME_PROPERTY,
1957
+ domain: { type: "string", minLength: 1, description: "Custom domain to detach." }
1958
+ }, ["projectName", "domain"], { type: "object", properties: { deleted: { type: "boolean" }, domain: { type: "string" } }, required: ["deleted", "domain"] }, deleteAck(send, "domain", (args) => requireString(args, "domain"), (args, domain) => ({
1959
+ method: "DELETE",
1960
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/domains/${encodeURIComponent(domain)}`,
1961
+ }))),
1962
+ cfTool("purge_pages_build_cache", "Clear a Pages project's build cache so its next deployment rebuilds dependencies and artifacts from scratch.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1963
+ projectName: PAGES_PROJECT_NAME_PROPERTY
1964
+ }, ["projectName"], { type: "object", properties: { purged: { type: "boolean" } }, required: ["purged"] }, async (args, ctx) => {
1965
+ await callCloudflare(send, {
1966
+ method: "POST",
1967
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/purge_build_cache`,
1968
+ }, ctx);
1969
+ return { purged: true };
1970
+ }),
1971
+ cfTool("delete_pages_project", "Permanently delete a Pages project, its deployments, and project configuration.", { readOnlyHint: false, destructiveHint: true }, "accountId", scope.accountId, {
1972
+ projectName: DELETE_PROJECT_NAME_PROPERTY
1973
+ }, ["projectName"], { type: "object", properties: { deleted: { type: "boolean" }, projectName: { type: "string" } }, required: ["deleted", "projectName"] }, deleteAck(send, "projectName", (args) => requireString(args, "projectName"), (args, projectName) => ({
1974
+ method: "DELETE",
1975
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(projectName)}`,
1976
+ }))),
1977
+ cfTool(
1978
+ // Additive: brings a record into being and destroys nothing, so
1979
+ // `destructiveHint` stays unset. `readOnlyHint: false` already routes it
1980
+ // through call_destructive_tool.
1981
+ "create_dns_record", "Create a content-based DNS record in a zone; the type enum lists the creatable types. Check list_dns_records first — Cloudflare rejects a duplicate rather than replacing it. Structured types like SRV and CAA are readable but not creatable.", { readOnlyHint: false }, "zoneId", scope.zoneId, {
1982
+ type: {
1983
+ type: "string",
1984
+ enum: [...CLOUDFLARE_CONTENT_DNS_RECORD_TYPES],
1985
+ description: "Record type.",
1986
+ },
1987
+ name: {
1988
+ type: "string",
1989
+ description: "Record name. Use the apex domain for the root, or a fully qualified subdomain, e.g. www.example.com.",
1990
+ },
1991
+ content: {
1992
+ type: "string",
1993
+ description: "Record value: an IPv4 address for A, IPv6 for AAAA, a hostname for CNAME/MX/NS, or the text body for TXT.",
1994
+ },
1995
+ ttl: {
1996
+ type: "integer",
1997
+ minimum: 1,
1998
+ maximum: 86400,
1999
+ description: "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.",
2000
+ },
2001
+ proxied: {
2002
+ type: "boolean",
2003
+ description: "Route through Cloudflare's proxy. Only A, AAAA, and CNAME records are proxiable. Defaults to false.",
2004
+ },
2005
+ priority: {
2006
+ type: "integer",
2007
+ minimum: 0,
2008
+ maximum: 65535,
2009
+ description: "Mail-server preference. MX records only.",
2010
+ },
2011
+ comment: {
2012
+ type: "string",
2013
+ description: "Operator-facing note stored with the record.",
2014
+ },
2015
+ tags: {
2016
+ type: "array",
2017
+ items: { type: "string" },
2018
+ description: "Custom tags, available on paid plans.",
2019
+ }
2020
+ }, ["type", "name", "content"], DNS_RECORD_SCHEMA, async (args, ctx) => {
2021
+ const { result } = await callCloudflare(send, {
2022
+ method: "POST",
2023
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
2024
+ body: compact({
2025
+ type: args["type"],
2026
+ name: args["name"],
2027
+ content: args["content"],
2028
+ ttl: optionalNumber(args, "ttl") ?? 1,
2029
+ proxied: args["proxied"],
2030
+ priority: args["priority"],
2031
+ comment: args["comment"],
2032
+ tags: args["tags"],
2033
+ }),
2034
+ }, ctx);
2035
+ return projectDnsRecord(result);
2036
+ }),
2037
+ cfTool(
2038
+ // Destructive: it overwrites what a record already resolves to.
2039
+ "update_dns_record", "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.", { readOnlyHint: false, destructiveHint: true }, "zoneId", scope.zoneId, {
2040
+ recordId: RECORD_ID_PROPERTY,
2041
+ type: {
2042
+ type: "string",
2043
+ enum: [...CLOUDFLARE_CONTENT_DNS_RECORD_TYPES],
2044
+ description: "Record type. Send it whenever content changes; Cloudflare treats type and content as a pair.",
2045
+ },
2046
+ name: { type: "string", description: "Fully qualified record name." },
2047
+ content: { type: "string", description: "New record value." },
2048
+ ttl: {
2049
+ type: "integer",
2050
+ minimum: 1,
2051
+ maximum: 86400,
2052
+ description: "Seconds; 1 means automatic, otherwise at least 60 (30 on Enterprise zones).",
2053
+ },
2054
+ proxied: {
2055
+ type: "boolean",
2056
+ description: "Route through Cloudflare's proxy. Only A, AAAA, and CNAME records are proxiable, and a proxied record must use ttl 1.",
2057
+ },
2058
+ priority: {
2059
+ type: "integer",
2060
+ minimum: 0,
2061
+ maximum: 65535,
2062
+ description: "Mail-server preference. MX records only.",
2063
+ },
2064
+ comment: {
2065
+ type: "string",
2066
+ description: "Operator-facing note stored with the record. Replaces the existing note.",
2067
+ },
2068
+ tags: {
2069
+ type: "array",
2070
+ items: { type: "string" },
2071
+ description: "Custom tags, available on paid plans. Replaces the existing tag set rather than adding to it.",
2072
+ }
2073
+ }, ["recordId"], DNS_RECORD_SCHEMA, async (args, ctx) => {
2074
+ const body = {};
2075
+ for (const key of [
2076
+ "type",
2077
+ "name",
2078
+ "content",
2079
+ "ttl",
2080
+ "proxied",
2081
+ "priority",
2082
+ "comment",
2083
+ "tags",
2084
+ ]) {
2085
+ if (args[key] !== undefined)
2086
+ body[key] = args[key];
2087
+ }
2088
+ if (Object.keys(body).length === 0) {
2089
+ throw new ConnectorCallError("invalid_args", "update_dns_record needs at least one field to change besides zoneId and recordId.", {
2090
+ validation: {
2091
+ issues: [
2092
+ {
2093
+ path: "/",
2094
+ code: "anyOf",
2095
+ expected: "at least one of type, name, content, ttl, proxied, priority, comment, tags",
2096
+ },
2097
+ ],
3111
2098
  },
3112
- prefixes: {
3113
- type: "array",
3114
- items: { type: "string" },
3115
- minItems: 1,
3116
- maxItems: 100,
3117
- description: "URL prefixes to purge, e.g. example.com/assets. Up to 100 per request; available on every plan.",
2099
+ });
2100
+ }
2101
+ const { result } = await callCloudflare(send, {
2102
+ method: "PATCH",
2103
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
2104
+ body,
2105
+ }, ctx);
2106
+ return projectDnsRecord(result);
2107
+ }),
2108
+ cfTool("delete_dns_record", "Delete a DNS record by id. The record stops resolving immediately and Cloudflare keeps no undo.", { readOnlyHint: false, destructiveHint: true }, "zoneId", scope.zoneId, {
2109
+ recordId: RECORD_ID_PROPERTY
2110
+ }, ["recordId"], {
2111
+ type: "object",
2112
+ properties: {
2113
+ deleted: { type: "boolean" },
2114
+ recordId: { type: "string" },
2115
+ },
2116
+ required: ["deleted", "recordId"],
2117
+ }, deleteAck(send, "recordId", (args) => String(args["recordId"]), (args, recordId) => ({
2118
+ method: "DELETE",
2119
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(recordId)}`,
2120
+ }))),
2121
+ cfTool("purge_cache", "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.", { readOnlyHint: false, destructiveHint: true }, "zoneId", scope.zoneId, {
2122
+ everything: {
2123
+ type: "boolean",
2124
+ description: "Purge the entire zone cache. Mutually exclusive with the targeted options below, and a real load event for the origin.",
2125
+ },
2126
+ files: {
2127
+ type: "array",
2128
+ items: { type: "string" },
2129
+ minItems: 1,
2130
+ maxItems: 100,
2131
+ description: "Absolute URLs to purge, e.g. https://example.com/style.css. Up to 100 per request (500 on Enterprise).",
2132
+ },
2133
+ tags: {
2134
+ type: "array",
2135
+ items: { type: "string" },
2136
+ minItems: 1,
2137
+ maxItems: 100,
2138
+ description: "Cache-Tag values to purge. Up to 100 per request; available on every plan.",
2139
+ },
2140
+ hosts: {
2141
+ type: "array",
2142
+ items: { type: "string" },
2143
+ minItems: 1,
2144
+ maxItems: 100,
2145
+ description: "Hostnames to purge. Up to 100 per request; available on every plan.",
2146
+ },
2147
+ prefixes: {
2148
+ type: "array",
2149
+ items: { type: "string" },
2150
+ minItems: 1,
2151
+ maxItems: 100,
2152
+ description: "URL prefixes to purge, e.g. example.com/assets. Up to 100 per request; available on every plan.",
2153
+ }
2154
+ }, [], {
2155
+ type: "object",
2156
+ properties: {
2157
+ purged: { type: "boolean" },
2158
+ zoneId: { type: "string" },
2159
+ scope: {
2160
+ type: "string",
2161
+ description: "Which variant ran: everything, files, tags, hosts, or prefixes.",
2162
+ },
2163
+ },
2164
+ required: ["purged", "zoneId", "scope"],
2165
+ }, async (args, ctx) => {
2166
+ const zoneId = zoneArg(args);
2167
+ const targeted = ["files", "tags", "hosts", "prefixes"].filter((key) => Array.isArray(args[key]) && args[key].length > 0);
2168
+ const everything = args["everything"] === true;
2169
+ // Cloudflare's purge body accepts exactly one variant. Refusing here
2170
+ // turns a confusing provider 400 into a schema-shaped failure.
2171
+ if (everything && targeted.length > 0) {
2172
+ throw new ConnectorCallError("invalid_args", "purge_cache takes either everything: true or one targeted list, never both.", {
2173
+ validation: {
2174
+ issues: [
2175
+ {
2176
+ path: "/everything",
2177
+ code: "oneOf",
2178
+ expected: "everything: true alone, or exactly one of files, tags, hosts, prefixes",
2179
+ },
2180
+ ],
3118
2181
  },
3119
- },
3120
- required: scopeRequired("zoneId", scope.zoneId),
3121
- additionalProperties: false,
3122
- },
3123
- outputSchema: {
3124
- type: "object",
3125
- properties: {
3126
- purged: { type: "boolean" },
3127
- zoneId: { type: "string" },
3128
- scope: {
3129
- type: "string",
3130
- description: "Which variant ran: everything, files, tags, hosts, or prefixes.",
2182
+ });
2183
+ }
2184
+ if (!everything && targeted.length !== 1) {
2185
+ throw new ConnectorCallError("invalid_args", targeted.length === 0
2186
+ ? "purge_cache needs everything: true or one of files, tags, hosts, or prefixes."
2187
+ : `purge_cache takes exactly one targeted list; received ${targeted.join(", ")}.`, {
2188
+ validation: {
2189
+ issues: [
2190
+ {
2191
+ path: "/",
2192
+ code: "oneOf",
2193
+ expected: "everything: true, or exactly one of files, tags, hosts, prefixes",
2194
+ },
2195
+ ],
3131
2196
  },
3132
- },
3133
- required: ["purged", "zoneId", "scope"],
3134
- },
3135
- handler: async (args, ctx) => {
3136
- const zoneId = zoneArg(args);
3137
- const targeted = ["files", "tags", "hosts", "prefixes"].filter((key) => Array.isArray(args[key]) && args[key].length > 0);
3138
- const everything = args["everything"] === true;
3139
- // Cloudflare's purge body accepts exactly one variant. Refusing here
3140
- // turns a confusing provider 400 into a schema-shaped failure.
3141
- if (everything && targeted.length > 0) {
3142
- throw new ConnectorCallError("invalid_args", "purge_cache takes either everything: true or one targeted list, never both.", {
3143
- validation: {
3144
- issues: [
3145
- {
3146
- path: "/everything",
3147
- code: "oneOf",
3148
- expected: "everything: true alone, or exactly one of files, tags, hosts, prefixes",
3149
- },
3150
- ],
3151
- },
3152
- });
3153
- }
3154
- if (!everything && targeted.length !== 1) {
3155
- throw new ConnectorCallError("invalid_args", targeted.length === 0
3156
- ? "purge_cache needs everything: true or one of files, tags, hosts, or prefixes."
3157
- : `purge_cache takes exactly one targeted list; received ${targeted.join(", ")}.`, {
3158
- validation: {
3159
- issues: [
3160
- {
3161
- path: "/",
3162
- code: "oneOf",
3163
- expected: "everything: true, or exactly one of files, tags, hosts, prefixes",
3164
- },
3165
- ],
3166
- },
3167
- });
3168
- }
3169
- const variant = everything ? "everything" : targeted[0];
3170
- await callCloudflare(send, {
3171
- method: "POST",
3172
- path: `/zones/${encodeURIComponent(zoneId)}/purge_cache`,
3173
- body: everything
3174
- ? { purge_everything: true }
3175
- : { [variant]: args[variant] },
3176
- }, ctx);
3177
- return { purged: true, zoneId, scope: variant };
3178
- },
3179
- },
2197
+ });
2198
+ }
2199
+ const variant = everything ? "everything" : targeted[0];
2200
+ await callCloudflare(send, {
2201
+ method: "POST",
2202
+ path: `/zones/${encodeURIComponent(zoneId)}/purge_cache`,
2203
+ body: everything
2204
+ ? { purge_everything: true }
2205
+ : { [variant]: args[variant] },
2206
+ }, ctx);
2207
+ return { purged: true, zoneId, scope: variant };
2208
+ }),
3180
2209
  ];
3181
- return tools.map((tool) => ({
3182
- ...tool,
3183
- handler: (args, ctx) => tool.handler(args, withAuthentication(ctx, authentication)),
3184
- }));
2210
+ return tools;
3185
2211
  }
3186
2212
  function usageGuide(purpose, scope, instructions, authentication) {
3187
2213
  const accountInstructions = instructions?.trim();
@@ -3228,7 +2254,7 @@ export function cloudflare(id, options) {
3228
2254
  throw new Error('cloudflare() authentication must be "apiToken" or "globalApiKey".');
3229
2255
  }
3230
2256
  const scope = {
3231
- send: cloudflareTransport(options.baseUrl?.trim() || CLOUDFLARE_API_BASE),
2257
+ send: cloudflareTransport(options.baseUrl?.trim() || CLOUDFLARE_API_BASE, authentication),
3232
2258
  accountId: options.accountId?.trim() || undefined,
3233
2259
  zoneId: options.zoneId?.trim() || undefined,
3234
2260
  };
@@ -3256,49 +2282,32 @@ export function cloudflare(id, options) {
3256
2282
  ...(authentication === "apiToken"
3257
2283
  ? {
3258
2284
  async testCredential(value, ctx) {
3259
- try {
3260
- const { result } = await callCloudflare(scope.send, { method: "GET", path: "/user/tokens/verify" }, withAuthentication({
3261
- ...ctx,
3262
- credential: {
3263
- get: async () => value,
3264
- getAll: async () => ({ value }),
3265
- },
3266
- }, authentication));
2285
+ return await testCloudflareCredential(scope.send, { method: "GET", path: "/user/tokens/verify" }, {
2286
+ ...ctx,
2287
+ credential: {
2288
+ get: async () => value,
2289
+ getAll: async () => ({ value }),
2290
+ },
2291
+ }, (result) => {
3267
2292
  const status = asRecord(result)["status"];
3268
2293
  return status === "active"
3269
2294
  ? { ok: true, message: "Token verified: active." }
3270
2295
  : { ok: false, message: `Token status is "${String(status)}".` };
3271
- }
3272
- catch (error) {
3273
- return {
3274
- ok: false,
3275
- message: error instanceof Error ? error.message : String(error),
3276
- };
3277
- }
2296
+ });
3278
2297
  },
3279
2298
  }
3280
2299
  : {
3281
2300
  async testCredentials(values, ctx) {
3282
- try {
3283
- const { result } = await callCloudflare(scope.send, { method: "GET", path: "/user" }, withAuthentication({
3284
- ...ctx,
3285
- credential: {
3286
- get: async (field) => field ? values[field] ?? null : null,
3287
- getAll: async () => values,
3288
- },
3289
- }, authentication));
3290
- const email = asRecord(result)["email"];
3291
- return {
3292
- ok: true,
3293
- message: `Global API Key verified for ${String(email)}.`,
3294
- };
3295
- }
3296
- catch (error) {
3297
- return {
3298
- ok: false,
3299
- message: error instanceof Error ? error.message : String(error),
3300
- };
3301
- }
2301
+ return await testCloudflareCredential(scope.send, { method: "GET", path: "/user" }, {
2302
+ ...ctx,
2303
+ credential: {
2304
+ get: async (field) => field ? values[field] ?? null : null,
2305
+ getAll: async () => values,
2306
+ },
2307
+ }, (result) => ({
2308
+ ok: true,
2309
+ message: `Global API Key verified for ${String(asRecord(result)["email"])}.`,
2310
+ }));
3302
2311
  },
3303
2312
  }),
3304
2313
  });