@zackbart/connecta 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,7 +67,8 @@ export const CLOUDFLARE_DNS_RECORD_TYPES = [
67
67
  * free-form `data` passthrough — exactly the untyped `{}` this connection
68
68
  * exists to avoid — or thirteen more hand-written schemas for record types
69
69
  * that are rare in the day-to-day work this surface is for. They remain fully
70
- * readable and filterable; only creating and updating them is out of scope.
70
+ * readable and filterable; only the named create/update tools omit them. The
71
+ * guarded raw mutation tool remains available for their documented bodies.
71
72
  */
72
73
  export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
73
74
  "A",
@@ -134,7 +135,7 @@ function admissionPolicy(maxConcurrency: number): ConnectorCallAdmissionPolicy {
134
135
  const DEFAULT_CREDENTIAL: ConnectorCredentialConfig = {
135
136
  label: "Cloudflare API token",
136
137
  description:
137
- "A scoped API token (My Profile → API Tokens → Create Token), not a Global API Key. Grant only the permissions the deployment needs: zone-scoped \"Zone Read\" and \"DNS Write\" for DNS work and \"Cache Purge\" for purges; account-scoped \"Workers Scripts Read\", \"Workers KV Storage Read\", \"Workers R2 Storage Read\", or \"Cloudflare Pages Read\" for the platform reads.",
138
+ "A scoped API token (My Profile → API Tokens → Create Token), not a Global API Key. Grant only the permissions the deployment needs: zone-scoped \"Zone Read\", \"Zone Settings Write\", \"DNS Write\", \"Cache Purge\", and the phase-specific Rules product Read permissions as needed; account-scoped \"Workers Scripts Read/Write\", \"Workers KV Storage Read/Write\", \"Workers R2 Storage Read/Write\", or \"Cloudflare Pages Read/Write\" for the platform tools.",
138
139
  placeholder: "Paste API token",
139
140
  };
140
141
 
@@ -154,6 +155,9 @@ interface CloudflareResultInfo {
154
155
  total_pages?: number;
155
156
  /** Cursor-paginated endpoints (R2 buckets, KV keys) report this instead. */
156
157
  cursor?: string;
158
+ is_truncated?: boolean;
159
+ delimited?: string[];
160
+ cursors?: { after?: string; before?: string };
157
161
  }
158
162
 
159
163
  interface CloudflareEnvelope {
@@ -299,10 +303,12 @@ function failureFor(
299
303
  // --- The request path --------------------------------------------------------
300
304
 
301
305
  interface RequestSpec {
302
- method: "GET" | "POST" | "PATCH" | "DELETE";
306
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
303
307
  path: string;
304
308
  query?: Record<string, string | number | boolean | undefined>;
309
+ headers?: Record<string, string | undefined>;
305
310
  body?: unknown;
311
+ rawBody?: BodyInit;
306
312
  }
307
313
 
308
314
  interface CloudflareResponse {
@@ -330,12 +336,15 @@ function buildUrl(base: string, spec: RequestSpec): string {
330
336
  return url.toString();
331
337
  }
332
338
 
333
- async function callCloudflare(
339
+ async function fetchCloudflare(
334
340
  base: string,
335
341
  spec: RequestSpec,
336
342
  ctx: ConnectorContext,
337
- ): Promise<CloudflareResponse> {
343
+ ): Promise<Response> {
338
344
  const token = await readToken(ctx);
345
+ if (spec.body !== undefined && spec.rawBody !== undefined) {
346
+ throw new Error("A Cloudflare request cannot have both JSON and raw bodies.");
347
+ }
339
348
  let response: Response;
340
349
  try {
341
350
  response = await fetch(buildUrl(base, spec), {
@@ -346,10 +355,17 @@ async function callCloudflare(
346
355
  ...(spec.body !== undefined
347
356
  ? { "Content-Type": "application/json" }
348
357
  : {}),
358
+ ...Object.fromEntries(
359
+ Object.entries(spec.headers ?? {}).filter(
360
+ (entry): entry is [string, string] => entry[1] !== undefined,
361
+ ),
362
+ ),
349
363
  },
350
364
  ...(spec.body !== undefined
351
365
  ? { body: JSON.stringify(spec.body) }
352
- : {}),
366
+ : spec.rawBody !== undefined
367
+ ? { body: spec.rawBody }
368
+ : {}),
353
369
  ...(ctx.signal ? { signal: ctx.signal } : {}),
354
370
  });
355
371
  } catch (cause) {
@@ -361,6 +377,15 @@ async function callCloudflare(
361
377
  { cause },
362
378
  );
363
379
  }
380
+ return response;
381
+ }
382
+
383
+ async function callCloudflare(
384
+ base: string,
385
+ spec: RequestSpec,
386
+ ctx: ConnectorContext,
387
+ ): Promise<CloudflareResponse> {
388
+ const response = await fetchCloudflare(base, spec, ctx);
364
389
 
365
390
  let envelope: CloudflareEnvelope;
366
391
  try {
@@ -386,6 +411,46 @@ async function callCloudflare(
386
411
  };
387
412
  }
388
413
 
414
+ function base64FromBytes(bytes: Uint8Array): string {
415
+ let binary = "";
416
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
417
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
418
+ }
419
+ return btoa(binary);
420
+ }
421
+
422
+ async function callCloudflareContent(
423
+ base: string,
424
+ spec: RequestSpec,
425
+ ctx: ConnectorContext,
426
+ responseType: "text" | "base64",
427
+ ): Promise<JsonRecord> {
428
+ const response = await fetchCloudflare(base, spec, ctx);
429
+ if (!response.ok) {
430
+ let errors: CloudflareEnvelopeError[] = [];
431
+ try {
432
+ const envelope = (await response.clone().json()) as CloudflareEnvelope;
433
+ if (Array.isArray(envelope.errors)) errors = envelope.errors;
434
+ } catch {
435
+ // A raw or gateway error body has no structured detail to preserve.
436
+ }
437
+ throw failureFor(response.status, response.headers, errors);
438
+ }
439
+ const common = {
440
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
441
+ ...(response.headers.get("etag")
442
+ ? { etag: response.headers.get("etag")! }
443
+ : {}),
444
+ };
445
+ if (responseType === "text") {
446
+ return { ...common, text: await response.text() };
447
+ }
448
+ return {
449
+ ...common,
450
+ base64: base64FromBytes(new Uint8Array(await response.arrayBuffer())),
451
+ };
452
+ }
453
+
389
454
  // --- Projections -------------------------------------------------------------
390
455
 
391
456
  interface PageInfo {
@@ -516,12 +581,118 @@ function projectR2Bucket(value: unknown): JsonRecord {
516
581
  ...(bucket["storage_class"] !== undefined
517
582
  ? { storageClass: bucket["storage_class"] }
518
583
  : {}),
584
+ ...(bucket["jurisdiction"] !== undefined
585
+ ? { jurisdiction: bucket["jurisdiction"] }
586
+ : {}),
519
587
  ...(bucket["creation_date"] !== undefined
520
588
  ? { creationDate: bucket["creation_date"] }
521
589
  : {}),
522
590
  };
523
591
  }
524
592
 
593
+ function projectR2Object(value: unknown): JsonRecord {
594
+ const object = asRecord(value);
595
+ return {
596
+ key: object["key"],
597
+ size: object["size"],
598
+ etag: object["etag"],
599
+ lastModified: object["last_modified"],
600
+ ...(object["storage_class"] !== undefined
601
+ ? { storageClass: object["storage_class"] }
602
+ : {}),
603
+ ...(object["http_metadata"] !== undefined
604
+ ? { httpMetadata: object["http_metadata"] }
605
+ : {}),
606
+ ...(object["custom_metadata"] !== undefined
607
+ ? { customMetadata: object["custom_metadata"] }
608
+ : {}),
609
+ };
610
+ }
611
+
612
+ function projectKvKey(value: unknown): JsonRecord {
613
+ const key = asRecord(value);
614
+ return {
615
+ name: key["name"],
616
+ ...(key["expiration"] !== undefined ? { expiration: key["expiration"] } : {}),
617
+ ...(key["metadata"] !== undefined ? { metadata: key["metadata"] } : {}),
618
+ };
619
+ }
620
+
621
+ function projectWorkerDeployment(value: unknown): JsonRecord {
622
+ const deployment = asRecord(value);
623
+ return {
624
+ id: deployment["id"],
625
+ ...(deployment["created_on"] !== undefined
626
+ ? { createdOn: deployment["created_on"] }
627
+ : {}),
628
+ ...(deployment["source"] !== undefined ? { source: deployment["source"] } : {}),
629
+ ...(deployment["strategy"] !== undefined
630
+ ? { strategy: deployment["strategy"] }
631
+ : {}),
632
+ ...(deployment["versions"] !== undefined
633
+ ? { versions: deployment["versions"] }
634
+ : {}),
635
+ };
636
+ }
637
+
638
+ function projectPagesDeployment(value: unknown): JsonRecord {
639
+ const deployment = asRecord(value);
640
+ return {
641
+ id: deployment["id"],
642
+ ...(deployment["project_name"] !== undefined
643
+ ? { projectName: deployment["project_name"] }
644
+ : {}),
645
+ ...(deployment["environment"] !== undefined
646
+ ? { environment: deployment["environment"] }
647
+ : {}),
648
+ ...(deployment["url"] !== undefined ? { url: deployment["url"] } : {}),
649
+ ...(deployment["aliases"] !== undefined ? { aliases: deployment["aliases"] } : {}),
650
+ ...(deployment["stage"] !== undefined ? { stage: deployment["stage"] } : {}),
651
+ ...(deployment["latest_stage"] !== undefined
652
+ ? { latestStage: deployment["latest_stage"] }
653
+ : {}),
654
+ ...(deployment["created_on"] !== undefined
655
+ ? { createdOn: deployment["created_on"] }
656
+ : {}),
657
+ ...(deployment["modified_on"] !== undefined
658
+ ? { modifiedOn: deployment["modified_on"] }
659
+ : {}),
660
+ };
661
+ }
662
+
663
+ function projectPagesDomain(value: unknown): JsonRecord {
664
+ const domain = asRecord(value);
665
+ return {
666
+ id: domain["id"],
667
+ name: domain["name"],
668
+ ...(domain["status"] !== undefined ? { status: domain["status"] } : {}),
669
+ ...(domain["verification_data"] !== undefined
670
+ ? { verificationData: domain["verification_data"] }
671
+ : {}),
672
+ ...(domain["created_on"] !== undefined
673
+ ? { createdOn: domain["created_on"] }
674
+ : {}),
675
+ };
676
+ }
677
+
678
+ function projectRuleset(value: unknown): JsonRecord {
679
+ const ruleset = asRecord(value);
680
+ return {
681
+ id: ruleset["id"],
682
+ name: ruleset["name"],
683
+ kind: ruleset["kind"],
684
+ phase: ruleset["phase"],
685
+ ...(ruleset["description"] !== undefined
686
+ ? { description: ruleset["description"] }
687
+ : {}),
688
+ ...(ruleset["version"] !== undefined ? { version: ruleset["version"] } : {}),
689
+ ...(ruleset["last_updated"] !== undefined
690
+ ? { lastUpdated: ruleset["last_updated"] }
691
+ : {}),
692
+ ...(ruleset["rules"] !== undefined ? { rules: ruleset["rules"] } : {}),
693
+ };
694
+ }
695
+
525
696
  function projectPagesProject(value: unknown): JsonRecord {
526
697
  const project = asRecord(value);
527
698
  const latest = asRecord(project["latest_deployment"]);
@@ -754,6 +925,287 @@ function optionalNumber(args: JsonRecord, key: string): number | undefined {
754
925
  return typeof value === "number" ? value : undefined;
755
926
  }
756
927
 
928
+ function optionalBoolean(args: JsonRecord, key: string): boolean | undefined {
929
+ const value = args[key];
930
+ return typeof value === "boolean" ? value : undefined;
931
+ }
932
+
933
+ function requireString(args: JsonRecord, key: string): string {
934
+ const value = optionalString(args, key);
935
+ if (value) return value;
936
+ throw new ConnectorCallError("invalid_args", `${key} must not be blank.`);
937
+ }
938
+
939
+ function encodePathSegment(value: string): string {
940
+ return encodeURIComponent(value);
941
+ }
942
+
943
+ function encodeObjectKey(value: string): string {
944
+ return value
945
+ .split("/")
946
+ .map((segment) => {
947
+ if (segment === "." || segment === "..") {
948
+ throw new ConnectorCallError(
949
+ "invalid_args",
950
+ "objectKey cannot contain '.' or '..' path segments because URL normalization would change the target resource.",
951
+ );
952
+ }
953
+ return encodeURIComponent(segment);
954
+ })
955
+ .join("/");
956
+ }
957
+
958
+ function cloudflareApiPath(value: unknown): string {
959
+ if (typeof value !== "string") {
960
+ throw new ConnectorCallError("invalid_args", "path must be a string.");
961
+ }
962
+ const path = value.trim();
963
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\")) {
964
+ throw new ConnectorCallError(
965
+ "invalid_args",
966
+ "path must be a relative Cloudflare v4 path beginning with one slash and containing no backslashes.",
967
+ );
968
+ }
969
+ if (path.includes("?") || path.includes("#")) {
970
+ throw new ConnectorCallError(
971
+ "invalid_args",
972
+ "Put query parameters in the query array; path cannot contain '?' or '#'.",
973
+ );
974
+ }
975
+ for (const segment of path.split("/")) {
976
+ let decoded = segment;
977
+ let stable = false;
978
+ for (let pass = 0; pass < 20; pass += 1) {
979
+ let next: string;
980
+ try {
981
+ next = decodeURIComponent(decoded);
982
+ } catch {
983
+ throw new ConnectorCallError(
984
+ "invalid_args",
985
+ "path contains invalid percent encoding.",
986
+ );
987
+ }
988
+ if (next === decoded) {
989
+ stable = true;
990
+ break;
991
+ }
992
+ decoded = next;
993
+ }
994
+ if (!stable) {
995
+ throw new ConnectorCallError(
996
+ "invalid_args",
997
+ "path contains too many layers of percent encoding.",
998
+ );
999
+ }
1000
+ if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
1001
+ throw new ConnectorCallError(
1002
+ "invalid_args",
1003
+ "path cannot contain encoded or literal traversal, slash, or backslash segments.",
1004
+ );
1005
+ }
1006
+ }
1007
+ const normalized = new URL(`https://connecta.invalid/client/v4${path}`);
1008
+ if (!normalized.pathname.startsWith("/client/v4/")) {
1009
+ throw new ConnectorCallError(
1010
+ "invalid_args",
1011
+ "path normalization escaped the Cloudflare v4 API base.",
1012
+ );
1013
+ }
1014
+ return path;
1015
+ }
1016
+
1017
+ function queryFromArgs(
1018
+ value: unknown,
1019
+ ): Record<string, string | number | boolean | undefined> | undefined {
1020
+ if (!Array.isArray(value) || value.length === 0) return undefined;
1021
+ const query: Record<string, string> = {};
1022
+ for (const item of value) {
1023
+ const entry = asRecord(item);
1024
+ query[String(entry["name"])] = String(entry["value"]);
1025
+ }
1026
+ return query;
1027
+ }
1028
+
1029
+ function headersFromArgs(value: unknown): Record<string, string> | undefined {
1030
+ if (!Array.isArray(value) || value.length === 0) return undefined;
1031
+ const headers: Record<string, string> = {};
1032
+ const forbidden = new Set([
1033
+ "authorization",
1034
+ "cookie",
1035
+ "host",
1036
+ "content-length",
1037
+ "content-type",
1038
+ "transfer-encoding",
1039
+ ]);
1040
+ for (const item of value) {
1041
+ const entry = asRecord(item);
1042
+ const name = String(entry["name"]).trim();
1043
+ if (forbidden.has(name.toLowerCase())) {
1044
+ throw new ConnectorCallError(
1045
+ "invalid_args",
1046
+ `The raw Cloudflare tools do not allow the ${name} header. Authentication and request framing are connector-owned; use contentType for a raw upload body.`,
1047
+ );
1048
+ }
1049
+ headers[name] = String(entry["value"]);
1050
+ }
1051
+ return headers;
1052
+ }
1053
+
1054
+ function r2Headers(args: JsonRecord): Record<string, string | undefined> {
1055
+ return { "cf-r2-jurisdiction": optionalString(args, "jurisdiction") };
1056
+ }
1057
+
1058
+ function bytesFromBase64(value: string): Uint8Array<ArrayBuffer> {
1059
+ try {
1060
+ const binary = atob(value);
1061
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
1062
+ } catch (cause) {
1063
+ throw new ConnectorCallError(
1064
+ "invalid_args",
1065
+ "base64Body and multipart file base64 values must be valid base64.",
1066
+ { cause },
1067
+ );
1068
+ }
1069
+ }
1070
+
1071
+ function uploadBody(args: JsonRecord): {
1072
+ rawBody: BodyInit;
1073
+ headers?: Record<string, string | undefined>;
1074
+ } {
1075
+ const fields = asArray(args["fields"]);
1076
+ const files = asArray(args["files"]);
1077
+ const hasMultipart = fields.length > 0 || files.length > 0;
1078
+ const textBody = typeof args["textBody"] === "string" ? args["textBody"] : undefined;
1079
+ const base64Body =
1080
+ typeof args["base64Body"] === "string" ? args["base64Body"] : undefined;
1081
+ const rawCount = Number(textBody !== undefined) + Number(base64Body !== undefined);
1082
+ if ((hasMultipart && rawCount > 0) || (!hasMultipart && rawCount !== 1)) {
1083
+ throw new ConnectorCallError(
1084
+ "invalid_args",
1085
+ "cloudflare_api_upload needs exactly one body shape: textBody, base64Body, or multipart fields/files.",
1086
+ );
1087
+ }
1088
+ if (hasMultipart) {
1089
+ const form = new FormData();
1090
+ for (const value of fields) {
1091
+ const field = asRecord(value);
1092
+ const name = String(field["name"]);
1093
+ const contentType = optionalString(field, "contentType");
1094
+ if (contentType) {
1095
+ form.append(
1096
+ name,
1097
+ new Blob([String(field["value"])], { type: contentType }),
1098
+ optionalString(field, "fileName") ?? name,
1099
+ );
1100
+ } else {
1101
+ form.append(name, String(field["value"]));
1102
+ }
1103
+ }
1104
+ for (const value of files) {
1105
+ const file = asRecord(value);
1106
+ const text = typeof file["text"] === "string" ? file["text"] : undefined;
1107
+ const base64 =
1108
+ typeof file["base64"] === "string" ? file["base64"] : undefined;
1109
+ if (Number(text !== undefined) + Number(base64 !== undefined) !== 1) {
1110
+ throw new ConnectorCallError(
1111
+ "invalid_args",
1112
+ "Each multipart file needs exactly one of text or base64.",
1113
+ );
1114
+ }
1115
+ const blob = new Blob(
1116
+ [text ?? bytesFromBase64(base64!)],
1117
+ { type: String(file["contentType"] ?? "application/octet-stream") },
1118
+ );
1119
+ form.append(String(file["name"]), blob, String(file["fileName"]));
1120
+ }
1121
+ return { rawBody: form };
1122
+ }
1123
+ return {
1124
+ rawBody: textBody ?? bytesFromBase64(base64Body!),
1125
+ headers: {
1126
+ "Content-Type":
1127
+ optionalString(args, "contentType") ??
1128
+ (textBody !== undefined ? "text/plain; charset=utf-8" : "application/octet-stream"),
1129
+ },
1130
+ };
1131
+ }
1132
+
1133
+ const OPEN_OBJECT_OUTPUT_SCHEMA: JsonSchema = {
1134
+ type: "object",
1135
+ description: "Cloudflare's result object. Its fields depend on the endpoint.",
1136
+ additionalProperties: true,
1137
+ };
1138
+
1139
+ const QUERY_INPUT_PROPERTY: JsonSchema = {
1140
+ type: "array",
1141
+ description:
1142
+ "Optional query parameters as name/value pairs. Each parameter name may appear once.",
1143
+ items: {
1144
+ type: "object",
1145
+ properties: {
1146
+ name: { type: "string", minLength: 1 },
1147
+ value: { type: ["string", "number", "boolean"] },
1148
+ },
1149
+ required: ["name", "value"],
1150
+ additionalProperties: false,
1151
+ },
1152
+ };
1153
+
1154
+ const HEADERS_INPUT_PROPERTY: JsonSchema = {
1155
+ type: "array",
1156
+ description:
1157
+ "Optional provider headers as name/value pairs, for example cf-r2-jurisdiction, Range, If-None-Match, or Cloudflare product metadata. Authorization, Cookie, Host, Content-Length, Content-Type, and Transfer-Encoding are connector-owned and refused.",
1158
+ items: {
1159
+ type: "object",
1160
+ properties: {
1161
+ name: { type: "string", minLength: 1 },
1162
+ value: { type: "string" },
1163
+ },
1164
+ required: ["name", "value"],
1165
+ additionalProperties: false,
1166
+ },
1167
+ };
1168
+
1169
+ const R2_JURISDICTION_PROPERTY: JsonSchema = {
1170
+ type: "string",
1171
+ enum: ["default", "eu", "fedramp"],
1172
+ description:
1173
+ "Bucket jurisdiction. Omit for ordinary buckets; set eu or fedramp for jurisdictional buckets.",
1174
+ };
1175
+
1176
+ const R2_BUCKET_NAME_PROPERTY: JsonSchema = {
1177
+ type: "string",
1178
+ minLength: 3,
1179
+ maxLength: 64,
1180
+ description: "R2 bucket name.",
1181
+ };
1182
+
1183
+ const R2_BUCKET_SCHEMA: JsonSchema = {
1184
+ type: "object",
1185
+ properties: {
1186
+ name: { type: "string" },
1187
+ location: { type: "string" },
1188
+ storageClass: { type: "string" },
1189
+ jurisdiction: { type: "string" },
1190
+ creationDate: { type: "string" },
1191
+ },
1192
+ required: ["name"],
1193
+ };
1194
+
1195
+ const R2_OBJECT_SCHEMA: JsonSchema = {
1196
+ type: "object",
1197
+ properties: {
1198
+ key: { type: "string" },
1199
+ size: { type: "number" },
1200
+ etag: { type: "string" },
1201
+ lastModified: { type: "string" },
1202
+ storageClass: { type: "string" },
1203
+ httpMetadata: { type: "object" },
1204
+ customMetadata: { type: "object" },
1205
+ },
1206
+ required: ["key"],
1207
+ };
1208
+
757
1209
  function buildTools(scope: Scoping): ApiTool[] {
758
1210
  const { base } = scope;
759
1211
  const zoneArg = (args: JsonRecord): string =>
@@ -808,394 +1260,1715 @@ function buildTools(scope: Scoping): ApiTool[] {
808
1260
  },
809
1261
  },
810
1262
  {
811
- name: "list_accounts",
1263
+ name: "cloudflare_api_get",
812
1264
  description:
813
- "List Cloudflare accounts this token can see. Supplies the accountId that the Workers, KV, R2, and Pages tools need.",
1265
+ "Call any GET endpoint under Cloudflare's v4 API with this connector's token. Use a named tool when one exists; use this read-only escape hatch for Images, Stream, Email Routing, D1, Queues, Access, Tunnels, Analytics, and newer product endpoints the curated surface does not yet name.",
814
1266
  annotations: readOnly,
815
1267
  inputSchema: {
816
1268
  type: "object",
817
1269
  properties: {
818
- name: {
1270
+ path: {
819
1271
  type: "string",
820
- description: "Filter by exact account name.",
1272
+ minLength: 1,
1273
+ description:
1274
+ "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.",
1275
+ },
1276
+ query: QUERY_INPUT_PROPERTY,
1277
+ headers: HEADERS_INPUT_PROPERTY,
1278
+ responseType: {
1279
+ type: "string",
1280
+ enum: ["json", "text", "base64"],
1281
+ description:
1282
+ "How to read a successful response. Defaults to json; use text or base64 for object, log, script, and media downloads.",
821
1283
  },
822
- ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
823
- raw: RAW_INPUT_PROPERTY,
824
1284
  },
825
- required: [],
1285
+ required: ["path"],
826
1286
  additionalProperties: false,
827
1287
  },
828
- outputSchema: listOutputSchema("accounts", ACCOUNT_SCHEMA),
1288
+ outputSchema: {
1289
+ type: "object",
1290
+ properties: {
1291
+ result: {
1292
+ description: "Cloudflare's unprojected result for the endpoint.",
1293
+ },
1294
+ resultInfo: {
1295
+ type: "object",
1296
+ description:
1297
+ "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1298
+ },
1299
+ text: { type: "string", description: "Text response body when responseType is text." },
1300
+ base64: { type: "string", description: "Base64 response bytes when responseType is base64." },
1301
+ contentType: { type: "string", description: "Response Content-Type for text/base64 reads." },
1302
+ etag: { type: "string", description: "Response ETag when Cloudflare supplies one." },
1303
+ },
1304
+ required: [],
1305
+ },
829
1306
  handler: async (args: JsonRecord, ctx) => {
1307
+ const query = queryFromArgs(args["query"]);
1308
+ const headers = headersFromArgs(args["headers"]);
1309
+ const responseType = optionalString(args, "responseType") ?? "json";
1310
+ const spec: RequestSpec = {
1311
+ method: "GET",
1312
+ path: cloudflareApiPath(args["path"]),
1313
+ ...(query !== undefined ? { query } : {}),
1314
+ ...(headers !== undefined ? { headers } : {}),
1315
+ };
1316
+ if (responseType === "text" || responseType === "base64") {
1317
+ return await callCloudflareContent(base, spec, ctx, responseType);
1318
+ }
830
1319
  const { result, resultInfo } = await callCloudflare(
831
1320
  base,
832
- {
833
- method: "GET",
834
- path: "/accounts",
835
- query: {
836
- name: optionalString(args, "name"),
837
- page: optionalNumber(args, "page"),
838
- per_page: optionalNumber(args, "perPage"),
839
- },
840
- },
1321
+ spec,
841
1322
  ctx,
842
1323
  );
843
- if (args["raw"] === true) return { accounts: result, page: pageInfo(resultInfo) };
844
1324
  return {
845
- accounts: asArray(result).map(projectAccount),
846
- page: pageInfo(resultInfo),
1325
+ result,
1326
+ ...(resultInfo !== undefined ? { resultInfo } : {}),
847
1327
  };
848
1328
  },
849
1329
  },
850
1330
  {
851
- name: "list_zones",
1331
+ name: "cloudflare_api_mutate",
852
1332
  description:
853
- "List zones (domains) this token can see, with their ids and status. This is the zoneId discovery step for every DNS and cache tool.",
854
- annotations: readOnly,
1333
+ "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's token. This is the approval-gated escape hatch for managing Cloudflare products without waiting for a named tool. It does not support multipart or binary uploads.",
1334
+ annotations: { readOnlyHint: false, destructiveHint: true },
855
1335
  inputSchema: {
856
1336
  type: "object",
857
1337
  properties: {
858
- name: {
1338
+ method: {
859
1339
  type: "string",
860
- description: "Filter by zone name, e.g. example.com.",
1340
+ enum: ["POST", "PUT", "PATCH", "DELETE"],
1341
+ description: "HTTP mutation method required by the Cloudflare endpoint.",
861
1342
  },
862
- accountId: {
1343
+ path: {
863
1344
  type: "string",
1345
+ minLength: 1,
864
1346
  description:
865
- "Restrict to one account. Defaults to every account the token can see.",
1347
+ "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
866
1348
  },
867
- status: {
868
- type: "string",
869
- enum: ["initializing", "pending", "active", "moved"],
870
- description: "Filter by zone status.",
1349
+ query: QUERY_INPUT_PROPERTY,
1350
+ headers: HEADERS_INPUT_PROPERTY,
1351
+ body: {
1352
+ type: ["object", "array", "string", "number", "boolean", "null"],
1353
+ description:
1354
+ "JSON request body exactly as documented by Cloudflare. Omit for endpoints with no body.",
871
1355
  },
872
- ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
873
- raw: RAW_INPUT_PROPERTY,
874
1356
  },
875
- required: [],
1357
+ required: ["method", "path"],
876
1358
  additionalProperties: false,
877
1359
  },
878
- outputSchema: listOutputSchema("zones", ZONE_SCHEMA),
1360
+ outputSchema: {
1361
+ type: "object",
1362
+ properties: {
1363
+ result: {
1364
+ description: "Cloudflare's unprojected result for the endpoint.",
1365
+ },
1366
+ resultInfo: {
1367
+ type: "object",
1368
+ description:
1369
+ "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
1370
+ },
1371
+ },
1372
+ required: ["result"],
1373
+ },
879
1374
  handler: async (args: JsonRecord, ctx) => {
1375
+ const method = String(args["method"]) as RequestSpec["method"];
1376
+ const query = queryFromArgs(args["query"]);
1377
+ const headers = headersFromArgs(args["headers"]);
880
1378
  const { result, resultInfo } = await callCloudflare(
881
1379
  base,
882
1380
  {
883
- method: "GET",
884
- path: "/zones",
885
- query: {
886
- name: optionalString(args, "name"),
887
- // Deliberately not defaulted to `scope.accountId`. This is the
888
- // discovery tool: a deployment default that silently narrowed
889
- // what an agent can see would contradict the property's own
890
- // description, and there would be no argument that escapes it.
891
- "account.id": optionalString(args, "accountId"),
892
- status: optionalString(args, "status"),
893
- page: optionalNumber(args, "page"),
894
- per_page: optionalNumber(args, "perPage"),
895
- },
1381
+ method,
1382
+ path: cloudflareApiPath(args["path"]),
1383
+ ...(query !== undefined ? { query } : {}),
1384
+ ...(headers !== undefined ? { headers } : {}),
1385
+ ...(args["body"] !== undefined ? { body: args["body"] } : {}),
896
1386
  },
897
1387
  ctx,
898
1388
  );
899
- if (args["raw"] === true) return { zones: result, page: pageInfo(resultInfo) };
900
1389
  return {
901
- zones: asArray(result).map(projectZone),
902
- page: pageInfo(resultInfo),
1390
+ result,
1391
+ ...(resultInfo !== undefined ? { resultInfo } : {}),
903
1392
  };
904
1393
  },
905
1394
  },
906
1395
  {
907
- name: "get_zone",
1396
+ name: "cloudflare_api_upload",
908
1397
  description:
909
- "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.",
910
- annotations: readOnly,
1398
+ "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.",
1399
+ annotations: { readOnlyHint: false, destructiveHint: true },
911
1400
  inputSchema: {
912
1401
  type: "object",
913
1402
  properties: {
914
- zoneId: scopeProperty("zoneId", scope.zoneId),
915
- raw: RAW_INPUT_PROPERTY,
916
- },
917
- required: scopeRequired("zoneId", scope.zoneId),
918
- additionalProperties: false,
919
- },
920
- outputSchema: ZONE_SCHEMA,
1403
+ method: {
1404
+ type: "string",
1405
+ enum: ["POST", "PUT"],
1406
+ description: "HTTP upload method required by the Cloudflare endpoint.",
1407
+ },
1408
+ path: {
1409
+ type: "string",
1410
+ minLength: 1,
1411
+ description:
1412
+ "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
1413
+ },
1414
+ query: QUERY_INPUT_PROPERTY,
1415
+ headers: HEADERS_INPUT_PROPERTY,
1416
+ contentType: {
1417
+ type: "string",
1418
+ minLength: 1,
1419
+ description:
1420
+ "Content-Type for a raw text/base64 body. Omit for multipart because fetch supplies the boundary.",
1421
+ },
1422
+ textBody: {
1423
+ type: "string",
1424
+ description: "Raw UTF-8 request body. Mutually exclusive with base64Body and multipart fields/files.",
1425
+ },
1426
+ base64Body: {
1427
+ type: "string",
1428
+ description: "Base64-encoded request bytes. Mutually exclusive with textBody and multipart fields/files.",
1429
+ },
1430
+ fields: {
1431
+ type: "array",
1432
+ description: "String fields for a multipart/form-data request.",
1433
+ items: {
1434
+ type: "object",
1435
+ properties: {
1436
+ name: { type: "string", minLength: 1 },
1437
+ value: { type: "string" },
1438
+ contentType: { type: "string", minLength: 1 },
1439
+ fileName: { type: "string", minLength: 1 },
1440
+ },
1441
+ required: ["name", "value"],
1442
+ additionalProperties: false,
1443
+ },
1444
+ },
1445
+ files: {
1446
+ type: "array",
1447
+ description:
1448
+ "File parts for multipart/form-data. Each file needs exactly one of text or base64.",
1449
+ items: {
1450
+ type: "object",
1451
+ properties: {
1452
+ name: { type: "string", minLength: 1 },
1453
+ fileName: { type: "string", minLength: 1 },
1454
+ contentType: { type: "string", minLength: 1 },
1455
+ text: { type: "string" },
1456
+ base64: { type: "string" },
1457
+ },
1458
+ required: ["name", "fileName", "contentType"],
1459
+ additionalProperties: false,
1460
+ },
1461
+ },
1462
+ },
1463
+ required: ["method", "path"],
1464
+ additionalProperties: false,
1465
+ },
1466
+ outputSchema: {
1467
+ type: "object",
1468
+ properties: {
1469
+ result: {
1470
+ description: "Cloudflare's unprojected upload result.",
1471
+ },
1472
+ },
1473
+ required: ["result"],
1474
+ },
921
1475
  handler: async (args: JsonRecord, ctx) => {
1476
+ const query = queryFromArgs(args["query"]);
1477
+ const headers = headersFromArgs(args["headers"]);
1478
+ const upload = uploadBody(args);
922
1479
  const { result } = await callCloudflare(
923
1480
  base,
924
- { method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` },
1481
+ {
1482
+ method: String(args["method"]) as "POST" | "PUT",
1483
+ path: cloudflareApiPath(args["path"]),
1484
+ ...(query !== undefined ? { query } : {}),
1485
+ ...(headers !== undefined || upload.headers !== undefined
1486
+ ? { headers: { ...headers, ...upload.headers } }
1487
+ : {}),
1488
+ rawBody: upload.rawBody,
1489
+ },
925
1490
  ctx,
926
1491
  );
927
- return args["raw"] === true ? result : projectZone(result);
1492
+ return { result };
928
1493
  },
929
1494
  },
930
1495
  {
931
- name: "list_dns_records",
1496
+ name: "list_accounts",
932
1497
  description:
933
- "List DNS records in a zone, filtered by name, type, or content. Returns record ids, which update_dns_record and delete_dns_record require.",
1498
+ "List Cloudflare accounts this token can see. Supplies the accountId that the Workers, KV, R2, and Pages tools need.",
934
1499
  annotations: readOnly,
935
1500
  inputSchema: {
936
1501
  type: "object",
937
1502
  properties: {
938
- zoneId: scopeProperty("zoneId", scope.zoneId),
939
1503
  name: {
940
1504
  type: "string",
941
- description:
942
- "Exact record name, fully qualified, e.g. www.example.com.",
943
- },
944
- type: {
945
- type: "string",
946
- enum: [...CLOUDFLARE_DNS_RECORD_TYPES],
947
- description: "Filter by record type.",
948
- },
949
- content: {
950
- type: "string",
951
- description: "Exact record content, e.g. an IP address.",
952
- },
953
- order: {
954
- type: "string",
955
- enum: ["type", "name", "content", "ttl", "proxied"],
956
- description: "Sort field.",
957
- },
958
- direction: {
959
- type: "string",
960
- enum: ["asc", "desc"],
961
- description: "Sort direction for `order`. Defaults to asc.",
1505
+ description: "Filter by exact account name.",
962
1506
  },
963
- // Cloudflare documents 1 to 5,000,000 here with a default of 100; the
964
- // ceiling is nominal, so this connection caps it at a page size that
965
- // actually returns.
966
- ...pagingInputProperties(1, 1000, {
967
- defaultPerPage: 100,
968
- bounds: "clamped",
969
- }),
1507
+ ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
970
1508
  raw: RAW_INPUT_PROPERTY,
971
1509
  },
972
- required: scopeRequired("zoneId", scope.zoneId),
1510
+ required: [],
973
1511
  additionalProperties: false,
974
1512
  },
975
- outputSchema: listOutputSchema("records", DNS_RECORD_SCHEMA),
1513
+ outputSchema: listOutputSchema("accounts", ACCOUNT_SCHEMA),
976
1514
  handler: async (args: JsonRecord, ctx) => {
977
1515
  const { result, resultInfo } = await callCloudflare(
978
1516
  base,
979
1517
  {
980
1518
  method: "GET",
981
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
1519
+ path: "/accounts",
982
1520
  query: {
983
1521
  name: optionalString(args, "name"),
984
- type: optionalString(args, "type"),
985
- content: optionalString(args, "content"),
986
- order: optionalString(args, "order"),
987
- direction: optionalString(args, "direction"),
988
1522
  page: optionalNumber(args, "page"),
989
1523
  per_page: optionalNumber(args, "perPage"),
990
1524
  },
991
1525
  },
992
1526
  ctx,
993
1527
  );
994
- if (args["raw"] === true)
995
- return { records: result, page: pageInfo(resultInfo) };
1528
+ if (args["raw"] === true) return { accounts: result, page: pageInfo(resultInfo) };
996
1529
  return {
997
- records: asArray(result).map(projectDnsRecord),
1530
+ accounts: asArray(result).map(projectAccount),
998
1531
  page: pageInfo(resultInfo),
999
1532
  };
1000
1533
  },
1001
1534
  },
1002
1535
  {
1003
- name: "get_dns_record",
1004
- description: "Fetch one DNS record by its record id.",
1536
+ name: "list_zones",
1537
+ description:
1538
+ "List zones (domains) this token can see, with their ids and status. This is the zoneId discovery step for every DNS and cache tool.",
1005
1539
  annotations: readOnly,
1006
1540
  inputSchema: {
1007
1541
  type: "object",
1008
1542
  properties: {
1009
- zoneId: scopeProperty("zoneId", scope.zoneId),
1010
- recordId: {
1543
+ name: {
1011
1544
  type: "string",
1012
- description: "DNS record id, from list_dns_records.",
1545
+ description: "Filter by zone name, e.g. example.com.",
1546
+ },
1547
+ accountId: {
1548
+ type: "string",
1549
+ description:
1550
+ "Restrict to one account. Defaults to every account the token can see.",
1551
+ },
1552
+ status: {
1553
+ type: "string",
1554
+ enum: ["initializing", "pending", "active", "moved"],
1555
+ description: "Filter by zone status.",
1013
1556
  },
1557
+ ...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
1014
1558
  raw: RAW_INPUT_PROPERTY,
1015
1559
  },
1016
- required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
1560
+ required: [],
1017
1561
  additionalProperties: false,
1018
1562
  },
1019
- outputSchema: DNS_RECORD_SCHEMA,
1563
+ outputSchema: listOutputSchema("zones", ZONE_SCHEMA),
1020
1564
  handler: async (args: JsonRecord, ctx) => {
1021
- const { result } = await callCloudflare(
1565
+ const { result, resultInfo } = await callCloudflare(
1022
1566
  base,
1023
1567
  {
1024
1568
  method: "GET",
1025
- path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(
1026
- String(args["recordId"]),
1027
- )}`,
1569
+ path: "/zones",
1570
+ query: {
1571
+ name: optionalString(args, "name"),
1572
+ // Deliberately not defaulted to `scope.accountId`. This is the
1573
+ // discovery tool: a deployment default that silently narrowed
1574
+ // what an agent can see would contradict the property's own
1575
+ // description, and there would be no argument that escapes it.
1576
+ "account.id": optionalString(args, "accountId"),
1577
+ status: optionalString(args, "status"),
1578
+ page: optionalNumber(args, "page"),
1579
+ per_page: optionalNumber(args, "perPage"),
1580
+ },
1028
1581
  },
1029
1582
  ctx,
1030
1583
  );
1031
- return args["raw"] === true ? result : projectDnsRecord(result);
1584
+ if (args["raw"] === true) return { zones: result, page: pageInfo(resultInfo) };
1585
+ return {
1586
+ zones: asArray(result).map(projectZone),
1587
+ page: pageInfo(resultInfo),
1588
+ };
1032
1589
  },
1033
1590
  },
1034
1591
  {
1035
- name: "list_worker_scripts",
1592
+ name: "get_zone",
1036
1593
  description:
1037
- "List Workers scripts deployed in an account, with their last-modified times.",
1594
+ "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.",
1038
1595
  annotations: readOnly,
1039
1596
  inputSchema: {
1040
1597
  type: "object",
1041
1598
  properties: {
1042
- accountId: scopeProperty("accountId", scope.accountId),
1599
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1043
1600
  raw: RAW_INPUT_PROPERTY,
1044
1601
  },
1045
- required: scopeRequired("accountId", scope.accountId),
1602
+ required: scopeRequired("zoneId", scope.zoneId),
1046
1603
  additionalProperties: false,
1047
1604
  },
1048
- outputSchema: listOutputSchema("scripts", {
1605
+ outputSchema: ZONE_SCHEMA,
1606
+ handler: async (args: JsonRecord, ctx) => {
1607
+ const { result } = await callCloudflare(
1608
+ base,
1609
+ { method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` },
1610
+ ctx,
1611
+ );
1612
+ return args["raw"] === true ? result : projectZone(result);
1613
+ },
1614
+ },
1615
+ {
1616
+ name: "list_zone_settings",
1617
+ description:
1618
+ "List the effective settings for a zone, including each setting's current value and whether the plan allows editing it.",
1619
+ annotations: readOnly,
1620
+ inputSchema: {
1049
1621
  type: "object",
1050
1622
  properties: {
1051
- id: { type: "string", description: "Script name." },
1052
- createdOn: { type: "string" },
1053
- modifiedOn: { type: "string" },
1054
- usageModel: { type: "string" },
1623
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1055
1624
  },
1056
- required: ["id"],
1057
- }),
1625
+ required: scopeRequired("zoneId", scope.zoneId),
1626
+ additionalProperties: false,
1627
+ },
1628
+ outputSchema: listOutputSchema("settings", OPEN_OBJECT_OUTPUT_SCHEMA),
1058
1629
  handler: async (args: JsonRecord, ctx) => {
1059
1630
  const { result, resultInfo } = await callCloudflare(
1060
1631
  base,
1061
1632
  {
1062
1633
  method: "GET",
1063
- path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
1634
+ path: `/zones/${encodePathSegment(zoneArg(args))}/settings`,
1064
1635
  },
1065
1636
  ctx,
1066
1637
  );
1067
- if (args["raw"] === true)
1068
- return { scripts: result, page: pageInfo(resultInfo) };
1069
- return {
1070
- scripts: asArray(result).map(projectWorkerScript),
1071
- page: pageInfo(resultInfo),
1072
- };
1638
+ return { settings: asArray(result), page: pageInfo(resultInfo) };
1073
1639
  },
1074
1640
  },
1075
1641
  {
1076
- name: "list_kv_namespaces",
1642
+ name: "get_zone_setting",
1077
1643
  description:
1078
- "List Workers KV namespaces in an account, with the namespace ids bindings refer to.",
1644
+ "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.",
1079
1645
  annotations: readOnly,
1080
1646
  inputSchema: {
1081
1647
  type: "object",
1082
1648
  properties: {
1083
- accountId: scopeProperty("accountId", scope.accountId),
1084
- ...pagingInputProperties(1, 1000, { defaultPerPage: 20 }),
1085
- raw: RAW_INPUT_PROPERTY,
1649
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1650
+ settingId: {
1651
+ type: "string",
1652
+ minLength: 1,
1653
+ description: "Cloudflare zone setting id from list_zone_settings.",
1654
+ },
1086
1655
  },
1087
- required: scopeRequired("accountId", scope.accountId),
1656
+ required: [...scopeRequired("zoneId", scope.zoneId), "settingId"],
1088
1657
  additionalProperties: false,
1089
1658
  },
1090
- outputSchema: listOutputSchema("namespaces", {
1659
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1660
+ handler: async (args: JsonRecord, ctx) => {
1661
+ const { result } = await callCloudflare(
1662
+ base,
1663
+ {
1664
+ method: "GET",
1665
+ path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1666
+ },
1667
+ ctx,
1668
+ );
1669
+ return result;
1670
+ },
1671
+ },
1672
+ {
1673
+ name: "update_zone_setting",
1674
+ description:
1675
+ "Set one editable zone setting. Read it first: allowed value types and plan restrictions differ by setting.",
1676
+ annotations: { readOnlyHint: false, destructiveHint: true },
1677
+ inputSchema: {
1091
1678
  type: "object",
1092
1679
  properties: {
1093
- id: { type: "string" },
1094
- title: { type: "string" },
1095
- supportsUrlEncoding: { type: "boolean" },
1680
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1681
+ settingId: {
1682
+ type: "string",
1683
+ minLength: 1,
1684
+ description: "Cloudflare zone setting id from list_zone_settings.",
1685
+ },
1686
+ value: {
1687
+ type: ["string", "number", "boolean", "array"],
1688
+ description:
1689
+ "New setting value in the type returned by get_zone_setting. Arrays must contain strings.",
1690
+ items: { type: "string" },
1691
+ },
1096
1692
  },
1097
- required: ["id", "title"],
1098
- }),
1693
+ required: [...scopeRequired("zoneId", scope.zoneId), "settingId", "value"],
1694
+ additionalProperties: false,
1695
+ },
1696
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1099
1697
  handler: async (args: JsonRecord, ctx) => {
1100
- const { result, resultInfo } = await callCloudflare(
1698
+ const { result } = await callCloudflare(
1101
1699
  base,
1102
1700
  {
1103
- method: "GET",
1104
- path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
1105
- query: {
1106
- page: optionalNumber(args, "page"),
1107
- per_page: optionalNumber(args, "perPage"),
1108
- },
1701
+ method: "PATCH",
1702
+ path: `/zones/${encodePathSegment(zoneArg(args))}/settings/${encodePathSegment(requireString(args, "settingId"))}`,
1703
+ body: { value: args["value"] },
1109
1704
  },
1110
1705
  ctx,
1111
1706
  );
1112
- if (args["raw"] === true)
1113
- return { namespaces: result, page: pageInfo(resultInfo) };
1114
- return {
1115
- namespaces: asArray(result).map(projectKvNamespace),
1116
- page: pageInfo(resultInfo),
1117
- };
1707
+ return result;
1118
1708
  },
1119
1709
  },
1120
1710
  {
1121
- name: "list_r2_buckets",
1711
+ name: "list_zone_rulesets",
1122
1712
  description:
1123
- "List R2 buckets in an account, with location and storage class.",
1713
+ "List zone rulesets for WAF, redirects, transforms, cache rules, configuration rules, and other Ruleset Engine phases.",
1124
1714
  annotations: readOnly,
1125
1715
  inputSchema: {
1126
1716
  type: "object",
1127
1717
  properties: {
1128
- accountId: scopeProperty("accountId", scope.accountId),
1129
- nameContains: {
1130
- type: "string",
1131
- description: "Filter to buckets whose name contains this string.",
1132
- },
1718
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1133
1719
  perPage: {
1134
1720
  type: "integer",
1135
1721
  minimum: 1,
1136
- maximum: 1000,
1137
- description: "Buckets per request, 1 to 1000. Defaults to 20.",
1722
+ maximum: 50,
1723
+ description: "Rulesets per request, 1 to 50.",
1138
1724
  },
1139
1725
  cursor: {
1140
1726
  type: "string",
1141
- description:
1142
- "Opaque cursor from a previous call's nextCursor. R2 paginates by cursor, not page number.",
1727
+ description: "Opaque cursor returned as nextCursor by the previous call.",
1143
1728
  },
1144
- raw: RAW_INPUT_PROPERTY,
1145
1729
  },
1146
- required: scopeRequired("accountId", scope.accountId),
1730
+ required: scopeRequired("zoneId", scope.zoneId),
1147
1731
  additionalProperties: false,
1148
1732
  },
1149
1733
  outputSchema: {
1150
1734
  type: "object",
1151
1735
  properties: {
1152
- buckets: {
1153
- type: "array",
1154
- items: {
1155
- type: "object",
1156
- properties: {
1157
- name: { type: "string" },
1158
- location: { type: "string" },
1159
- storageClass: { type: "string" },
1160
- creationDate: { type: "string" },
1161
- },
1736
+ rulesets: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
1737
+ nextCursor: { type: "string" },
1738
+ },
1739
+ required: ["rulesets"],
1740
+ },
1741
+ handler: async (args: JsonRecord, ctx) => {
1742
+ const { result, resultInfo } = await callCloudflare(
1743
+ base,
1744
+ {
1745
+ method: "GET",
1746
+ path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets`,
1747
+ query: {
1748
+ per_page: optionalNumber(args, "perPage"),
1749
+ cursor: optionalString(args, "cursor"),
1750
+ },
1751
+ },
1752
+ ctx,
1753
+ );
1754
+ const cursor = resultInfo?.cursors?.after;
1755
+ return {
1756
+ rulesets: asArray(result).map(projectRuleset),
1757
+ ...(typeof cursor === "string" && cursor !== ""
1758
+ ? { nextCursor: cursor }
1759
+ : {}),
1760
+ };
1761
+ },
1762
+ },
1763
+ {
1764
+ name: "get_zone_ruleset",
1765
+ description:
1766
+ "Get one zone ruleset including its ordered rules, expressions, actions, parameters, and enabled state.",
1767
+ annotations: readOnly,
1768
+ inputSchema: {
1769
+ type: "object",
1770
+ properties: {
1771
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1772
+ rulesetId: {
1773
+ type: "string",
1774
+ minLength: 1,
1775
+ description: "Ruleset id from list_zone_rulesets.",
1776
+ },
1777
+ },
1778
+ required: [...scopeRequired("zoneId", scope.zoneId), "rulesetId"],
1779
+ additionalProperties: false,
1780
+ },
1781
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1782
+ handler: async (args: JsonRecord, ctx) => {
1783
+ const { result } = await callCloudflare(
1784
+ base,
1785
+ {
1786
+ method: "GET",
1787
+ path: `/zones/${encodePathSegment(zoneArg(args))}/rulesets/${encodePathSegment(requireString(args, "rulesetId"))}`,
1788
+ },
1789
+ ctx,
1790
+ );
1791
+ return projectRuleset(result);
1792
+ },
1793
+ },
1794
+ {
1795
+ name: "list_dns_records",
1796
+ description:
1797
+ "List DNS records in a zone, filtered by name, type, or content. Returns record ids, which update_dns_record and delete_dns_record require.",
1798
+ annotations: readOnly,
1799
+ inputSchema: {
1800
+ type: "object",
1801
+ properties: {
1802
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1803
+ name: {
1804
+ type: "string",
1805
+ description:
1806
+ "Exact record name, fully qualified, e.g. www.example.com.",
1807
+ },
1808
+ type: {
1809
+ type: "string",
1810
+ enum: [...CLOUDFLARE_DNS_RECORD_TYPES],
1811
+ description: "Filter by record type.",
1812
+ },
1813
+ content: {
1814
+ type: "string",
1815
+ description: "Exact record content, e.g. an IP address.",
1816
+ },
1817
+ order: {
1818
+ type: "string",
1819
+ enum: ["type", "name", "content", "ttl", "proxied"],
1820
+ description: "Sort field.",
1821
+ },
1822
+ direction: {
1823
+ type: "string",
1824
+ enum: ["asc", "desc"],
1825
+ description: "Sort direction for `order`. Defaults to asc.",
1826
+ },
1827
+ // Cloudflare documents 1 to 5,000,000 here with a default of 100; the
1828
+ // ceiling is nominal, so this connection caps it at a page size that
1829
+ // actually returns.
1830
+ ...pagingInputProperties(1, 1000, {
1831
+ defaultPerPage: 100,
1832
+ bounds: "clamped",
1833
+ }),
1834
+ raw: RAW_INPUT_PROPERTY,
1835
+ },
1836
+ required: scopeRequired("zoneId", scope.zoneId),
1837
+ additionalProperties: false,
1838
+ },
1839
+ outputSchema: listOutputSchema("records", DNS_RECORD_SCHEMA),
1840
+ handler: async (args: JsonRecord, ctx) => {
1841
+ const { result, resultInfo } = await callCloudflare(
1842
+ base,
1843
+ {
1844
+ method: "GET",
1845
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
1846
+ query: {
1847
+ name: optionalString(args, "name"),
1848
+ type: optionalString(args, "type"),
1849
+ content: optionalString(args, "content"),
1850
+ order: optionalString(args, "order"),
1851
+ direction: optionalString(args, "direction"),
1852
+ page: optionalNumber(args, "page"),
1853
+ per_page: optionalNumber(args, "perPage"),
1854
+ },
1855
+ },
1856
+ ctx,
1857
+ );
1858
+ if (args["raw"] === true)
1859
+ return { records: result, page: pageInfo(resultInfo) };
1860
+ return {
1861
+ records: asArray(result).map(projectDnsRecord),
1862
+ page: pageInfo(resultInfo),
1863
+ };
1864
+ },
1865
+ },
1866
+ {
1867
+ name: "get_dns_record",
1868
+ description: "Fetch one DNS record by its record id.",
1869
+ annotations: readOnly,
1870
+ inputSchema: {
1871
+ type: "object",
1872
+ properties: {
1873
+ zoneId: scopeProperty("zoneId", scope.zoneId),
1874
+ recordId: {
1875
+ type: "string",
1876
+ description: "DNS record id, from list_dns_records.",
1877
+ },
1878
+ raw: RAW_INPUT_PROPERTY,
1879
+ },
1880
+ required: [...scopeRequired("zoneId", scope.zoneId), "recordId"],
1881
+ additionalProperties: false,
1882
+ },
1883
+ outputSchema: DNS_RECORD_SCHEMA,
1884
+ handler: async (args: JsonRecord, ctx) => {
1885
+ const { result } = await callCloudflare(
1886
+ base,
1887
+ {
1888
+ method: "GET",
1889
+ path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(
1890
+ String(args["recordId"]),
1891
+ )}`,
1892
+ },
1893
+ ctx,
1894
+ );
1895
+ return args["raw"] === true ? result : projectDnsRecord(result);
1896
+ },
1897
+ },
1898
+ {
1899
+ name: "list_worker_scripts",
1900
+ description:
1901
+ "List Workers scripts deployed in an account, with their last-modified times.",
1902
+ annotations: readOnly,
1903
+ inputSchema: {
1904
+ type: "object",
1905
+ properties: {
1906
+ accountId: scopeProperty("accountId", scope.accountId),
1907
+ raw: RAW_INPUT_PROPERTY,
1908
+ },
1909
+ required: scopeRequired("accountId", scope.accountId),
1910
+ additionalProperties: false,
1911
+ },
1912
+ outputSchema: listOutputSchema("scripts", {
1913
+ type: "object",
1914
+ properties: {
1915
+ id: { type: "string", description: "Script name." },
1916
+ createdOn: { type: "string" },
1917
+ modifiedOn: { type: "string" },
1918
+ usageModel: { type: "string" },
1919
+ },
1920
+ required: ["id"],
1921
+ }),
1922
+ handler: async (args: JsonRecord, ctx) => {
1923
+ const { result, resultInfo } = await callCloudflare(
1924
+ base,
1925
+ {
1926
+ method: "GET",
1927
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
1928
+ },
1929
+ ctx,
1930
+ );
1931
+ if (args["raw"] === true)
1932
+ return { scripts: result, page: pageInfo(resultInfo) };
1933
+ return {
1934
+ scripts: asArray(result).map(projectWorkerScript),
1935
+ page: pageInfo(resultInfo),
1936
+ };
1937
+ },
1938
+ },
1939
+ {
1940
+ name: "get_worker_settings",
1941
+ description:
1942
+ "Get a Worker's compatibility date and flags, bindings, limits, observability, placement, usage model, and other script settings.",
1943
+ annotations: readOnly,
1944
+ inputSchema: {
1945
+ type: "object",
1946
+ properties: {
1947
+ accountId: scopeProperty("accountId", scope.accountId),
1948
+ scriptName: {
1949
+ type: "string",
1950
+ minLength: 1,
1951
+ description: "Worker script name from list_worker_scripts.",
1952
+ },
1953
+ },
1954
+ required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
1955
+ additionalProperties: false,
1956
+ },
1957
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
1958
+ handler: async (args: JsonRecord, ctx) => {
1959
+ const { result } = await callCloudflare(
1960
+ base,
1961
+ {
1962
+ method: "GET",
1963
+ path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/settings`,
1964
+ },
1965
+ ctx,
1966
+ );
1967
+ return result;
1968
+ },
1969
+ },
1970
+ {
1971
+ name: "list_worker_deployments",
1972
+ description:
1973
+ "List deployments of a Worker script, including version traffic allocations and deployment strategy.",
1974
+ annotations: readOnly,
1975
+ inputSchema: {
1976
+ type: "object",
1977
+ properties: {
1978
+ accountId: scopeProperty("accountId", scope.accountId),
1979
+ scriptName: {
1980
+ type: "string",
1981
+ minLength: 1,
1982
+ description: "Worker script name from list_worker_scripts.",
1983
+ },
1984
+ },
1985
+ required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
1986
+ additionalProperties: false,
1987
+ },
1988
+ outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
1989
+ handler: async (args: JsonRecord, ctx) => {
1990
+ const { result } = await callCloudflare(
1991
+ base,
1992
+ {
1993
+ method: "GET",
1994
+ path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments`,
1995
+ },
1996
+ ctx,
1997
+ );
1998
+ const record = asRecord(result);
1999
+ const deployments = Array.isArray(result)
2000
+ ? result
2001
+ : asArray(record["deployments"]);
2002
+ return { deployments: deployments.map(projectWorkerDeployment) };
2003
+ },
2004
+ },
2005
+ {
2006
+ name: "get_worker_deployment",
2007
+ description: "Get one Worker deployment and its version traffic allocations.",
2008
+ annotations: readOnly,
2009
+ inputSchema: {
2010
+ type: "object",
2011
+ properties: {
2012
+ accountId: scopeProperty("accountId", scope.accountId),
2013
+ scriptName: {
2014
+ type: "string",
2015
+ minLength: 1,
2016
+ description: "Worker script name from list_worker_scripts.",
2017
+ },
2018
+ deploymentId: {
2019
+ type: "string",
2020
+ minLength: 1,
2021
+ description: "Deployment id from list_worker_deployments.",
2022
+ },
2023
+ },
2024
+ required: [
2025
+ ...scopeRequired("accountId", scope.accountId),
2026
+ "scriptName",
2027
+ "deploymentId",
2028
+ ],
2029
+ additionalProperties: false,
2030
+ },
2031
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2032
+ handler: async (args: JsonRecord, ctx) => {
2033
+ const { result } = await callCloudflare(
2034
+ base,
2035
+ {
2036
+ method: "GET",
2037
+ path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(requireString(args, "scriptName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
2038
+ },
2039
+ ctx,
2040
+ );
2041
+ return projectWorkerDeployment(result);
2042
+ },
2043
+ },
2044
+ {
2045
+ name: "delete_worker_script",
2046
+ description:
2047
+ "Delete a Worker script and stop traffic served by that script. This cannot be undone from the API.",
2048
+ annotations: { readOnlyHint: false, destructiveHint: true },
2049
+ inputSchema: {
2050
+ type: "object",
2051
+ properties: {
2052
+ accountId: scopeProperty("accountId", scope.accountId),
2053
+ scriptName: {
2054
+ type: "string",
2055
+ minLength: 1,
2056
+ description: "Worker script name from list_worker_scripts.",
2057
+ },
2058
+ force: {
2059
+ type: "boolean",
2060
+ description:
2061
+ "Pass Cloudflare's force=true option when the script has dependencies that permit forced removal.",
2062
+ },
2063
+ },
2064
+ required: [...scopeRequired("accountId", scope.accountId), "scriptName"],
2065
+ additionalProperties: false,
2066
+ },
2067
+ outputSchema: {
2068
+ type: "object",
2069
+ properties: {
2070
+ deleted: { type: "boolean" },
2071
+ scriptName: { type: "string" },
2072
+ },
2073
+ required: ["deleted", "scriptName"],
2074
+ },
2075
+ handler: async (args: JsonRecord, ctx) => {
2076
+ const scriptName = requireString(args, "scriptName");
2077
+ await callCloudflare(
2078
+ base,
2079
+ {
2080
+ method: "DELETE",
2081
+ path: `/accounts/${encodePathSegment(accountArg(args))}/workers/scripts/${encodePathSegment(scriptName)}`,
2082
+ query: { force: optionalBoolean(args, "force") },
2083
+ },
2084
+ ctx,
2085
+ );
2086
+ return { deleted: true, scriptName };
2087
+ },
2088
+ },
2089
+ {
2090
+ name: "list_kv_namespaces",
2091
+ description:
2092
+ "List Workers KV namespaces in an account, with the namespace ids bindings refer to.",
2093
+ annotations: readOnly,
2094
+ inputSchema: {
2095
+ type: "object",
2096
+ properties: {
2097
+ accountId: scopeProperty("accountId", scope.accountId),
2098
+ ...pagingInputProperties(1, 1000, { defaultPerPage: 20 }),
2099
+ raw: RAW_INPUT_PROPERTY,
2100
+ },
2101
+ required: scopeRequired("accountId", scope.accountId),
2102
+ additionalProperties: false,
2103
+ },
2104
+ outputSchema: listOutputSchema("namespaces", {
2105
+ type: "object",
2106
+ properties: {
2107
+ id: { type: "string" },
2108
+ title: { type: "string" },
2109
+ supportsUrlEncoding: { type: "boolean" },
2110
+ },
2111
+ required: ["id", "title"],
2112
+ }),
2113
+ handler: async (args: JsonRecord, ctx) => {
2114
+ const { result, resultInfo } = await callCloudflare(
2115
+ base,
2116
+ {
2117
+ method: "GET",
2118
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
2119
+ query: {
2120
+ page: optionalNumber(args, "page"),
2121
+ per_page: optionalNumber(args, "perPage"),
2122
+ },
2123
+ },
2124
+ ctx,
2125
+ );
2126
+ if (args["raw"] === true)
2127
+ return { namespaces: result, page: pageInfo(resultInfo) };
2128
+ return {
2129
+ namespaces: asArray(result).map(projectKvNamespace),
2130
+ page: pageInfo(resultInfo),
2131
+ };
2132
+ },
2133
+ },
2134
+ {
2135
+ name: "get_kv_namespace",
2136
+ description: "Get one Workers KV namespace by id.",
2137
+ annotations: readOnly,
2138
+ inputSchema: {
2139
+ type: "object",
2140
+ properties: {
2141
+ accountId: scopeProperty("accountId", scope.accountId),
2142
+ namespaceId: {
2143
+ type: "string",
2144
+ minLength: 1,
2145
+ description: "KV namespace id from list_kv_namespaces.",
2146
+ },
2147
+ },
2148
+ required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
2149
+ additionalProperties: false,
2150
+ },
2151
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2152
+ handler: async (args: JsonRecord, ctx) => {
2153
+ const { result } = await callCloudflare(
2154
+ base,
2155
+ {
2156
+ method: "GET",
2157
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
2158
+ },
2159
+ ctx,
2160
+ );
2161
+ return projectKvNamespace(result);
2162
+ },
2163
+ },
2164
+ {
2165
+ name: "create_kv_namespace",
2166
+ description: "Create a Workers KV namespace.",
2167
+ annotations: { readOnlyHint: false },
2168
+ inputSchema: {
2169
+ type: "object",
2170
+ properties: {
2171
+ accountId: scopeProperty("accountId", scope.accountId),
2172
+ title: {
2173
+ type: "string",
2174
+ minLength: 1,
2175
+ maxLength: 512,
2176
+ description: "Human-readable namespace title.",
2177
+ },
2178
+ },
2179
+ required: [...scopeRequired("accountId", scope.accountId), "title"],
2180
+ additionalProperties: false,
2181
+ },
2182
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2183
+ handler: async (args: JsonRecord, ctx) => {
2184
+ const { result } = await callCloudflare(
2185
+ base,
2186
+ {
2187
+ method: "POST",
2188
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces`,
2189
+ body: { title: requireString(args, "title") },
2190
+ },
2191
+ ctx,
2192
+ );
2193
+ return projectKvNamespace(result);
2194
+ },
2195
+ },
2196
+ {
2197
+ name: "rename_kv_namespace",
2198
+ description: "Rename an existing Workers KV namespace without changing its id or keys.",
2199
+ annotations: { readOnlyHint: false, destructiveHint: true },
2200
+ inputSchema: {
2201
+ type: "object",
2202
+ properties: {
2203
+ accountId: scopeProperty("accountId", scope.accountId),
2204
+ namespaceId: {
2205
+ type: "string",
2206
+ minLength: 1,
2207
+ description: "KV namespace id from list_kv_namespaces.",
2208
+ },
2209
+ title: {
2210
+ type: "string",
2211
+ minLength: 1,
2212
+ maxLength: 512,
2213
+ description: "Replacement namespace title.",
2214
+ },
2215
+ },
2216
+ required: [
2217
+ ...scopeRequired("accountId", scope.accountId),
2218
+ "namespaceId",
2219
+ "title",
2220
+ ],
2221
+ additionalProperties: false,
2222
+ },
2223
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2224
+ handler: async (args: JsonRecord, ctx) => {
2225
+ const { result } = await callCloudflare(
2226
+ base,
2227
+ {
2228
+ method: "PUT",
2229
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}`,
2230
+ body: { title: requireString(args, "title") },
2231
+ },
2232
+ ctx,
2233
+ );
2234
+ return result ?? { renamed: true, namespaceId: args["namespaceId"] };
2235
+ },
2236
+ },
2237
+ {
2238
+ name: "delete_kv_namespace",
2239
+ description: "Permanently delete a Workers KV namespace and every key stored in it.",
2240
+ annotations: { readOnlyHint: false, destructiveHint: true },
2241
+ inputSchema: {
2242
+ type: "object",
2243
+ properties: {
2244
+ accountId: scopeProperty("accountId", scope.accountId),
2245
+ namespaceId: {
2246
+ type: "string",
2247
+ minLength: 1,
2248
+ description: "KV namespace id from list_kv_namespaces.",
2249
+ },
2250
+ },
2251
+ required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
2252
+ additionalProperties: false,
2253
+ },
2254
+ outputSchema: {
2255
+ type: "object",
2256
+ properties: {
2257
+ deleted: { type: "boolean" },
2258
+ namespaceId: { type: "string" },
2259
+ },
2260
+ required: ["deleted", "namespaceId"],
2261
+ },
2262
+ handler: async (args: JsonRecord, ctx) => {
2263
+ const namespaceId = requireString(args, "namespaceId");
2264
+ await callCloudflare(
2265
+ base,
2266
+ {
2267
+ method: "DELETE",
2268
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(namespaceId)}`,
2269
+ },
2270
+ ctx,
2271
+ );
2272
+ return { deleted: true, namespaceId };
2273
+ },
2274
+ },
2275
+ {
2276
+ name: "list_kv_keys",
2277
+ description:
2278
+ "List keys and metadata in a Workers KV namespace by prefix, using cursor pagination.",
2279
+ annotations: readOnly,
2280
+ inputSchema: {
2281
+ type: "object",
2282
+ properties: {
2283
+ accountId: scopeProperty("accountId", scope.accountId),
2284
+ namespaceId: {
2285
+ type: "string",
2286
+ minLength: 1,
2287
+ description: "KV namespace id from list_kv_namespaces.",
2288
+ },
2289
+ prefix: {
2290
+ type: "string",
2291
+ description: "Return only keys beginning with this prefix.",
2292
+ },
2293
+ limit: {
2294
+ type: "integer",
2295
+ minimum: 10,
2296
+ maximum: 1000,
2297
+ description: "Keys per request, 10 to 1000. Defaults to 1000.",
2298
+ },
2299
+ cursor: {
2300
+ type: "string",
2301
+ description: "Opaque cursor returned as nextCursor by the previous call.",
2302
+ },
2303
+ },
2304
+ required: [...scopeRequired("accountId", scope.accountId), "namespaceId"],
2305
+ additionalProperties: false,
2306
+ },
2307
+ outputSchema: {
2308
+ type: "object",
2309
+ properties: {
2310
+ keys: { type: "array", items: OPEN_OBJECT_OUTPUT_SCHEMA },
2311
+ nextCursor: { type: "string" },
2312
+ },
2313
+ required: ["keys"],
2314
+ },
2315
+ handler: async (args: JsonRecord, ctx) => {
2316
+ const { result, resultInfo } = await callCloudflare(
2317
+ base,
2318
+ {
2319
+ method: "GET",
2320
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/keys`,
2321
+ query: {
2322
+ prefix: optionalString(args, "prefix"),
2323
+ limit: optionalNumber(args, "limit"),
2324
+ cursor: optionalString(args, "cursor"),
2325
+ },
2326
+ },
2327
+ ctx,
2328
+ );
2329
+ const cursor = resultInfo?.cursor;
2330
+ return {
2331
+ keys: asArray(result).map(projectKvKey),
2332
+ ...(typeof cursor === "string" && cursor !== ""
2333
+ ? { nextCursor: cursor }
2334
+ : {}),
2335
+ };
2336
+ },
2337
+ },
2338
+ {
2339
+ name: "bulk_get_kv_values",
2340
+ description:
2341
+ "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.",
2342
+ annotations: readOnly,
2343
+ inputSchema: {
2344
+ type: "object",
2345
+ properties: {
2346
+ accountId: scopeProperty("accountId", scope.accountId),
2347
+ namespaceId: {
2348
+ type: "string",
2349
+ minLength: 1,
2350
+ description: "KV namespace id from list_kv_namespaces.",
2351
+ },
2352
+ keys: {
2353
+ type: "array",
2354
+ minItems: 1,
2355
+ maxItems: 100,
2356
+ items: { type: "string", minLength: 1, maxLength: 512 },
2357
+ description: "Key names to retrieve, up to 100.",
2358
+ },
2359
+ withMetadata: {
2360
+ type: "boolean",
2361
+ description: "Include each key's metadata and expiration when true.",
2362
+ },
2363
+ type: {
2364
+ type: "string",
2365
+ enum: ["text", "json"],
2366
+ description: "Return strings as stored, or parse JSON values before returning them.",
2367
+ },
2368
+ },
2369
+ required: [
2370
+ ...scopeRequired("accountId", scope.accountId),
2371
+ "namespaceId",
2372
+ "keys",
2373
+ ],
2374
+ additionalProperties: false,
2375
+ },
2376
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2377
+ handler: async (args: JsonRecord, ctx) => {
2378
+ const { result } = await callCloudflare(
2379
+ base,
2380
+ {
2381
+ method: "POST",
2382
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/get`,
2383
+ body: {
2384
+ keys: args["keys"],
2385
+ ...(args["withMetadata"] !== undefined
2386
+ ? { withMetadata: args["withMetadata"] }
2387
+ : {}),
2388
+ ...(args["type"] !== undefined ? { type: args["type"] } : {}),
2389
+ },
2390
+ },
2391
+ ctx,
2392
+ );
2393
+ return asRecord(result);
2394
+ },
2395
+ },
2396
+ {
2397
+ name: "bulk_write_kv_values",
2398
+ description:
2399
+ "Create or replace multiple Workers KV values, with optional expirations and JSON metadata.",
2400
+ annotations: { readOnlyHint: false, destructiveHint: true },
2401
+ inputSchema: {
2402
+ type: "object",
2403
+ properties: {
2404
+ accountId: scopeProperty("accountId", scope.accountId),
2405
+ namespaceId: {
2406
+ type: "string",
2407
+ minLength: 1,
2408
+ description: "KV namespace id from list_kv_namespaces.",
2409
+ },
2410
+ entries: {
2411
+ type: "array",
2412
+ minItems: 1,
2413
+ maxItems: 10_000,
2414
+ description: "Key/value entries to write, up to Cloudflare's 10,000-key bulk limit.",
2415
+ items: {
2416
+ type: "object",
2417
+ properties: {
2418
+ key: { type: "string", minLength: 1, maxLength: 512 },
2419
+ value: { type: "string", maxLength: 26_214_400 },
2420
+ expiration: { type: "number" },
2421
+ expiration_ttl: { type: "number", minimum: 60 },
2422
+ metadata: { type: ["object", "array", "string", "number", "boolean", "null"] },
2423
+ base64: { type: "boolean" },
2424
+ },
2425
+ required: ["key", "value"],
2426
+ additionalProperties: false,
2427
+ },
2428
+ },
2429
+ },
2430
+ required: [
2431
+ ...scopeRequired("accountId", scope.accountId),
2432
+ "namespaceId",
2433
+ "entries",
2434
+ ],
2435
+ additionalProperties: false,
2436
+ },
2437
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2438
+ handler: async (args: JsonRecord, ctx) => {
2439
+ const { result } = await callCloudflare(
2440
+ base,
2441
+ {
2442
+ method: "PUT",
2443
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk`,
2444
+ body: args["entries"],
2445
+ },
2446
+ ctx,
2447
+ );
2448
+ return asRecord(result);
2449
+ },
2450
+ },
2451
+ {
2452
+ name: "bulk_delete_kv_values",
2453
+ description: "Permanently delete multiple keys from a Workers KV namespace.",
2454
+ annotations: { readOnlyHint: false, destructiveHint: true },
2455
+ inputSchema: {
2456
+ type: "object",
2457
+ properties: {
2458
+ accountId: scopeProperty("accountId", scope.accountId),
2459
+ namespaceId: {
2460
+ type: "string",
2461
+ minLength: 1,
2462
+ description: "KV namespace id from list_kv_namespaces.",
2463
+ },
2464
+ keys: {
2465
+ type: "array",
2466
+ minItems: 1,
2467
+ maxItems: 10_000,
2468
+ items: { type: "string", minLength: 1, maxLength: 512 },
2469
+ description: "Key names to delete, up to Cloudflare's 10,000-key bulk limit.",
2470
+ },
2471
+ },
2472
+ required: [
2473
+ ...scopeRequired("accountId", scope.accountId),
2474
+ "namespaceId",
2475
+ "keys",
2476
+ ],
2477
+ additionalProperties: false,
2478
+ },
2479
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2480
+ handler: async (args: JsonRecord, ctx) => {
2481
+ const { result } = await callCloudflare(
2482
+ base,
2483
+ {
2484
+ method: "POST",
2485
+ path: `/accounts/${encodePathSegment(accountArg(args))}/storage/kv/namespaces/${encodePathSegment(requireString(args, "namespaceId"))}/bulk/delete`,
2486
+ body: args["keys"],
2487
+ },
2488
+ ctx,
2489
+ );
2490
+ return asRecord(result);
2491
+ },
2492
+ },
2493
+ {
2494
+ name: "list_r2_buckets",
2495
+ description:
2496
+ "List R2 buckets in an account, with location and storage class.",
2497
+ annotations: readOnly,
2498
+ inputSchema: {
2499
+ type: "object",
2500
+ properties: {
2501
+ accountId: scopeProperty("accountId", scope.accountId),
2502
+ nameContains: {
2503
+ type: "string",
2504
+ description: "Filter to buckets whose name contains this string.",
2505
+ },
2506
+ perPage: {
2507
+ type: "integer",
2508
+ minimum: 1,
2509
+ maximum: 1000,
2510
+ description: "Buckets per request, 1 to 1000. Defaults to 20.",
2511
+ },
2512
+ cursor: {
2513
+ type: "string",
2514
+ description:
2515
+ "Opaque cursor from a previous call's nextCursor. R2 paginates by cursor, not page number.",
2516
+ },
2517
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2518
+ raw: RAW_INPUT_PROPERTY,
2519
+ },
2520
+ required: scopeRequired("accountId", scope.accountId),
2521
+ additionalProperties: false,
2522
+ },
2523
+ outputSchema: {
2524
+ type: "object",
2525
+ properties: {
2526
+ buckets: {
2527
+ type: "array",
2528
+ items: {
2529
+ type: "object",
2530
+ properties: {
2531
+ name: { type: "string" },
2532
+ location: { type: "string" },
2533
+ storageClass: { type: "string" },
2534
+ jurisdiction: { type: "string" },
2535
+ creationDate: { type: "string" },
2536
+ },
1162
2537
  required: ["name"],
1163
2538
  },
1164
2539
  },
1165
- nextCursor: {
2540
+ nextCursor: {
2541
+ type: "string",
2542
+ description:
2543
+ "Pass back as `cursor` to continue. Absent when the listing is complete.",
2544
+ },
2545
+ },
2546
+ required: ["buckets"],
2547
+ },
2548
+ handler: async (args: JsonRecord, ctx) => {
2549
+ const { result, resultInfo } = await callCloudflare(
2550
+ base,
2551
+ {
2552
+ method: "GET",
2553
+ path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
2554
+ query: {
2555
+ name_contains: optionalString(args, "nameContains"),
2556
+ per_page: optionalNumber(args, "perPage"),
2557
+ cursor: optionalString(args, "cursor"),
2558
+ },
2559
+ headers: r2Headers(args),
2560
+ },
2561
+ ctx,
2562
+ );
2563
+ // R2 nests its list under `buckets` rather than returning a bare array,
2564
+ // and its result_info carries a cursor instead of page counters.
2565
+ const cursor = resultInfo?.cursor;
2566
+ const next =
2567
+ typeof cursor === "string" && cursor !== ""
2568
+ ? { nextCursor: cursor }
2569
+ : {};
2570
+ if (args["raw"] === true) return { buckets: result, ...next };
2571
+ return {
2572
+ buckets: asArray(asRecord(result)["buckets"]).map(projectR2Bucket),
2573
+ ...next,
2574
+ };
2575
+ },
2576
+ },
2577
+ {
2578
+ name: "get_r2_bucket",
2579
+ description: "Get one R2 bucket's location, jurisdiction, storage class, and creation time.",
2580
+ annotations: readOnly,
2581
+ inputSchema: {
2582
+ type: "object",
2583
+ properties: {
2584
+ accountId: scopeProperty("accountId", scope.accountId),
2585
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2586
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2587
+ },
2588
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2589
+ additionalProperties: false,
2590
+ },
2591
+ outputSchema: R2_BUCKET_SCHEMA,
2592
+ handler: async (args: JsonRecord, ctx) => {
2593
+ const { result } = await callCloudflare(
2594
+ base,
2595
+ {
2596
+ method: "GET",
2597
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2598
+ headers: r2Headers(args),
2599
+ },
2600
+ ctx,
2601
+ );
2602
+ return projectR2Bucket(result);
2603
+ },
2604
+ },
2605
+ {
2606
+ name: "create_r2_bucket",
2607
+ description: "Create an R2 bucket with an optional location hint and default storage class.",
2608
+ annotations: { readOnlyHint: false },
2609
+ inputSchema: {
2610
+ type: "object",
2611
+ properties: {
2612
+ accountId: scopeProperty("accountId", scope.accountId),
2613
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2614
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2615
+ locationHint: {
2616
+ type: "string",
2617
+ enum: ["apac", "eeur", "enam", "weur", "wnam", "oc"],
2618
+ description: "Optional placement hint for the new bucket.",
2619
+ },
2620
+ storageClass: {
2621
+ type: "string",
2622
+ enum: ["Standard", "InfrequentAccess"],
2623
+ description: "Default storage class for new objects.",
2624
+ },
2625
+ },
2626
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2627
+ additionalProperties: false,
2628
+ },
2629
+ outputSchema: R2_BUCKET_SCHEMA,
2630
+ handler: async (args: JsonRecord, ctx) => {
2631
+ const { result } = await callCloudflare(
2632
+ base,
2633
+ {
2634
+ method: "POST",
2635
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets`,
2636
+ headers: r2Headers(args),
2637
+ body: {
2638
+ name: requireString(args, "bucketName"),
2639
+ ...(args["locationHint"] !== undefined
2640
+ ? { locationHint: args["locationHint"] }
2641
+ : {}),
2642
+ ...(args["storageClass"] !== undefined
2643
+ ? { storageClass: args["storageClass"] }
2644
+ : {}),
2645
+ },
2646
+ },
2647
+ ctx,
2648
+ );
2649
+ return projectR2Bucket(result);
2650
+ },
2651
+ },
2652
+ {
2653
+ name: "update_r2_bucket",
2654
+ description: "Change the default storage class used for newly uploaded objects in an R2 bucket.",
2655
+ annotations: { readOnlyHint: false, destructiveHint: true },
2656
+ inputSchema: {
2657
+ type: "object",
2658
+ properties: {
2659
+ accountId: scopeProperty("accountId", scope.accountId),
2660
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2661
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2662
+ storageClass: {
2663
+ type: "string",
2664
+ enum: ["Standard", "InfrequentAccess"],
2665
+ description: "New default storage class for future uploads.",
2666
+ },
2667
+ },
2668
+ required: [
2669
+ ...scopeRequired("accountId", scope.accountId),
2670
+ "bucketName",
2671
+ "storageClass",
2672
+ ],
2673
+ additionalProperties: false,
2674
+ },
2675
+ outputSchema: R2_BUCKET_SCHEMA,
2676
+ handler: async (args: JsonRecord, ctx) => {
2677
+ const { result } = await callCloudflare(
2678
+ base,
2679
+ {
2680
+ method: "PATCH",
2681
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}`,
2682
+ headers: {
2683
+ ...r2Headers(args),
2684
+ "cf-r2-storage-class": String(args["storageClass"]),
2685
+ },
2686
+ },
2687
+ ctx,
2688
+ );
2689
+ return projectR2Bucket(result);
2690
+ },
2691
+ },
2692
+ {
2693
+ name: "delete_r2_bucket",
2694
+ description:
2695
+ "Permanently delete an empty R2 bucket and all of its configuration. Cloudflare refuses non-empty buckets.",
2696
+ annotations: { readOnlyHint: false, destructiveHint: true },
2697
+ inputSchema: {
2698
+ type: "object",
2699
+ properties: {
2700
+ accountId: scopeProperty("accountId", scope.accountId),
2701
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2702
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2703
+ },
2704
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2705
+ additionalProperties: false,
2706
+ },
2707
+ outputSchema: {
2708
+ type: "object",
2709
+ properties: {
2710
+ deleted: { type: "boolean" },
2711
+ bucketName: { type: "string" },
2712
+ },
2713
+ required: ["deleted", "bucketName"],
2714
+ },
2715
+ handler: async (args: JsonRecord, ctx) => {
2716
+ const bucketName = requireString(args, "bucketName");
2717
+ await callCloudflare(
2718
+ base,
2719
+ {
2720
+ method: "DELETE",
2721
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(bucketName)}`,
2722
+ headers: r2Headers(args),
2723
+ },
2724
+ ctx,
2725
+ );
2726
+ return { deleted: true, bucketName };
2727
+ },
2728
+ },
2729
+ {
2730
+ name: "list_r2_objects",
2731
+ description:
2732
+ "List object keys and metadata in an R2 bucket by prefix, with delimiter grouping and cursor pagination.",
2733
+ annotations: readOnly,
2734
+ inputSchema: {
2735
+ type: "object",
2736
+ properties: {
2737
+ accountId: scopeProperty("accountId", scope.accountId),
2738
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2739
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2740
+ prefix: {
2741
+ type: "string",
2742
+ description: "Return only object keys beginning with this prefix.",
2743
+ },
2744
+ delimiter: {
2745
+ type: "string",
2746
+ minLength: 1,
2747
+ maxLength: 1,
2748
+ description: "One character used to group path-like keys, usually '/'.",
2749
+ },
2750
+ startAfter: {
2751
+ type: "string",
2752
+ description: "Begin after this key in lexicographic order.",
2753
+ },
2754
+ perPage: {
2755
+ type: "integer",
2756
+ minimum: 1,
2757
+ maximum: 1000,
2758
+ description: "Objects per request, 1 to 1000.",
2759
+ },
2760
+ cursor: {
2761
+ type: "string",
2762
+ description: "Opaque cursor returned as nextCursor by the previous call.",
2763
+ },
2764
+ },
2765
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2766
+ additionalProperties: false,
2767
+ },
2768
+ outputSchema: {
2769
+ type: "object",
2770
+ properties: {
2771
+ objects: { type: "array", items: R2_OBJECT_SCHEMA },
2772
+ commonPrefixes: { type: "array", items: { type: "string" } },
2773
+ nextCursor: { type: "string" },
2774
+ truncated: { type: "boolean" },
2775
+ },
2776
+ required: ["objects", "truncated"],
2777
+ },
2778
+ handler: async (args: JsonRecord, ctx) => {
2779
+ const { result, resultInfo } = await callCloudflare(
2780
+ base,
2781
+ {
2782
+ method: "GET",
2783
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects`,
2784
+ headers: r2Headers(args),
2785
+ query: {
2786
+ prefix: optionalString(args, "prefix"),
2787
+ delimiter: optionalString(args, "delimiter"),
2788
+ start_after: optionalString(args, "startAfter"),
2789
+ per_page: optionalNumber(args, "perPage"),
2790
+ cursor: optionalString(args, "cursor"),
2791
+ },
2792
+ },
2793
+ ctx,
2794
+ );
2795
+ const cursor = resultInfo?.cursor;
2796
+ return {
2797
+ objects: asArray(result).map(projectR2Object),
2798
+ ...(Array.isArray(resultInfo?.delimited)
2799
+ ? { commonPrefixes: resultInfo.delimited }
2800
+ : {}),
2801
+ ...(typeof cursor === "string" && cursor !== ""
2802
+ ? { nextCursor: cursor }
2803
+ : {}),
2804
+ truncated: resultInfo?.is_truncated === true,
2805
+ };
2806
+ },
2807
+ },
2808
+ {
2809
+ name: "delete_r2_object",
2810
+ description: "Permanently delete one object from an R2 bucket by key.",
2811
+ annotations: { readOnlyHint: false, destructiveHint: true },
2812
+ inputSchema: {
2813
+ type: "object",
2814
+ properties: {
2815
+ accountId: scopeProperty("accountId", scope.accountId),
2816
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2817
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2818
+ objectKey: {
1166
2819
  type: "string",
2820
+ minLength: 1,
2821
+ description: "Exact object key. Slashes are preserved as path separators.",
2822
+ },
2823
+ },
2824
+ required: [
2825
+ ...scopeRequired("accountId", scope.accountId),
2826
+ "bucketName",
2827
+ "objectKey",
2828
+ ],
2829
+ additionalProperties: false,
2830
+ },
2831
+ outputSchema: {
2832
+ type: "object",
2833
+ properties: {
2834
+ deleted: { type: "boolean" },
2835
+ objectKey: { type: "string" },
2836
+ },
2837
+ required: ["deleted", "objectKey"],
2838
+ },
2839
+ handler: async (args: JsonRecord, ctx) => {
2840
+ const objectKey = requireString(args, "objectKey");
2841
+ await callCloudflare(
2842
+ base,
2843
+ {
2844
+ method: "DELETE",
2845
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/objects/${encodeObjectKey(objectKey)}`,
2846
+ headers: r2Headers(args),
2847
+ },
2848
+ ctx,
2849
+ );
2850
+ return { deleted: true, objectKey };
2851
+ },
2852
+ },
2853
+ {
2854
+ name: "get_r2_metrics",
2855
+ description:
2856
+ "Get account-level R2 object-count and storage-size metrics split by storage class and publication state.",
2857
+ annotations: readOnly,
2858
+ inputSchema: {
2859
+ type: "object",
2860
+ properties: { accountId: scopeProperty("accountId", scope.accountId) },
2861
+ required: scopeRequired("accountId", scope.accountId),
2862
+ additionalProperties: false,
2863
+ },
2864
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2865
+ handler: async (args: JsonRecord, ctx) => {
2866
+ const { result } = await callCloudflare(
2867
+ base,
2868
+ {
2869
+ method: "GET",
2870
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/metrics`,
2871
+ },
2872
+ ctx,
2873
+ );
2874
+ return asRecord(result);
2875
+ },
2876
+ },
2877
+ {
2878
+ name: "get_r2_cors",
2879
+ description: "Get the browser CORS rules configured on an R2 bucket.",
2880
+ annotations: readOnly,
2881
+ inputSchema: {
2882
+ type: "object",
2883
+ properties: {
2884
+ accountId: scopeProperty("accountId", scope.accountId),
2885
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2886
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2887
+ },
2888
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2889
+ additionalProperties: false,
2890
+ },
2891
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2892
+ handler: async (args: JsonRecord, ctx) => {
2893
+ const { result } = await callCloudflare(
2894
+ base,
2895
+ {
2896
+ method: "GET",
2897
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2898
+ headers: r2Headers(args),
2899
+ },
2900
+ ctx,
2901
+ );
2902
+ return asRecord(result);
2903
+ },
2904
+ },
2905
+ {
2906
+ name: "set_r2_cors",
2907
+ description:
2908
+ "Replace an R2 bucket's CORS policy. Supply the complete desired rule list; omitted existing rules are removed.",
2909
+ annotations: { readOnlyHint: false, destructiveHint: true },
2910
+ inputSchema: {
2911
+ type: "object",
2912
+ properties: {
2913
+ accountId: scopeProperty("accountId", scope.accountId),
2914
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2915
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2916
+ rules: {
2917
+ type: "array",
2918
+ maxItems: 100,
1167
2919
  description:
1168
- "Pass back as `cursor` to continue. Absent when the listing is complete.",
2920
+ "Complete CORS rule list using Cloudflare fields: allowed.methods, allowed.origins, optional allowed.headers, id, exposeHeaders, and maxAgeSeconds.",
2921
+ items: { type: "object", additionalProperties: true },
1169
2922
  },
1170
2923
  },
1171
- required: ["buckets"],
2924
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName", "rules"],
2925
+ additionalProperties: false,
2926
+ },
2927
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
2928
+ handler: async (args: JsonRecord, ctx) => {
2929
+ const { result } = await callCloudflare(
2930
+ base,
2931
+ {
2932
+ method: "PUT",
2933
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2934
+ headers: r2Headers(args),
2935
+ body: { rules: args["rules"] },
2936
+ },
2937
+ ctx,
2938
+ );
2939
+ return asRecord(result);
2940
+ },
2941
+ },
2942
+ {
2943
+ name: "delete_r2_cors",
2944
+ description: "Remove the complete CORS policy from an R2 bucket.",
2945
+ annotations: { readOnlyHint: false, destructiveHint: true },
2946
+ inputSchema: {
2947
+ type: "object",
2948
+ properties: {
2949
+ accountId: scopeProperty("accountId", scope.accountId),
2950
+ bucketName: R2_BUCKET_NAME_PROPERTY,
2951
+ jurisdiction: R2_JURISDICTION_PROPERTY,
2952
+ },
2953
+ required: [...scopeRequired("accountId", scope.accountId), "bucketName"],
2954
+ additionalProperties: false,
2955
+ },
2956
+ outputSchema: {
2957
+ type: "object",
2958
+ properties: { deleted: { type: "boolean" } },
2959
+ required: ["deleted"],
1172
2960
  },
1173
2961
  handler: async (args: JsonRecord, ctx) => {
1174
- const { result, resultInfo } = await callCloudflare(
2962
+ await callCloudflare(
1175
2963
  base,
1176
2964
  {
1177
- method: "GET",
1178
- path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
1179
- query: {
1180
- name_contains: optionalString(args, "nameContains"),
1181
- per_page: optionalNumber(args, "perPage"),
1182
- cursor: optionalString(args, "cursor"),
1183
- },
2965
+ method: "DELETE",
2966
+ path: `/accounts/${encodePathSegment(accountArg(args))}/r2/buckets/${encodePathSegment(requireString(args, "bucketName"))}/cors`,
2967
+ headers: r2Headers(args),
1184
2968
  },
1185
2969
  ctx,
1186
2970
  );
1187
- // R2 nests its list under `buckets` rather than returning a bare array,
1188
- // and its result_info carries a cursor instead of page counters.
1189
- const cursor = resultInfo?.cursor;
1190
- const next =
1191
- typeof cursor === "string" && cursor !== ""
1192
- ? { nextCursor: cursor }
1193
- : {};
1194
- if (args["raw"] === true) return { buckets: result, ...next };
1195
- return {
1196
- buckets: asArray(asRecord(result)["buckets"]).map(projectR2Bucket),
1197
- ...next,
1198
- };
2971
+ return { deleted: true };
1199
2972
  },
1200
2973
  },
1201
2974
  {
@@ -1254,6 +3027,338 @@ function buildTools(scope: Scoping): ApiTool[] {
1254
3027
  };
1255
3028
  },
1256
3029
  },
3030
+ {
3031
+ name: "get_pages_project",
3032
+ description: "Get one Pages project, including build configuration, deployment configuration, domains, and latest deployment.",
3033
+ annotations: readOnly,
3034
+ inputSchema: {
3035
+ type: "object",
3036
+ properties: {
3037
+ accountId: scopeProperty("accountId", scope.accountId),
3038
+ projectName: {
3039
+ type: "string",
3040
+ minLength: 1,
3041
+ description: "Pages project name from list_pages_projects.",
3042
+ },
3043
+ raw: RAW_INPUT_PROPERTY,
3044
+ },
3045
+ required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3046
+ additionalProperties: false,
3047
+ },
3048
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3049
+ handler: async (args: JsonRecord, ctx) => {
3050
+ const { result } = await callCloudflare(
3051
+ base,
3052
+ {
3053
+ method: "GET",
3054
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}`,
3055
+ },
3056
+ ctx,
3057
+ );
3058
+ return args["raw"] === true ? result : projectPagesProject(result);
3059
+ },
3060
+ },
3061
+ {
3062
+ name: "list_pages_deployments",
3063
+ description: "List production and preview deployments for a Pages project.",
3064
+ annotations: readOnly,
3065
+ inputSchema: {
3066
+ type: "object",
3067
+ properties: {
3068
+ accountId: scopeProperty("accountId", scope.accountId),
3069
+ projectName: {
3070
+ type: "string",
3071
+ minLength: 1,
3072
+ description: "Pages project name from list_pages_projects.",
3073
+ },
3074
+ env: {
3075
+ type: "string",
3076
+ enum: ["production", "preview"],
3077
+ description: "Optional deployment environment filter.",
3078
+ },
3079
+ ...pagingInputProperties(1, 100, { bounds: "undocumented" }),
3080
+ },
3081
+ required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3082
+ additionalProperties: false,
3083
+ },
3084
+ outputSchema: listOutputSchema("deployments", OPEN_OBJECT_OUTPUT_SCHEMA),
3085
+ handler: async (args: JsonRecord, ctx) => {
3086
+ const { result, resultInfo } = await callCloudflare(
3087
+ base,
3088
+ {
3089
+ method: "GET",
3090
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments`,
3091
+ query: {
3092
+ env: optionalString(args, "env"),
3093
+ page: optionalNumber(args, "page"),
3094
+ per_page: optionalNumber(args, "perPage"),
3095
+ },
3096
+ },
3097
+ ctx,
3098
+ );
3099
+ return {
3100
+ deployments: asArray(result).map(projectPagesDeployment),
3101
+ page: pageInfo(resultInfo),
3102
+ };
3103
+ },
3104
+ },
3105
+ {
3106
+ name: "get_pages_deployment",
3107
+ description: "Get one Pages deployment including its environment, URLs, stages, source, and build configuration.",
3108
+ annotations: readOnly,
3109
+ inputSchema: {
3110
+ type: "object",
3111
+ properties: {
3112
+ accountId: scopeProperty("accountId", scope.accountId),
3113
+ projectName: {
3114
+ type: "string",
3115
+ minLength: 1,
3116
+ description: "Pages project name from list_pages_projects.",
3117
+ },
3118
+ deploymentId: {
3119
+ type: "string",
3120
+ minLength: 1,
3121
+ description: "Deployment id from list_pages_deployments.",
3122
+ },
3123
+ raw: RAW_INPUT_PROPERTY,
3124
+ },
3125
+ required: [
3126
+ ...scopeRequired("accountId", scope.accountId),
3127
+ "projectName",
3128
+ "deploymentId",
3129
+ ],
3130
+ additionalProperties: false,
3131
+ },
3132
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3133
+ handler: async (args: JsonRecord, ctx) => {
3134
+ const { result } = await callCloudflare(
3135
+ base,
3136
+ {
3137
+ method: "GET",
3138
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}`,
3139
+ },
3140
+ ctx,
3141
+ );
3142
+ return args["raw"] === true ? result : projectPagesDeployment(result);
3143
+ },
3144
+ },
3145
+ {
3146
+ name: "retry_pages_deployment",
3147
+ description: "Retry a failed or cancelled Pages deployment using its existing source and build configuration.",
3148
+ annotations: { readOnlyHint: false },
3149
+ inputSchema: {
3150
+ type: "object",
3151
+ properties: {
3152
+ accountId: scopeProperty("accountId", scope.accountId),
3153
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3154
+ deploymentId: { type: "string", minLength: 1, description: "Deployment id to retry." },
3155
+ },
3156
+ required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
3157
+ additionalProperties: false,
3158
+ },
3159
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3160
+ handler: async (args: JsonRecord, ctx) => {
3161
+ const { result } = await callCloudflare(
3162
+ base,
3163
+ {
3164
+ method: "POST",
3165
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/retry`,
3166
+ },
3167
+ ctx,
3168
+ );
3169
+ return projectPagesDeployment(result);
3170
+ },
3171
+ },
3172
+ {
3173
+ name: "rollback_pages_deployment",
3174
+ description: "Promote a previous Pages deployment to production, replacing the currently served production deployment.",
3175
+ annotations: { readOnlyHint: false, destructiveHint: true },
3176
+ inputSchema: {
3177
+ type: "object",
3178
+ properties: {
3179
+ accountId: scopeProperty("accountId", scope.accountId),
3180
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3181
+ deploymentId: { type: "string", minLength: 1, description: "Previous deployment id to promote." },
3182
+ },
3183
+ required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
3184
+ additionalProperties: false,
3185
+ },
3186
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3187
+ handler: async (args: JsonRecord, ctx) => {
3188
+ const { result } = await callCloudflare(
3189
+ base,
3190
+ {
3191
+ method: "POST",
3192
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(requireString(args, "deploymentId"))}/rollback`,
3193
+ },
3194
+ ctx,
3195
+ );
3196
+ return projectPagesDeployment(result);
3197
+ },
3198
+ },
3199
+ {
3200
+ name: "delete_pages_deployment",
3201
+ description: "Permanently delete a Pages deployment and its immutable deployment URL.",
3202
+ annotations: { readOnlyHint: false, destructiveHint: true },
3203
+ inputSchema: {
3204
+ type: "object",
3205
+ properties: {
3206
+ accountId: scopeProperty("accountId", scope.accountId),
3207
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3208
+ deploymentId: { type: "string", minLength: 1, description: "Deployment id to delete." },
3209
+ },
3210
+ required: [...scopeRequired("accountId", scope.accountId), "projectName", "deploymentId"],
3211
+ additionalProperties: false,
3212
+ },
3213
+ outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["deleted", "deploymentId"] },
3214
+ handler: async (args: JsonRecord, ctx) => {
3215
+ const deploymentId = requireString(args, "deploymentId");
3216
+ await callCloudflare(
3217
+ base,
3218
+ {
3219
+ method: "DELETE",
3220
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/deployments/${encodePathSegment(deploymentId)}`,
3221
+ },
3222
+ ctx,
3223
+ );
3224
+ return { deleted: true, deploymentId };
3225
+ },
3226
+ },
3227
+ {
3228
+ name: "list_pages_domains",
3229
+ description: "List custom domains attached to a Pages project and their validation status.",
3230
+ annotations: readOnly,
3231
+ inputSchema: {
3232
+ type: "object",
3233
+ properties: {
3234
+ accountId: scopeProperty("accountId", scope.accountId),
3235
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3236
+ },
3237
+ required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3238
+ additionalProperties: false,
3239
+ },
3240
+ outputSchema: listOutputSchema("domains", OPEN_OBJECT_OUTPUT_SCHEMA),
3241
+ handler: async (args: JsonRecord, ctx) => {
3242
+ const { result } = await callCloudflare(
3243
+ base,
3244
+ {
3245
+ method: "GET",
3246
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
3247
+ },
3248
+ ctx,
3249
+ );
3250
+ return { domains: asArray(result).map(projectPagesDomain) };
3251
+ },
3252
+ },
3253
+ {
3254
+ name: "add_pages_domain",
3255
+ description: "Attach a custom domain to a Pages project. DNS ownership and validation still apply.",
3256
+ annotations: { readOnlyHint: false },
3257
+ inputSchema: {
3258
+ type: "object",
3259
+ properties: {
3260
+ accountId: scopeProperty("accountId", scope.accountId),
3261
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3262
+ domain: { type: "string", minLength: 1, description: "Fully qualified custom domain to attach." },
3263
+ },
3264
+ required: [...scopeRequired("accountId", scope.accountId), "projectName", "domain"],
3265
+ additionalProperties: false,
3266
+ },
3267
+ outputSchema: OPEN_OBJECT_OUTPUT_SCHEMA,
3268
+ handler: async (args: JsonRecord, ctx) => {
3269
+ const { result } = await callCloudflare(
3270
+ base,
3271
+ {
3272
+ method: "POST",
3273
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains`,
3274
+ body: { name: requireString(args, "domain") },
3275
+ },
3276
+ ctx,
3277
+ );
3278
+ return projectPagesDomain(result);
3279
+ },
3280
+ },
3281
+ {
3282
+ name: "delete_pages_domain",
3283
+ description: "Detach a custom domain from a Pages project.",
3284
+ annotations: { readOnlyHint: false, destructiveHint: true },
3285
+ inputSchema: {
3286
+ type: "object",
3287
+ properties: {
3288
+ accountId: scopeProperty("accountId", scope.accountId),
3289
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3290
+ domain: { type: "string", minLength: 1, description: "Custom domain to detach." },
3291
+ },
3292
+ required: [...scopeRequired("accountId", scope.accountId), "projectName", "domain"],
3293
+ additionalProperties: false,
3294
+ },
3295
+ outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, domain: { type: "string" } }, required: ["deleted", "domain"] },
3296
+ handler: async (args: JsonRecord, ctx) => {
3297
+ const domain = requireString(args, "domain");
3298
+ await callCloudflare(
3299
+ base,
3300
+ {
3301
+ method: "DELETE",
3302
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/domains/${encodePathSegment(domain)}`,
3303
+ },
3304
+ ctx,
3305
+ );
3306
+ return { deleted: true, domain };
3307
+ },
3308
+ },
3309
+ {
3310
+ name: "purge_pages_build_cache",
3311
+ description: "Clear a Pages project's build cache so its next deployment rebuilds dependencies and artifacts from scratch.",
3312
+ annotations: { readOnlyHint: false, destructiveHint: true },
3313
+ inputSchema: {
3314
+ type: "object",
3315
+ properties: {
3316
+ accountId: scopeProperty("accountId", scope.accountId),
3317
+ projectName: { type: "string", minLength: 1, description: "Pages project name." },
3318
+ },
3319
+ required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3320
+ additionalProperties: false,
3321
+ },
3322
+ outputSchema: { type: "object", properties: { purged: { type: "boolean" } }, required: ["purged"] },
3323
+ handler: async (args: JsonRecord, ctx) => {
3324
+ await callCloudflare(
3325
+ base,
3326
+ {
3327
+ method: "POST",
3328
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(requireString(args, "projectName"))}/purge_build_cache`,
3329
+ },
3330
+ ctx,
3331
+ );
3332
+ return { purged: true };
3333
+ },
3334
+ },
3335
+ {
3336
+ name: "delete_pages_project",
3337
+ description: "Permanently delete a Pages project, its deployments, and project configuration.",
3338
+ annotations: { readOnlyHint: false, destructiveHint: true },
3339
+ inputSchema: {
3340
+ type: "object",
3341
+ properties: {
3342
+ accountId: scopeProperty("accountId", scope.accountId),
3343
+ projectName: { type: "string", minLength: 1, description: "Pages project name to delete." },
3344
+ },
3345
+ required: [...scopeRequired("accountId", scope.accountId), "projectName"],
3346
+ additionalProperties: false,
3347
+ },
3348
+ outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, projectName: { type: "string" } }, required: ["deleted", "projectName"] },
3349
+ handler: async (args: JsonRecord, ctx) => {
3350
+ const projectName = requireString(args, "projectName");
3351
+ await callCloudflare(
3352
+ base,
3353
+ {
3354
+ method: "DELETE",
3355
+ path: `/accounts/${encodePathSegment(accountArg(args))}/pages/projects/${encodePathSegment(projectName)}`,
3356
+ },
3357
+ ctx,
3358
+ );
3359
+ return { deleted: true, projectName };
3360
+ },
3361
+ },
1257
3362
  {
1258
3363
  // Additive: brings a record into being and destroys nothing, so
1259
3364
  // `destructiveHint` stays unset. `readOnlyHint: false` already routes it
@@ -1630,12 +3735,14 @@ Account purpose: ${purpose}
1630
3735
 
1631
3736
  - ${zoneLine}
1632
3737
  - ${accountLine}
1633
- - Every tool's schema is complete. The arguments a call needs are in \`required\`, and the values a field accepts are in its \`enum\` you do not need to read Cloudflare's API documentation to make a call here.
1634
- - Lists paginate with \`page\` and \`perPage\` and return a \`page\` object; request the next page only when \`page.hasMore\` is true. Two exceptions: \`list_r2_buckets\` paginates by cursor pass the returned \`nextCursor\` back as \`cursor\` until it is absent and \`list_worker_scripts\` is unpaginated.
3738
+ - Prefer a named tool: its schema is complete, projected, and enough to call it without provider documentation. For an operation without a named tool, use \`cloudflare_api_get\` for GET, \`cloudflare_api_mutate\` for JSON POST/PUT/PATCH/DELETE, or \`cloudflare_api_upload\` for raw and multipart content. Raw tools take a path below \`/client/v4\`; their argument schemas are complete, but endpoint-specific query, header, and body fields come from Cloudflare's API reference. Use \`headers\` for endpoint-specific controls such as \`cf-r2-jurisdiction\`, ETags, and object metadata; authentication, host, content type, and request framing remain connector-owned.
3739
+ - The raw tools cover the wider control plane without weakening routing: GET is explicitly read-only; every mutation and upload is destructive and must cross the host's approval boundary. The API token remains the hard provider-side permission boundary. Absolute URLs, traversal, and query strings embedded in \`path\` are refused locally.
3740
+ - Useful raw paths include \`/accounts/{accountId}/images/v1\` (Images), \`/accounts/{accountId}/stream\` (Stream), \`/zones/{zoneId}/email/routing/rules\` (Email Routing), \`/accounts/{accountId}/d1/database\` (D1), and \`/accounts/{accountId}/queues\` (Queues). On GET, use \`responseType: "text"\` or \`"base64"\` for non-JSON content. Direct-upload endpoints can issue upload URLs; \`cloudflare_api_upload\` can also send explicit text, base64 bytes, or multipart fields/files.
3741
+ - Lists paginate with \`page\` and \`perPage\` and return a \`page\` object; request the next page only when \`page.hasMore\` is true. \`list_zone_rulesets\`, \`list_r2_buckets\`, \`list_r2_objects\`, and \`list_kv_keys\` instead return \`nextCursor\`; \`list_worker_scripts\` is unpaginated.
1635
3742
  - Results are projected to the fields that identify and describe a resource. Pass \`raw: true\` on a read when you genuinely need a field the projection drops.
1636
3743
  - The API token is operator-managed and scoped by permission, not by role. An \`auth_required\` failure means the token is missing, invalid, or lacks that call's permission — it is never fixed by retrying. Call \`verify_api_token\` to tell a dead token from a missing permission, then report which permission is needed rather than trying other tools.
1637
3744
  - A \`rate_limited\` failure carries the wait window. Cloudflare's limit is 1,200 requests per five minutes per user, counted across the dashboard and every token, so do not fan out speculatively; filter server-side with \`name\`, \`type\`, and \`content\` instead of listing everything and filtering locally.
1638
- - Writes: \`create_dns_record\` is additive; \`update_dns_record\`, \`delete_dns_record\`, and \`purge_cache\` change or discard live state and are annotated destructive. Read the current record with \`list_dns_records\` before changing or deleting one, and prefer a targeted \`purge_cache\` over \`everything\`.
3745
+ - Named creates that only add a resource are write-routed without claiming destruction. Updates, overwrites, deletes, rollbacks, cache purges, \`cloudflare_api_mutate\`, and \`cloudflare_api_upload\` are destructive. Read current state before changing it, and prefer a targeted \`purge_cache\` over \`everything\`.
1639
3746
  ${
1640
3747
  accountInstructions
1641
3748
  ? `\n## Account instructions\n\n${accountInstructions}\n`
@@ -1660,7 +3767,7 @@ export function cloudflare(id: string, options: CloudflareOptions): Connector {
1660
3767
  };
1661
3768
  return api(id, {
1662
3769
  title: options.title ?? "Cloudflare",
1663
- description: `Cloudflare zones, DNS, cache, and platform resources — ${purpose}`,
3770
+ description: `Cloudflare control-plane access for zones, DNS, Workers, KV, R2, Pages, media, email, and other v4 APIs — ${purpose}`,
1664
3771
  credential: options.credential ?? DEFAULT_CREDENTIAL,
1665
3772
  callAdmission: admissionPolicy(maxConcurrency),
1666
3773
  usageGuide: usageGuide(purpose, scope, options.instructions),