@zackbart/connecta 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +21 -16
- package/bin/connecta.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-service.js +7 -2
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config-value-sources.js +6 -3
- package/dist/config.d.ts +26 -8
- package/dist/config.js +38 -16
- package/dist/connector-access.d.ts +2 -1
- package/dist/connectors/guarded-fetch.d.ts +18 -1
- package/dist/connectors/guarded-fetch.js +21 -2
- package/dist/describe-config.d.ts +6 -3
- package/dist/describe-config.js +3 -2
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.js +4 -3
- package/dist/invocation.js +39 -4
- package/dist/meta-tools.js +17 -19
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -0
- package/dist/operator-ui/contract.d.ts +5 -4
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -1
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +12 -0
- package/dist/program-writes.js +4 -49
- package/dist/provider.d.ts +6 -5
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.js +1 -1
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +274 -23
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +76 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1186 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +286 -24
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +1 -1
- package/dist/providers/sheets/index.js +3 -3
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/routes/mcp.js +30 -10
- package/dist/routes/operator.js +14 -7
- package/dist/routes/ui-config.js +2 -2
- package/dist/server.js +3 -0
- package/dist/skills.js +5 -4
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +155 -28
- package/documentation/auth.md +86 -27
- package/documentation/code-mode.md +81 -72
- package/documentation/deploying.md +10 -5
- package/documentation/integrating.md +72 -9
- package/documentation/meta-tools.md +52 -38
- package/documentation/operating.md +11 -8
- package/documentation/operator-tests.md +2 -3
- package/documentation/operator-ui.md +9 -3
- package/documentation/provider-auth-migration.md +308 -0
- package/documentation/provider-migration-0.29.md +18 -6
- package/examples/worker/AGENTS.md +5 -0
- package/examples/worker/README.md +2 -2
- package/examples/worker/src/connecta.config.ts +5 -0
- package/package.json +7 -2
- package/templates/node/AGENTS.md +6 -4
- package/templates/node/README.md +6 -4
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +5 -0
- package/dist/providers/vercel/mcp-ownership.js +0 -212
- package/dist/tool-safety.d.ts +0 -9
|
@@ -1,107 +1,49 @@
|
|
|
1
1
|
import { skill } from "./skill.generated.js";
|
|
2
2
|
/**
|
|
3
|
-
* No official `cloudflare` SDK on purpose.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `{ success, errors, messages, result, result_info }` envelope, so Web APIs
|
|
10
|
-
* alone keep this provider Workers-clean. `test/package-surface.node.test.ts` pins
|
|
11
|
-
* it: no `cloudflare` package in any dependency field, every import relative.
|
|
3
|
+
* No official `cloudflare` SDK on purpose. Cloudflare's v4 API is
|
|
4
|
+
* authenticated `fetch` over a uniform `{ success, errors, result,
|
|
5
|
+
* result_info }` envelope, and the key connector validates every call against
|
|
6
|
+
* a pinned operation index instead of typed wrappers, so Web APIs alone keep
|
|
7
|
+
* this provider Workers-clean. `test/package-surface.node.test.ts` pins it: no
|
|
8
|
+
* `cloudflare` package in any dependency field, every import relative.
|
|
12
9
|
*/
|
|
13
|
-
import { apiConnector
|
|
14
|
-
import { remoteMcp, withCredentialDefaults } from "../../connectors/remote-mcp.js";
|
|
10
|
+
import { apiConnector } from "../../connectors/api-connector.js";
|
|
15
11
|
import { reviewedCatalog } from "../../catalog-drift.js";
|
|
16
|
-
import { guardedFetch, retryAfterMs, } from "../../connectors/guarded-fetch.js";
|
|
17
|
-
import { ConnectorCallError } from "../../errors.js";
|
|
18
12
|
import { keys, optionsOf, variants } from "../../config-schema.js";
|
|
19
|
-
import { CREDENTIAL, PROVIDER_COMMON, REMOTE_MCP_AUTH } from "../../connectors/option-shapes.js";
|
|
20
13
|
import { defineProvider } from "../../provider.js";
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
import { byAuth, hostedOAuth } from "../_shared/rest/dispatch.js";
|
|
15
|
+
import { restTools } from "../_shared/rest/tools.js";
|
|
16
|
+
import { CLOUDFLARE_API_BASE, cloudflareRest } from "./rest.js";
|
|
17
|
+
export { CLOUDFLARE_API_BASE } from "./rest.js";
|
|
23
18
|
/** Cloudflare's official whole-API hosted MCP endpoint. */
|
|
24
19
|
export const CLOUDFLARE_MCP_ENDPOINT = "https://mcp.cloudflare.com/mcp";
|
|
25
|
-
/**
|
|
26
|
-
* The 21 record types Cloudflare accepts. Eight carry a single `content`
|
|
27
|
-
* string; the other thirteen carry a per-type structured `data` object.
|
|
28
|
-
* `list_dns_records` filters on all 21 because reading them costs nothing.
|
|
29
|
-
*/
|
|
30
|
-
export const CLOUDFLARE_DNS_RECORD_TYPES = [
|
|
31
|
-
"A",
|
|
32
|
-
"AAAA",
|
|
33
|
-
"CAA",
|
|
34
|
-
"CERT",
|
|
35
|
-
"CNAME",
|
|
36
|
-
"DNSKEY",
|
|
37
|
-
"DS",
|
|
38
|
-
"HTTPS",
|
|
39
|
-
"LOC",
|
|
40
|
-
"MX",
|
|
41
|
-
"NAPTR",
|
|
42
|
-
"NS",
|
|
43
|
-
"OPENPGPKEY",
|
|
44
|
-
"PTR",
|
|
45
|
-
"SMIMEA",
|
|
46
|
-
"SRV",
|
|
47
|
-
"SSHFP",
|
|
48
|
-
"SVCB",
|
|
49
|
-
"TLSA",
|
|
50
|
-
"TXT",
|
|
51
|
-
"URI",
|
|
52
|
-
];
|
|
53
|
-
/**
|
|
54
|
-
* Content-valued types only, and the only types `create_dns_record` and
|
|
55
|
-
* `update_dns_record` accept. Covering the thirteen structured-`data` types
|
|
56
|
-
* would mean either a free-form `data` passthrough — the untyped body this
|
|
57
|
-
* connection exists to avoid — or thirteen more hand-written schemas for record
|
|
58
|
-
* types that are rare in day-to-day zone administration. They stay fully
|
|
59
|
-
* readable; writing one goes through the approval-gated raw mutation tool with
|
|
60
|
-
* Cloudflare's documented per-type `data` body.
|
|
61
|
-
*/
|
|
62
|
-
export const CLOUDFLARE_CONTENT_DNS_RECORD_TYPES = [
|
|
63
|
-
"A",
|
|
64
|
-
"AAAA",
|
|
65
|
-
"CNAME",
|
|
66
|
-
"MX",
|
|
67
|
-
"NS",
|
|
68
|
-
"OPENPGPKEY",
|
|
69
|
-
"PTR",
|
|
70
|
-
"TXT",
|
|
71
|
-
];
|
|
72
20
|
/**
|
|
73
21
|
* Cloudflare documents a global limit of 1,200 requests per five minutes per
|
|
74
22
|
* user, counted cumulatively across the dashboard, API keys, and API tokens
|
|
75
23
|
* (developers.cloudflare.com/fundamentals/api/reference/limits/). The matching
|
|
76
|
-
* rolling window
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* because one `execute_code` program can fan out faster than the window
|
|
81
|
-
* notices.
|
|
24
|
+
* rolling window is a best-effort per-runtime approximation, not an
|
|
25
|
+
* enforcement: N isolates can each admit 1,200, and dashboard traffic counts
|
|
26
|
+
* for Cloudflare but not here. `maxConcurrency` is Connecta's own choice and
|
|
27
|
+
* the bound that protects a shared credential from one program's fan-out.
|
|
82
28
|
*/
|
|
83
29
|
function admissionPolicy(maxConcurrency) {
|
|
84
30
|
return {
|
|
85
31
|
rules: [
|
|
86
32
|
{
|
|
87
33
|
maxConcurrency,
|
|
88
|
-
budget: {
|
|
89
|
-
kind: "rolling-window",
|
|
90
|
-
maxCalls: 1200,
|
|
91
|
-
windowMs: 300_000,
|
|
92
|
-
},
|
|
34
|
+
budget: { kind: "rolling-window", maxCalls: 1200, windowMs: 300_000 },
|
|
93
35
|
},
|
|
94
36
|
],
|
|
95
37
|
};
|
|
96
38
|
}
|
|
97
39
|
const API_TOKEN_CREDENTIAL = {
|
|
98
40
|
label: "Cloudflare API token",
|
|
99
|
-
description:
|
|
41
|
+
description: "A scoped API token (user or account token), not a Global API Key. Grant only the permissions this connector needs, on only the accounts and zones it serves. It is sent only to api.cloudflare.com, stored encrypted, and never displayed.",
|
|
100
42
|
placeholder: "Paste API token",
|
|
101
43
|
};
|
|
102
44
|
const GLOBAL_API_KEY_CREDENTIAL = {
|
|
103
45
|
label: "Cloudflare Global API Key",
|
|
104
|
-
description: "Legacy user-scoped authentication
|
|
46
|
+
description: "Legacy user-scoped authentication with the same access as its Cloudflare user across every account and zone that user can reach. Prefer a scoped API token.",
|
|
105
47
|
fields: [
|
|
106
48
|
{
|
|
107
49
|
name: "email",
|
|
@@ -119,1959 +61,208 @@ const GLOBAL_API_KEY_CREDENTIAL = {
|
|
|
119
61
|
},
|
|
120
62
|
],
|
|
121
63
|
};
|
|
122
|
-
function credentialConfig(authentication, override) {
|
|
123
|
-
if (authentication === "apiToken") {
|
|
124
|
-
const credential = override ?? API_TOKEN_CREDENTIAL;
|
|
125
|
-
if (credential.fields?.length) {
|
|
126
|
-
throw new Error("cloudflare() API token authentication requires a single-value credential.");
|
|
127
|
-
}
|
|
128
|
-
return credential;
|
|
129
|
-
}
|
|
130
|
-
const credential = override
|
|
131
|
-
? {
|
|
132
|
-
...GLOBAL_API_KEY_CREDENTIAL,
|
|
133
|
-
...override,
|
|
134
|
-
fields: override.fields ?? GLOBAL_API_KEY_CREDENTIAL.fields,
|
|
135
|
-
}
|
|
136
|
-
: GLOBAL_API_KEY_CREDENTIAL;
|
|
137
|
-
const fields = credential.fields?.map((field) => field.name).sort();
|
|
138
|
-
if (fields?.join(",") !== "apiKey,email") {
|
|
139
|
-
throw new Error('cloudflare() Global API Key authentication requires credential fields named "email" and "apiKey".');
|
|
140
|
-
}
|
|
141
|
-
return credential;
|
|
142
|
-
}
|
|
143
|
-
function asRecord(value) {
|
|
144
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
145
|
-
}
|
|
146
|
-
function asArray(value) {
|
|
147
|
-
return Array.isArray(value) ? value : [];
|
|
148
|
-
}
|
|
149
|
-
function compact(value) {
|
|
150
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
151
|
-
}
|
|
152
|
-
/** Flatten Cloudflare's error array (and any nested chain) into one line. */
|
|
153
|
-
function describeErrors(errors) {
|
|
154
|
-
const parts = [];
|
|
155
|
-
const walk = (list) => {
|
|
156
|
-
for (const entry of list) {
|
|
157
|
-
const code = typeof entry.code === "number" ? entry.code : undefined;
|
|
158
|
-
const message = typeof entry.message === "string" ? entry.message : "Unknown error";
|
|
159
|
-
parts.push(code === undefined ? message : `${code}: ${message}`);
|
|
160
|
-
if (Array.isArray(entry.error_chain))
|
|
161
|
-
walk(entry.error_chain);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
walk(errors);
|
|
165
|
-
return parts.length > 0 ? parts.join("; ") : "Cloudflare reported no detail.";
|
|
166
|
-
}
|
|
167
|
-
function errorCodes(errors) {
|
|
168
|
-
const codes = new Set();
|
|
169
|
-
const walk = (list) => {
|
|
170
|
-
for (const entry of list) {
|
|
171
|
-
if (typeof entry.code === "number")
|
|
172
|
-
codes.add(entry.code);
|
|
173
|
-
if (Array.isArray(entry.error_chain))
|
|
174
|
-
walk(entry.error_chain);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
walk(errors);
|
|
178
|
-
return codes;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Credential-shaped Cloudflare error codes that are *not* already implied by a
|
|
182
|
-
* 401 or 403: a missing or malformed `Authorization` header, and the legacy
|
|
183
|
-
* key/email headers. These arrive on HTTP 400, so status alone would misfile
|
|
184
|
-
* them as an argument problem the agent could repair.
|
|
185
|
-
*
|
|
186
|
-
* Deliberately excludes 10000. Cloudflare returns 10000 for "Authentication
|
|
187
|
-
* error" but has also been observed reusing it as a generic validation code
|
|
188
|
-
* ("domain_name is required", "Invalid pagination cursor"), so routing on it
|
|
189
|
-
* would risk telling an agent its token was broken when its arguments were.
|
|
190
|
-
* Genuine 10000 auth failures arrive with 401 or 403 and are caught by status.
|
|
191
|
-
*
|
|
192
|
-
* Provenance worth knowing before editing this set: Cloudflare publishes no
|
|
193
|
-
* official table mapping error codes to causes, so these six — and the 10000
|
|
194
|
-
* observation above — come from community reports and probing rather than
|
|
195
|
-
* documentation. They are well-supported readings Cloudflare could invalidate
|
|
196
|
-
* without notice; prefer `verify_api_token` or `verify_global_api_key` when a
|
|
197
|
-
* diagnosis actually matters.
|
|
198
|
-
*
|
|
199
|
-
* Cloudflare also rate-limits authentication failures separately from the
|
|
200
|
-
* global limit: a few requests with a bad token return 429 with code 10502.
|
|
201
|
-
* That is one more reason to diagnose a broken token once with a verify tool
|
|
202
|
-
* rather than by retrying real calls.
|
|
203
|
-
*/
|
|
204
|
-
const AUTH_ERROR_CODES = new Set([1001, 6003, 6111, 9103, 9106, 9107]);
|
|
205
64
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* Cloudflare returns 403 for both "this token is invalid" and "this token
|
|
210
|
-
* cannot do that" — an agent needs to stop retrying either way, and the
|
|
211
|
-
* operator needs to know the token is the thing to fix.
|
|
212
|
-
*/
|
|
213
|
-
function failureFor(status, headers, errors) {
|
|
214
|
-
const detail = describeErrors(errors);
|
|
215
|
-
const codes = errorCodes(errors);
|
|
216
|
-
// 429 is checked before the authentication codes on purpose: Cloudflare
|
|
217
|
-
// reuses the generic 10000 code on throttled responses, and reading a rate
|
|
218
|
-
// limit as an auth failure would tell an agent to stop when it should wait.
|
|
219
|
-
if (status === 429) {
|
|
220
|
-
const wait = retryAfterMs(headers);
|
|
221
|
-
return new ConnectorCallError("rate_limited", `Cloudflare rate limit reached (HTTP 429). ${detail} The documented limit is 1,200 requests per five minutes per user, counted across the dashboard and every token.`,
|
|
222
|
-
// Cloudflare blocks the remainder of the five-minute window when the
|
|
223
|
-
// global limit trips, so the honest fallback is the whole window.
|
|
224
|
-
{ retryAfterMs: wait ?? 300_000 });
|
|
225
|
-
}
|
|
226
|
-
const authCoded = [...codes].some((code) => AUTH_ERROR_CODES.has(code));
|
|
227
|
-
if (status === 401 || authCoded) {
|
|
228
|
-
return new ConnectorCallError("auth_required", `Cloudflare rejected the configured credential (HTTP ${status}). ${detail} Check that it is valid and has permission to access this resource.`);
|
|
229
|
-
}
|
|
230
|
-
if (status === 403) {
|
|
231
|
-
return new ConnectorCallError("provider_permission_denied", `Cloudflare refused access to this resource (HTTP 403). ${detail} Verify the configured credential first; if valid, ask the account administrator to grant the required resource permission and token scope.`);
|
|
232
|
-
}
|
|
233
|
-
if (status === 400 || status === 409 || status === 422) {
|
|
234
|
-
return new ConnectorCallError("invalid_args", `Cloudflare rejected the request (HTTP ${status}). ${detail}`);
|
|
235
|
-
}
|
|
236
|
-
// Cloudflare refuses a token that may not touch a resource with 401 or 403,
|
|
237
|
-
// so a 404 here is a real absence rather than a permission gap wearing a
|
|
238
|
-
// miss — the unambiguous case `not_found` exists for, and something an agent
|
|
239
|
-
// can act on by re-running discovery for the id.
|
|
240
|
-
if (status === 404) {
|
|
241
|
-
return new ConnectorCallError("not_found", `Cloudflare found no such resource (HTTP 404). ${detail} Confirm the zone or account id with list_zones or list_accounts.`);
|
|
242
|
-
}
|
|
243
|
-
if (status >= 500) {
|
|
244
|
-
return new ConnectorCallError("unavailable", `Cloudflare is unavailable (HTTP ${status}). ${detail}`);
|
|
245
|
-
}
|
|
246
|
-
return new ConnectorCallError("connector_call_failed", `Cloudflare request failed (HTTP ${status}). ${detail}`);
|
|
247
|
-
}
|
|
248
|
-
// --- The request path --------------------------------------------------------
|
|
249
|
-
/**
|
|
250
|
-
* The largest response this connection will read.
|
|
251
|
-
*
|
|
252
|
-
* Generous rather than tight, because `cloudflare_api_get` legitimately
|
|
253
|
-
* downloads Worker scripts and R2 objects. It is a ceiling on absurdity, not a
|
|
254
|
-
* quota: anything approaching it is already far past whatever `maxResultBytes`
|
|
255
|
-
* the deployment set, so the caller was never going to see it whole.
|
|
256
|
-
*/
|
|
257
|
-
const CLOUDFLARE_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
258
|
-
async function readAuthenticationHeaders(ctx, authentication) {
|
|
259
|
-
if (authentication === "globalApiKey") {
|
|
260
|
-
const values = await ctx.credential?.getAll();
|
|
261
|
-
const email = values?.["email"];
|
|
262
|
-
const apiKey = values?.["apiKey"];
|
|
263
|
-
if (!email || !apiKey) {
|
|
264
|
-
throw new ConnectorCallError("auth_required", "No Cloudflare Global API Key and account email are configured for this connector. An operator must add both before any call can run.");
|
|
265
|
-
}
|
|
266
|
-
return { "X-Auth-Email": email, "X-Auth-Key": apiKey };
|
|
267
|
-
}
|
|
268
|
-
const token = await ctx.credential?.get();
|
|
269
|
-
if (!token) {
|
|
270
|
-
throw new ConnectorCallError("auth_required", "No Cloudflare API token is configured for this connector. An operator must add one before any call can run.");
|
|
271
|
-
}
|
|
272
|
-
return { Authorization: `Bearer ${token}` };
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* The one transport every Cloudflare tool goes through.
|
|
276
|
-
*
|
|
277
|
-
* URL confinement, `ctx.signal`, redirect refusal, bounded reads, and the
|
|
278
|
-
* "could not reach the provider" normalization all live in the shared helper.
|
|
279
|
-
* What stays here is what only Cloudflare knows: which headers prove identity,
|
|
280
|
-
* and what a status code means once it arrives.
|
|
281
|
-
*/
|
|
282
|
-
function cloudflareTransport(baseUrl, authentication) {
|
|
283
|
-
return guardedFetch({
|
|
284
|
-
provider: "Cloudflare",
|
|
285
|
-
baseUrl,
|
|
286
|
-
headers: { Accept: "application/json" },
|
|
287
|
-
maxResponseBytes: CLOUDFLARE_MAX_RESPONSE_BYTES,
|
|
288
|
-
authenticate: (ctx) => readAuthenticationHeaders(ctx, authentication),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
async function callCloudflare(send, spec, ctx) {
|
|
292
|
-
return await send(spec, ctx, async (response) => {
|
|
293
|
-
const parsed = await response.jsonResult();
|
|
294
|
-
const envelope = "value" in parsed ? parsed.value : undefined;
|
|
295
|
-
if (envelope === undefined) {
|
|
296
|
-
// A gateway error page or an empty body, not an envelope: the status is
|
|
297
|
-
// the only real signal left.
|
|
298
|
-
// No cause: the parser's error quotes the body it could not read.
|
|
299
|
-
throw response.ok
|
|
300
|
-
? new ConnectorCallError("unavailable", "Cloudflare returned a non-JSON body for a successful status.")
|
|
301
|
-
: failureFor(response.status, response.headers, []);
|
|
302
|
-
}
|
|
303
|
-
const errors = Array.isArray(envelope.errors) ? envelope.errors : [];
|
|
304
|
-
if (!response.ok || envelope.success === false) {
|
|
305
|
-
throw failureFor(response.status, response.headers, errors);
|
|
306
|
-
}
|
|
307
|
-
const isV4Envelope = "success" in envelope || "result" in envelope || "result_info" in envelope || "messages" in envelope;
|
|
308
|
-
return {
|
|
309
|
-
// `/graphql` and a small number of product APIs return ordinary JSON
|
|
310
|
-
// instead of the standard v4 envelope. Preserve that document whole so
|
|
311
|
-
// the raw tools cover them too.
|
|
312
|
-
result: isV4Envelope ? envelope.result : envelope,
|
|
313
|
-
resultInfo: isV4Envelope ? envelope.result_info : undefined,
|
|
314
|
-
};
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
async function testCloudflareCredential(send, spec, ctx, success) {
|
|
318
|
-
try {
|
|
319
|
-
const { result } = await callCloudflare(send, spec, ctx);
|
|
320
|
-
return success(result);
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
return {
|
|
324
|
-
ok: false,
|
|
325
|
-
message: error instanceof Error ? error.message : String(error),
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
function base64FromBytes(bytes) {
|
|
330
|
-
let binary = "";
|
|
331
|
-
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
332
|
-
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
333
|
-
}
|
|
334
|
-
return btoa(binary);
|
|
335
|
-
}
|
|
336
|
-
async function callCloudflareContent(send, spec, ctx, responseType) {
|
|
337
|
-
return await send(spec, ctx, async (response) => {
|
|
338
|
-
if (!response.ok) {
|
|
339
|
-
let envelope;
|
|
340
|
-
try {
|
|
341
|
-
const parsed = await response.jsonResult();
|
|
342
|
-
envelope = "value" in parsed ? parsed.value : undefined;
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
// Content reads classify an already-failed status even when its error
|
|
346
|
-
// body exceeds the transport ceiling.
|
|
347
|
-
}
|
|
348
|
-
const errors = envelope && Array.isArray(envelope.errors) ? envelope.errors : [];
|
|
349
|
-
throw failureFor(response.status, response.headers, errors);
|
|
350
|
-
}
|
|
351
|
-
const common = compact({
|
|
352
|
-
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
353
|
-
etag: response.headers.get("etag") ?? undefined,
|
|
354
|
-
});
|
|
355
|
-
if (responseType === "text") {
|
|
356
|
-
return { ...common, text: await response.text() };
|
|
357
|
-
}
|
|
358
|
-
return { ...common, base64: base64FromBytes(await response.bytes()) };
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Cloudflare's `result_info` reshaped into the one question an agent actually
|
|
363
|
-
* asks — is there another page? — with the raw counters kept alongside it.
|
|
364
|
-
*/
|
|
365
|
-
function pageInfo(info) {
|
|
366
|
-
if (!info)
|
|
367
|
-
return undefined;
|
|
368
|
-
// A cursor-only result_info carries no page counters; inventing them would
|
|
369
|
-
// report `hasMore: false` on a listing that has more.
|
|
370
|
-
if (info.page === undefined && info.total_pages === undefined && info.count === undefined) {
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
const page = typeof info.page === "number" ? info.page : 1;
|
|
374
|
-
const totalPages = typeof info.total_pages === "number" ? info.total_pages : undefined;
|
|
375
|
-
return compact({
|
|
376
|
-
page,
|
|
377
|
-
perPage: typeof info.per_page === "number" ? info.per_page : 0,
|
|
378
|
-
count: typeof info.count === "number" ? info.count : 0,
|
|
379
|
-
totalCount: typeof info.total_count === "number" ? info.total_count : undefined,
|
|
380
|
-
totalPages,
|
|
381
|
-
hasMore: totalPages !== undefined ? page < totalPages : false,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
function pagedList(key, project) {
|
|
385
|
-
return (result, resultInfo, raw) => ({
|
|
386
|
-
[key]: raw ? result : asArray(result).map(project),
|
|
387
|
-
page: pageInfo(resultInfo),
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
function cursorResult(cursor) {
|
|
391
|
-
return typeof cursor === "string" && cursor !== "" ? { nextCursor: cursor } : {};
|
|
392
|
-
}
|
|
393
|
-
function projectAccount(value) {
|
|
394
|
-
const account = asRecord(value);
|
|
395
|
-
return compact({
|
|
396
|
-
id: account["id"],
|
|
397
|
-
name: account["name"],
|
|
398
|
-
type: account["type"],
|
|
399
|
-
createdOn: account["created_on"],
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
function projectZone(value) {
|
|
403
|
-
const zone = asRecord(value);
|
|
404
|
-
const account = asRecord(zone["account"]);
|
|
405
|
-
const plan = asRecord(zone["plan"]);
|
|
406
|
-
const nameServers = Array.isArray(zone["name_servers"]) ? zone["name_servers"] : undefined;
|
|
407
|
-
return compact({
|
|
408
|
-
id: zone["id"],
|
|
409
|
-
name: zone["name"],
|
|
410
|
-
status: zone["status"],
|
|
411
|
-
paused: zone["paused"],
|
|
412
|
-
type: zone["type"],
|
|
413
|
-
accountId: account["id"],
|
|
414
|
-
accountName: account["name"],
|
|
415
|
-
plan: plan["name"],
|
|
416
|
-
nameServers,
|
|
417
|
-
createdOn: zone["created_on"],
|
|
418
|
-
modifiedOn: zone["modified_on"],
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
function projectZoneSetting(value) {
|
|
422
|
-
const setting = asRecord(value);
|
|
423
|
-
return compact({
|
|
424
|
-
id: setting["id"],
|
|
425
|
-
value: setting["value"],
|
|
426
|
-
editable: setting["editable"],
|
|
427
|
-
modifiedOn: setting["modified_on"],
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
function projectDnsRecord(value) {
|
|
431
|
-
const record = asRecord(value);
|
|
432
|
-
const comment = record["comment"] ? record["comment"] : undefined;
|
|
433
|
-
const tags = Array.isArray(record["tags"]) && record["tags"].length > 0 ? record["tags"] : undefined;
|
|
434
|
-
return compact({
|
|
435
|
-
id: record["id"],
|
|
436
|
-
name: record["name"],
|
|
437
|
-
type: record["type"],
|
|
438
|
-
content: record["content"],
|
|
439
|
-
ttl: record["ttl"],
|
|
440
|
-
proxied: record["proxied"],
|
|
441
|
-
priority: record["priority"],
|
|
442
|
-
comment,
|
|
443
|
-
tags,
|
|
444
|
-
createdOn: record["created_on"],
|
|
445
|
-
modifiedOn: record["modified_on"],
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
function projectWorkerScript(value) {
|
|
449
|
-
const script = asRecord(value);
|
|
450
|
-
return compact({
|
|
451
|
-
id: script["id"],
|
|
452
|
-
createdOn: script["created_on"],
|
|
453
|
-
modifiedOn: script["modified_on"],
|
|
454
|
-
usageModel: script["usage_model"],
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
function projectWorkerSettings(value) {
|
|
458
|
-
const settings = asRecord(value);
|
|
459
|
-
return compact({
|
|
460
|
-
compatibilityDate: settings["compatibility_date"],
|
|
461
|
-
compatibilityFlags: settings["compatibility_flags"],
|
|
462
|
-
bindings: settings["bindings"],
|
|
463
|
-
limits: settings["limits"],
|
|
464
|
-
observability: settings["observability"],
|
|
465
|
-
placement: settings["placement"],
|
|
466
|
-
usageModel: settings["usage_model"],
|
|
467
|
-
tailConsumers: settings["tail_consumers"],
|
|
468
|
-
logpush: settings["logpush"],
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
function projectKvNamespace(value) {
|
|
472
|
-
const namespace = asRecord(value);
|
|
473
|
-
return compact({
|
|
474
|
-
id: namespace["id"],
|
|
475
|
-
title: namespace["title"],
|
|
476
|
-
supportsUrlEncoding: namespace["supports_url_encoding"],
|
|
477
|
-
jurisdiction: namespace["jurisdiction"],
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
function projectKvBulkValues(value) {
|
|
481
|
-
const result = asRecord(value);
|
|
482
|
-
return compact({ values: result["values"] });
|
|
483
|
-
}
|
|
484
|
-
function projectR2Bucket(value) {
|
|
485
|
-
const bucket = asRecord(value);
|
|
486
|
-
return compact({
|
|
487
|
-
name: bucket["name"],
|
|
488
|
-
location: bucket["location"],
|
|
489
|
-
storageClass: bucket["storage_class"],
|
|
490
|
-
jurisdiction: bucket["jurisdiction"],
|
|
491
|
-
creationDate: bucket["creation_date"],
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
function projectR2Object(value) {
|
|
495
|
-
const object = asRecord(value);
|
|
496
|
-
return compact({
|
|
497
|
-
key: object["key"],
|
|
498
|
-
size: object["size"],
|
|
499
|
-
etag: object["etag"],
|
|
500
|
-
lastModified: object["last_modified"],
|
|
501
|
-
storageClass: object["storage_class"],
|
|
502
|
-
httpMetadata: object["http_metadata"],
|
|
503
|
-
customMetadata: object["custom_metadata"],
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
function projectR2Cors(value) {
|
|
507
|
-
const cors = asRecord(value);
|
|
508
|
-
return compact({ rules: cors["rules"] });
|
|
509
|
-
}
|
|
510
|
-
function projectKvKey(value) {
|
|
511
|
-
const key = asRecord(value);
|
|
512
|
-
return compact({
|
|
513
|
-
name: key["name"],
|
|
514
|
-
expiration: key["expiration"],
|
|
515
|
-
metadata: key["metadata"],
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
function projectWorkerDeployment(value) {
|
|
519
|
-
const deployment = asRecord(value);
|
|
520
|
-
return compact({
|
|
521
|
-
id: deployment["id"],
|
|
522
|
-
createdOn: deployment["created_on"],
|
|
523
|
-
source: deployment["source"],
|
|
524
|
-
strategy: deployment["strategy"],
|
|
525
|
-
versions: deployment["versions"],
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
function projectPagesDeployment(value) {
|
|
529
|
-
const deployment = asRecord(value);
|
|
530
|
-
return compact({
|
|
531
|
-
id: deployment["id"],
|
|
532
|
-
projectName: deployment["project_name"],
|
|
533
|
-
environment: deployment["environment"],
|
|
534
|
-
url: deployment["url"],
|
|
535
|
-
aliases: deployment["aliases"],
|
|
536
|
-
stage: deployment["stage"],
|
|
537
|
-
latestStage: deployment["latest_stage"],
|
|
538
|
-
createdOn: deployment["created_on"],
|
|
539
|
-
modifiedOn: deployment["modified_on"],
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
function projectPagesDomain(value) {
|
|
543
|
-
const domain = asRecord(value);
|
|
544
|
-
return compact({
|
|
545
|
-
id: domain["id"],
|
|
546
|
-
name: domain["name"],
|
|
547
|
-
status: domain["status"],
|
|
548
|
-
verificationData: domain["verification_data"],
|
|
549
|
-
createdOn: domain["created_on"],
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
function projectRuleset(value) {
|
|
553
|
-
const ruleset = asRecord(value);
|
|
554
|
-
return compact({
|
|
555
|
-
id: ruleset["id"],
|
|
556
|
-
name: ruleset["name"],
|
|
557
|
-
kind: ruleset["kind"],
|
|
558
|
-
phase: ruleset["phase"],
|
|
559
|
-
description: ruleset["description"],
|
|
560
|
-
version: ruleset["version"],
|
|
561
|
-
lastUpdated: ruleset["last_updated"],
|
|
562
|
-
rules: ruleset["rules"],
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
function projectPagesProject(value) {
|
|
566
|
-
const project = asRecord(value);
|
|
567
|
-
const latest = asRecord(project["latest_deployment"]);
|
|
568
|
-
const domains = Array.isArray(project["domains"]) ? project["domains"] : undefined;
|
|
569
|
-
const latestDeployment = latest["id"] === undefined
|
|
570
|
-
? undefined
|
|
571
|
-
: compact({
|
|
572
|
-
id: latest["id"],
|
|
573
|
-
environment: latest["environment"],
|
|
574
|
-
url: latest["url"],
|
|
575
|
-
createdOn: latest["created_on"],
|
|
576
|
-
});
|
|
577
|
-
return compact({
|
|
578
|
-
name: project["name"],
|
|
579
|
-
subdomain: project["subdomain"],
|
|
580
|
-
domains,
|
|
581
|
-
productionBranch: project["production_branch"],
|
|
582
|
-
createdOn: project["created_on"],
|
|
583
|
-
latestDeployment,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
// --- Schema fragments --------------------------------------------------------
|
|
587
|
-
const PAGE_OUTPUT_SCHEMA = {
|
|
588
|
-
type: "object",
|
|
589
|
-
description: "Pagination counters from Cloudflare's result_info. Absent when the endpoint does not paginate.",
|
|
590
|
-
properties: {
|
|
591
|
-
page: { type: "integer" },
|
|
592
|
-
perPage: { type: "integer" },
|
|
593
|
-
count: { type: "integer", description: "Items on this page." },
|
|
594
|
-
totalCount: { type: "integer" },
|
|
595
|
-
totalPages: { type: "integer" },
|
|
596
|
-
hasMore: {
|
|
597
|
-
type: "boolean",
|
|
598
|
-
description: "True when a further page exists; request page + 1.",
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
required: ["page", "perPage", "count", "hasMore"],
|
|
602
|
-
};
|
|
603
|
-
const RAW_INPUT_PROPERTY = {
|
|
604
|
-
type: "boolean",
|
|
605
|
-
description: "Return Cloudflare's unprojected result instead of the lean shape. Use only when a field the projection drops is genuinely needed; the raw shape is much larger.",
|
|
606
|
-
};
|
|
607
|
-
/**
|
|
608
|
-
* Local enforcement of a `perPage` range is only a favor when the bound is
|
|
609
|
-
* really Cloudflare's, so `bounds` decides what the description admits to.
|
|
610
|
-
* `list_accounts`/`list_zones` (5–50) and `list_kv_namespaces` (1–1000) are
|
|
611
|
-
* Cloudflare's documented bounds. `list_dns_records` takes Cloudflare's
|
|
612
|
-
* minimum but is `clamped` at 1,000: Cloudflare's schema documents `per_page`
|
|
613
|
-
* on `/zones/{id}/dns_records` up to 5,000,000, a nominal ceiling no listing
|
|
614
|
-
* will honor, and a local cap an agent is told about beats a page size that
|
|
615
|
-
* fails somewhere inside Cloudflare. `list_pages_projects` is `undocumented` —
|
|
616
|
-
* Cloudflare publishes no bounds and no default for it, so 1–100 is ours.
|
|
65
|
+
* Reviewed in #705's provider audit against https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/.
|
|
66
|
+
* Classifies the hosted OAuth catalog only; the REST connector annotates
|
|
67
|
+
* each tool it authors.
|
|
617
68
|
*/
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
: bounds === "undocumented"
|
|
624
|
-
? " Cloudflare documents no bounds for this endpoint; the range is this connection's own."
|
|
625
|
-
: "";
|
|
626
|
-
return {
|
|
627
|
-
page: {
|
|
628
|
-
type: "integer",
|
|
629
|
-
minimum: 1,
|
|
630
|
-
description: "1-based page number. Defaults to 1.",
|
|
69
|
+
const CLOUDFLARE_MCP_CLASSIFICATION = {
|
|
70
|
+
tools: {
|
|
71
|
+
search: {
|
|
72
|
+
verdict: "read",
|
|
73
|
+
reason: "Searches the Cloudflare OpenAPI contract without executing API methods.",
|
|
631
74
|
},
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
maximum: maxPerPage,
|
|
636
|
-
description: `Items per page, ${minPerPage} to ${maxPerPage}.${defaultNote}${boundsNote}`,
|
|
75
|
+
execute: {
|
|
76
|
+
verdict: "destructive",
|
|
77
|
+
reason: "Can mix HTTP methods across the Cloudflare API; no input schema proves a program only reads.",
|
|
637
78
|
},
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Four endpoints — `list_zone_rulesets`, `list_r2_buckets`, `list_r2_objects`,
|
|
642
|
-
* `list_kv_keys` — page by cursor and get no `page` object. Both the argument
|
|
643
|
-
* and the result say so, so the loop condition is legible from either end of
|
|
644
|
-
* one tool rather than only to a reader who compared all of them.
|
|
645
|
-
*/
|
|
646
|
-
const CURSOR_INPUT_PROPERTY = {
|
|
647
|
-
type: "string",
|
|
648
|
-
description: "Opaque cursor from a previous call's nextCursor. This endpoint pages by cursor, not page number.",
|
|
649
|
-
};
|
|
650
|
-
const NEXT_CURSOR_OUTPUT_PROPERTY = {
|
|
651
|
-
type: "string",
|
|
652
|
-
description: "Pass back as `cursor` to continue. Absent when the listing is complete — this is the only signal; there is no page object.",
|
|
653
|
-
};
|
|
654
|
-
function listOutputSchema(key, item) {
|
|
655
|
-
return {
|
|
656
|
-
type: "object",
|
|
657
|
-
properties: { [key]: { type: "array", items: item }, page: PAGE_OUTPUT_SCHEMA },
|
|
658
|
-
required: [key],
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
const ACCOUNT_SCHEMA = {
|
|
662
|
-
type: "object",
|
|
663
|
-
properties: {
|
|
664
|
-
id: { type: "string" },
|
|
665
|
-
name: { type: "string" },
|
|
666
|
-
type: { type: "string" },
|
|
667
|
-
createdOn: { type: "string" },
|
|
668
|
-
},
|
|
669
|
-
required: ["id", "name"],
|
|
670
|
-
};
|
|
671
|
-
const ZONE_SCHEMA = {
|
|
672
|
-
type: "object",
|
|
673
|
-
properties: {
|
|
674
|
-
id: { type: "string", description: "Zone id — the argument every zone-scoped tool wants." },
|
|
675
|
-
name: { type: "string", description: "Apex domain, e.g. example.com." },
|
|
676
|
-
status: { type: "string" },
|
|
677
|
-
paused: { type: "boolean" },
|
|
678
|
-
type: { type: "string" },
|
|
679
|
-
accountId: { type: "string" },
|
|
680
|
-
accountName: { type: "string" },
|
|
681
|
-
plan: { type: "string" },
|
|
682
|
-
nameServers: { type: "array", items: { type: "string" } },
|
|
683
|
-
createdOn: { type: "string" },
|
|
684
|
-
modifiedOn: { type: "string" },
|
|
685
79
|
},
|
|
686
|
-
required: ["id", "name", "status"],
|
|
687
80
|
};
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
81
|
+
function instructionsSection(instructions) {
|
|
82
|
+
const text = instructions?.trim();
|
|
83
|
+
return text ? `\n## ${skill.instructionsHeading}\n\n${text}\n` : "";
|
|
84
|
+
}
|
|
85
|
+
function oauthUsageGuide(purpose, instructions) {
|
|
86
|
+
const { oauth, shared, oauth_tail } = skill.fragments;
|
|
87
|
+
return `# Cloudflare usage
|
|
88
|
+
|
|
89
|
+
Hosted MCP over OAuth: \`search\` and \`execute\`. Connector purpose: ${purpose}${oauth}${shared}${oauth_tail}${instructionsSection(instructions)}`;
|
|
90
|
+
}
|
|
91
|
+
function idList(ids) {
|
|
92
|
+
return ids?.length ? ids.map((id) => `\`${id}\``).join(", ") : "none";
|
|
93
|
+
}
|
|
94
|
+
function keyUsageGuide(auth, options) {
|
|
95
|
+
const { key, global_warning, shared, key_tail } = skill.fragments;
|
|
96
|
+
const scheme = auth === "apiToken" ? "a scoped API token" : "a Global API Key";
|
|
97
|
+
const pinned = options.pin
|
|
98
|
+
? [
|
|
99
|
+
options.pin.accountIds?.length ? `accounts ${idList(options.pin.accountIds)} and every zone in them` : "",
|
|
100
|
+
options.pin.zoneIds?.length ? `zones ${idList(options.pin.zoneIds)}` : "",
|
|
101
|
+
].filter(Boolean)
|
|
102
|
+
: [];
|
|
103
|
+
const pin = options.pin
|
|
104
|
+
? `\n\nPinned to ${pinned.join(", plus ")}. Calls naming any other account or zone are refused before they are sent, and so is every operation that names neither, except the credential's own user, the filtered account, zone, and membership lists, zone creation in a pinned account, and public data.`
|
|
105
|
+
: auth === "globalApiKey"
|
|
106
|
+
? "\n\nUnpinned: this connector reaches every account and zone the key's user can."
|
|
107
|
+
: "";
|
|
108
|
+
const defaults = [
|
|
109
|
+
options.accountId ? `\`{account_id}\` in a path fills with \`${options.accountId}\`` : "",
|
|
110
|
+
options.zoneId ? `\`{zone_id}\` fills with \`${options.zoneId}\`` : "",
|
|
111
|
+
].filter(Boolean);
|
|
112
|
+
const defaultLine = defaults.length
|
|
113
|
+
? `\n\nDefaults: ${defaults.join("; ")}. Pass another id in the path when the request names a different one.`
|
|
114
|
+
: "";
|
|
115
|
+
return `# Cloudflare usage
|
|
116
|
+
|
|
117
|
+
REST with ${scheme}. Connector purpose: ${options.purpose}${auth === "globalApiKey" ? global_warning.trimEnd() : ""}${pin}${defaultLine}${key}${shared}${key_tail}${instructionsSection(options.instructions)}`;
|
|
118
|
+
}
|
|
119
|
+
const COMMON = keys("title", "authScope", "purpose", "instructions", "maxResultBytes");
|
|
120
|
+
const KEY_COMMON = keys("accountId", "zoneId", "baseUrl", "maxConcurrency");
|
|
121
|
+
const PIN = optionsOf()(keys("accountIds", "zoneIds"));
|
|
122
|
+
/** The closed options cloudflare() accepts, selected by `auth.type`; see `assertKnownOptions`. */
|
|
123
|
+
const CLOUDFLARE_OPTIONS = variants(["auth", "type"], {
|
|
124
|
+
oauth: optionsOf()({
|
|
125
|
+
...COMMON,
|
|
126
|
+
...keys("callAdmission"),
|
|
127
|
+
auth: optionsOf()(keys("type")),
|
|
128
|
+
}).shape,
|
|
129
|
+
apiToken: optionsOf()({
|
|
130
|
+
...COMMON,
|
|
131
|
+
...KEY_COMMON,
|
|
132
|
+
pin: PIN,
|
|
133
|
+
auth: optionsOf()(keys("type")),
|
|
134
|
+
}).shape,
|
|
135
|
+
globalApiKey: optionsOf()({
|
|
136
|
+
...COMMON,
|
|
137
|
+
...KEY_COMMON,
|
|
138
|
+
...keys("unpinned"),
|
|
139
|
+
pin: PIN,
|
|
140
|
+
auth: optionsOf()(keys("type")),
|
|
141
|
+
}).shape,
|
|
142
|
+
});
|
|
143
|
+
function cloudflareOAuth(id, options, provider) {
|
|
144
|
+
return hostedOAuth(id, provider, {
|
|
145
|
+
url: CLOUDFLARE_MCP_ENDPOINT,
|
|
146
|
+
title: options.title ?? "Cloudflare (MCP)",
|
|
147
|
+
description: `Cloudflare's official whole-API MCP interface: ${options.purpose.trim()}`,
|
|
148
|
+
...(options.callAdmission ? { callAdmission: options.callAdmission } : {}),
|
|
149
|
+
usageGuide: {
|
|
150
|
+
content: oauthUsageGuide(options.purpose.trim(), options.instructions),
|
|
151
|
+
summary: "Hosted MCP over OAuth. Search the OpenAPI document; execute programs always classify as writes.",
|
|
152
|
+
required: true,
|
|
730
153
|
},
|
|
731
154
|
});
|
|
732
155
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
description: fallback
|
|
741
|
-
? `${noun} id. Optional — defaults to this connector's configured ${kind}. Pass one to address a different ${noun.toLowerCase()}; ${discovery} lists them.`
|
|
742
|
-
: `${noun} id. Required — this connector declares no default; ${discovery} returns it.`,
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
function scopeRequired(kind, fallback) {
|
|
746
|
-
return fallback ? [] : [kind];
|
|
747
|
-
}
|
|
748
|
-
function optionalString(args, key) {
|
|
749
|
-
const value = args[key];
|
|
750
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
751
|
-
}
|
|
752
|
-
function optionalNumber(args, key) {
|
|
753
|
-
const value = args[key];
|
|
754
|
-
return typeof value === "number" ? value : undefined;
|
|
755
|
-
}
|
|
756
|
-
function requireString(args, key) {
|
|
757
|
-
const value = optionalString(args, key);
|
|
758
|
-
if (value)
|
|
759
|
-
return value;
|
|
760
|
-
throw new ConnectorCallError("invalid_args", `${key} must not be blank.`);
|
|
761
|
-
}
|
|
762
|
-
function cloudflareApiPath(value) {
|
|
763
|
-
if (typeof value !== "string") {
|
|
764
|
-
throw new ConnectorCallError("invalid_args", "path must be a string.");
|
|
765
|
-
}
|
|
766
|
-
const path = value.trim();
|
|
767
|
-
if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\")) {
|
|
768
|
-
throw new ConnectorCallError("invalid_args", "path must be a relative Cloudflare v4 path beginning with one slash and containing no backslashes.");
|
|
769
|
-
}
|
|
770
|
-
if (path.includes("?") || path.includes("#")) {
|
|
771
|
-
throw new ConnectorCallError("invalid_args", "Put query parameters in the query array; path cannot contain '?' or '#'.");
|
|
772
|
-
}
|
|
773
|
-
for (const segment of path.split("/")) {
|
|
774
|
-
let decoded = segment;
|
|
775
|
-
let stable = false;
|
|
776
|
-
for (let pass = 0; pass < 20; pass += 1) {
|
|
777
|
-
let next;
|
|
778
|
-
try {
|
|
779
|
-
next = decodeURIComponent(decoded);
|
|
780
|
-
}
|
|
781
|
-
catch {
|
|
782
|
-
throw new ConnectorCallError("invalid_args", "path contains invalid percent encoding.");
|
|
783
|
-
}
|
|
784
|
-
if (next === decoded) {
|
|
785
|
-
stable = true;
|
|
786
|
-
break;
|
|
787
|
-
}
|
|
788
|
-
decoded = next;
|
|
789
|
-
}
|
|
790
|
-
if (!stable) {
|
|
791
|
-
throw new ConnectorCallError("invalid_args", "path contains too many layers of percent encoding.");
|
|
792
|
-
}
|
|
793
|
-
if (decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\")) {
|
|
794
|
-
throw new ConnectorCallError("invalid_args", "path cannot contain encoded or literal traversal, slash, or backslash segments.");
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
const normalized = new URL(`https://connecta.invalid/client/v4${path}`);
|
|
798
|
-
if (!normalized.pathname.startsWith("/client/v4/")) {
|
|
799
|
-
throw new ConnectorCallError("invalid_args", "path normalization escaped the Cloudflare v4 API base.");
|
|
156
|
+
function idsOf(id, value, path) {
|
|
157
|
+
if (value === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (!Array.isArray(value) ||
|
|
160
|
+
value.length === 0 ||
|
|
161
|
+
!value.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
162
|
+
throw new Error(`cloudflare("${id}") ${path} must be a non-empty list of ids.`);
|
|
800
163
|
}
|
|
801
|
-
return
|
|
164
|
+
return value.map((item) => item.trim());
|
|
802
165
|
}
|
|
803
|
-
function
|
|
804
|
-
if (
|
|
166
|
+
function optionalId(id, value, path) {
|
|
167
|
+
if (value === undefined)
|
|
805
168
|
return undefined;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const entry = asRecord(item);
|
|
809
|
-
query[String(entry["name"])] = String(entry["value"]);
|
|
169
|
+
if (typeof value !== "string" || value.trim() === "" || /[/?#\s]/.test(value.trim())) {
|
|
170
|
+
throw new Error(`cloudflare("${id}") ${path} must be a Cloudflare id.`);
|
|
810
171
|
}
|
|
811
|
-
return
|
|
172
|
+
return value.trim();
|
|
812
173
|
}
|
|
813
|
-
function
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
"authorization",
|
|
819
|
-
"x-auth-email",
|
|
820
|
-
"x-auth-key",
|
|
821
|
-
"cookie",
|
|
822
|
-
"host",
|
|
823
|
-
"content-length",
|
|
824
|
-
"content-type",
|
|
825
|
-
"transfer-encoding",
|
|
826
|
-
]);
|
|
827
|
-
for (const item of value) {
|
|
828
|
-
const entry = asRecord(item);
|
|
829
|
-
const name = String(entry["name"]).trim();
|
|
830
|
-
if (forbidden.has(name.toLowerCase())) {
|
|
831
|
-
throw new ConnectorCallError("invalid_args", `The raw Cloudflare tools do not allow the ${name} header. Authentication and request framing are connector-owned; use contentType for a raw upload body.`);
|
|
174
|
+
function cloudflareKey(auth) {
|
|
175
|
+
return (id, options, provider) => {
|
|
176
|
+
const maxConcurrency = options.maxConcurrency ?? 6;
|
|
177
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
178
|
+
throw new Error(`cloudflare("${id}") maxConcurrency must be a positive integer.`);
|
|
832
179
|
}
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
|
|
180
|
+
let pin;
|
|
181
|
+
if (options.pin !== undefined) {
|
|
182
|
+
const accountIds = idsOf(id, options.pin.accountIds, "pin.accountIds");
|
|
183
|
+
const zoneIds = idsOf(id, options.pin.zoneIds, "pin.zoneIds");
|
|
184
|
+
if (!accountIds && !zoneIds)
|
|
185
|
+
throw new Error(`cloudflare("${id}") pin names accountIds, zoneIds, or both.`);
|
|
186
|
+
pin = {
|
|
187
|
+
...(accountIds ? { accountIds } : {}),
|
|
188
|
+
...(zoneIds ? { zoneIds } : {}),
|
|
189
|
+
};
|
|
836
190
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
841
|
-
function rawSpec(args) {
|
|
842
|
-
return compact({
|
|
843
|
-
path: cloudflareApiPath(args["path"]),
|
|
844
|
-
query: queryFromArgs(args["query"]),
|
|
845
|
-
headers: headersFromArgs(args["headers"]),
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
// The hosted helper cannot send R2 jurisdiction or bucket-edit storage headers.
|
|
849
|
-
// Ordinary headers such as Accept are not a reason to restore JSON duplicates.
|
|
850
|
-
function needsMutationHeaders(method, spec) {
|
|
851
|
-
const headers = Object.entries(spec.headers ?? {});
|
|
852
|
-
if (method === "PATCH" &&
|
|
853
|
-
/^\/accounts\/[^/]+\/r2\/buckets\/[^/]+\/?$/.test(spec.path) &&
|
|
854
|
-
headers.some(([name, value]) => name.toLowerCase() === "cf-r2-storage-class" && (value === "Standard" || value === "InfrequentAccess")))
|
|
855
|
-
return true;
|
|
856
|
-
return (/^\/accounts\/[^/]+\/r2\/buckets(?:\/|$)/.test(spec.path) &&
|
|
857
|
-
headers.some(([name, value]) => name.toLowerCase() === "cf-r2-jurisdiction" && typeof value === "string" && value.trim().length > 0));
|
|
858
|
-
}
|
|
859
|
-
// Reviewed raw-body families, rather than a Content-Type assertion that could
|
|
860
|
-
// send an ordinary JSON DNS/configuration mutation through the upload tool.
|
|
861
|
-
function isUploadEndpoint(method, path) {
|
|
862
|
-
const match = /^\/accounts\/[^/]+(\/.*)$/.exec(path);
|
|
863
|
-
if (!match)
|
|
864
|
-
return false;
|
|
865
|
-
const accountPath = match[1];
|
|
866
|
-
if (method === "PUT") {
|
|
867
|
-
return /^(?:\/workers\/scripts\/[^/]+|\/storage\/kv\/namespaces\/[^/]+\/values\/.+|\/r2\/buckets\/[^/]+\/objects\/.+)\/?$/.test(accountPath);
|
|
868
|
-
}
|
|
869
|
-
if (method === "POST") {
|
|
870
|
-
return /^(?:\/workers\/scripts\/[^/]+\/versions|\/images\/v1|\/stream|\/pages\/assets\/upload|\/pages\/projects\/[^/]+\/deployments)\/?$/.test(accountPath);
|
|
871
|
-
}
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
874
|
-
function r2Headers(args) {
|
|
875
|
-
return { "cf-r2-jurisdiction": optionalString(args, "jurisdiction") };
|
|
876
|
-
}
|
|
877
|
-
function bytesFromBase64(value) {
|
|
878
|
-
try {
|
|
879
|
-
const binary = atob(value);
|
|
880
|
-
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
881
|
-
}
|
|
882
|
-
catch (cause) {
|
|
883
|
-
throw new ConnectorCallError("invalid_args", "base64Body and multipart file base64 values must be valid base64.", {
|
|
884
|
-
cause,
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
function uploadBody(args) {
|
|
889
|
-
const fields = asArray(args["fields"]);
|
|
890
|
-
const files = asArray(args["files"]);
|
|
891
|
-
const hasMultipart = fields.length > 0 || files.length > 0;
|
|
892
|
-
const textBody = typeof args["textBody"] === "string" ? args["textBody"] : undefined;
|
|
893
|
-
const base64Body = typeof args["base64Body"] === "string" ? args["base64Body"] : undefined;
|
|
894
|
-
const rawCount = Number(textBody !== undefined) + Number(base64Body !== undefined);
|
|
895
|
-
if ((hasMultipart && rawCount > 0) || (!hasMultipart && rawCount !== 1)) {
|
|
896
|
-
throw new ConnectorCallError("invalid_args", "cloudflare_api_upload needs exactly one body shape: textBody, base64Body, or multipart fields/files.");
|
|
897
|
-
}
|
|
898
|
-
if (hasMultipart) {
|
|
899
|
-
const form = new FormData();
|
|
900
|
-
for (const value of fields) {
|
|
901
|
-
const field = asRecord(value);
|
|
902
|
-
const name = String(field["name"]);
|
|
903
|
-
const contentType = optionalString(field, "contentType");
|
|
904
|
-
if (contentType) {
|
|
905
|
-
form.append(name, new Blob([String(field["value"])], { type: contentType }), optionalString(field, "fileName") ?? name);
|
|
191
|
+
if (auth === "globalApiKey") {
|
|
192
|
+
const unpinned = options.unpinned;
|
|
193
|
+
if (unpinned !== undefined && unpinned !== true) {
|
|
194
|
+
throw new Error(`cloudflare("${id}") unpinned must be true when set.`);
|
|
906
195
|
}
|
|
907
|
-
|
|
908
|
-
|
|
196
|
+
if (pin && unpinned)
|
|
197
|
+
throw new Error(`cloudflare("${id}") takes pin or unpinned: true, not both.`);
|
|
198
|
+
if (!pin && !unpinned) {
|
|
199
|
+
throw new Error(`cloudflare("${id}") requires pin: { accountIds, zoneIds } with globalApiKey auth, or unpinned: true: a Global API Key reaches every account and zone its user can.`);
|
|
909
200
|
}
|
|
910
201
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (Number(text !== undefined) + Number(base64 !== undefined) !== 1) {
|
|
916
|
-
throw new ConnectorCallError("invalid_args", "Each multipart file needs exactly one of text or base64.");
|
|
917
|
-
}
|
|
918
|
-
const blob = new Blob([text ?? bytesFromBase64(base64)], {
|
|
919
|
-
type: String(file["contentType"] ?? "application/octet-stream"),
|
|
920
|
-
});
|
|
921
|
-
form.append(String(file["name"]), blob, String(file["fileName"]));
|
|
202
|
+
const accountId = optionalId(id, options.accountId, "accountId");
|
|
203
|
+
const zoneId = optionalId(id, options.zoneId, "zoneId");
|
|
204
|
+
if (pin && accountId && pin.accountIds && !pin.accountIds.includes(accountId)) {
|
|
205
|
+
throw new Error(`cloudflare("${id}") accountId ${accountId} is outside its pin.`);
|
|
922
206
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
id: { type: "string" },
|
|
951
|
-
name: { type: "string" },
|
|
952
|
-
kind: { type: "string" },
|
|
953
|
-
phase: { type: "string" },
|
|
954
|
-
description: { type: "string" },
|
|
955
|
-
version: { type: "string" },
|
|
956
|
-
lastUpdated: { type: "string" },
|
|
957
|
-
rules: {
|
|
958
|
-
type: "array",
|
|
959
|
-
items: { type: "object", additionalProperties: true },
|
|
960
|
-
},
|
|
961
|
-
},
|
|
962
|
-
required: ["id", "name"],
|
|
963
|
-
};
|
|
964
|
-
const WORKER_SETTINGS_SCHEMA = {
|
|
965
|
-
type: "object",
|
|
966
|
-
properties: {
|
|
967
|
-
compatibilityDate: { type: "string" },
|
|
968
|
-
compatibilityFlags: { type: "array", items: { type: "string" } },
|
|
969
|
-
bindings: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
970
|
-
limits: { type: "object", additionalProperties: true },
|
|
971
|
-
observability: { type: "object", additionalProperties: true },
|
|
972
|
-
placement: { type: "object", additionalProperties: true },
|
|
973
|
-
usageModel: { type: "string" },
|
|
974
|
-
tailConsumers: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
975
|
-
logpush: { type: "boolean" },
|
|
976
|
-
},
|
|
977
|
-
};
|
|
978
|
-
const WORKER_DEPLOYMENT_SCHEMA = {
|
|
979
|
-
type: "object",
|
|
980
|
-
properties: {
|
|
981
|
-
id: { type: "string" },
|
|
982
|
-
createdOn: { type: "string" },
|
|
983
|
-
source: { type: "string" },
|
|
984
|
-
strategy: { type: "string" },
|
|
985
|
-
versions: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
986
|
-
},
|
|
987
|
-
required: ["id"],
|
|
988
|
-
};
|
|
989
|
-
const KV_NAMESPACE_SCHEMA = {
|
|
990
|
-
type: "object",
|
|
991
|
-
properties: {
|
|
992
|
-
id: { type: "string" },
|
|
993
|
-
title: { type: "string" },
|
|
994
|
-
supportsUrlEncoding: { type: "boolean" },
|
|
995
|
-
jurisdiction: { type: "string", enum: ["eu", "fedramp", "us"] },
|
|
996
|
-
renamed: { type: "boolean" },
|
|
997
|
-
namespaceId: { type: "string" },
|
|
998
|
-
},
|
|
999
|
-
};
|
|
1000
|
-
const KV_BULK_VALUES_SCHEMA = {
|
|
1001
|
-
type: "object",
|
|
1002
|
-
properties: {
|
|
1003
|
-
values: {
|
|
1004
|
-
type: "object",
|
|
1005
|
-
additionalProperties: true,
|
|
1006
|
-
},
|
|
1007
|
-
},
|
|
1008
|
-
required: ["values"],
|
|
1009
|
-
};
|
|
1010
|
-
const R2_CORS_SCHEMA = {
|
|
1011
|
-
type: "object",
|
|
1012
|
-
properties: {
|
|
1013
|
-
rules: {
|
|
1014
|
-
type: "array",
|
|
1015
|
-
items: { type: "object", additionalProperties: true },
|
|
1016
|
-
},
|
|
1017
|
-
},
|
|
1018
|
-
required: ["rules"],
|
|
1019
|
-
};
|
|
1020
|
-
const PAGES_DEPLOYMENT_SCHEMA = {
|
|
1021
|
-
type: "object",
|
|
1022
|
-
properties: {
|
|
1023
|
-
id: { type: "string" },
|
|
1024
|
-
projectName: { type: "string" },
|
|
1025
|
-
environment: { type: "string" },
|
|
1026
|
-
url: { type: "string" },
|
|
1027
|
-
aliases: { type: "array", items: { type: "string" } },
|
|
1028
|
-
stage: { type: "object", additionalProperties: true },
|
|
1029
|
-
latestStage: { type: "object", additionalProperties: true },
|
|
1030
|
-
createdOn: { type: "string" },
|
|
1031
|
-
modifiedOn: { type: "string" },
|
|
1032
|
-
},
|
|
1033
|
-
required: ["id"],
|
|
1034
|
-
};
|
|
1035
|
-
const PAGES_DOMAIN_SCHEMA = {
|
|
1036
|
-
type: "object",
|
|
1037
|
-
properties: {
|
|
1038
|
-
id: { type: "string" },
|
|
1039
|
-
name: { type: "string" },
|
|
1040
|
-
status: { type: "string" },
|
|
1041
|
-
verificationData: { type: "object", additionalProperties: true },
|
|
1042
|
-
createdOn: { type: "string" },
|
|
1043
|
-
},
|
|
1044
|
-
required: ["name"],
|
|
1045
|
-
};
|
|
1046
|
-
const PAGES_PROJECT_SCHEMA = {
|
|
1047
|
-
type: "object",
|
|
1048
|
-
properties: {
|
|
1049
|
-
name: { type: "string" },
|
|
1050
|
-
subdomain: { type: "string" },
|
|
1051
|
-
domains: { type: "array", items: { type: "string" } },
|
|
1052
|
-
productionBranch: { type: "string" },
|
|
1053
|
-
createdOn: { type: "string" },
|
|
1054
|
-
latestDeployment: PAGES_DEPLOYMENT_SCHEMA,
|
|
1055
|
-
},
|
|
1056
|
-
required: ["name"],
|
|
1057
|
-
};
|
|
1058
|
-
const QUERY_INPUT_PROPERTY = {
|
|
1059
|
-
type: "array",
|
|
1060
|
-
description: "Query parameters as name/value pairs; each name may appear once.",
|
|
1061
|
-
items: {
|
|
1062
|
-
type: "object",
|
|
1063
|
-
properties: {
|
|
1064
|
-
name: { type: "string", minLength: 1 },
|
|
1065
|
-
value: { type: ["string", "number", "boolean"] },
|
|
1066
|
-
},
|
|
1067
|
-
required: ["name", "value"],
|
|
1068
|
-
additionalProperties: false,
|
|
1069
|
-
},
|
|
1070
|
-
};
|
|
1071
|
-
// Connector-owned and refused: Authorization, Cookie, Host, Content-Length,
|
|
1072
|
-
// Content-Type, Transfer-Encoding — authentication, host selection, content
|
|
1073
|
-
// type, and request framing are not the caller's to set. That list stays out of
|
|
1074
|
-
// this description on purpose: the compact renderer inlines a shared property
|
|
1075
|
-
// description once per tool, and spelling the six out three times pushed
|
|
1076
|
-
// cloudflare_api_upload's compact input past the 1,024-byte discovery budget.
|
|
1077
|
-
const HEADERS_INPUT_PROPERTY = {
|
|
1078
|
-
type: "array",
|
|
1079
|
-
description: "Endpoint headers as name/value pairs, e.g. cf-r2-jurisdiction, cf-r2-storage-class (Standard or InfrequentAccess), or Range. Connector-owned headers are refused.",
|
|
1080
|
-
items: {
|
|
1081
|
-
type: "object",
|
|
1082
|
-
properties: {
|
|
1083
|
-
name: { type: "string", minLength: 1 },
|
|
1084
|
-
value: { type: "string" },
|
|
1085
|
-
},
|
|
1086
|
-
required: ["name", "value"],
|
|
1087
|
-
additionalProperties: false,
|
|
1088
|
-
},
|
|
1089
|
-
};
|
|
1090
|
-
const R2_JURISDICTION_PROPERTY = {
|
|
1091
|
-
type: "string",
|
|
1092
|
-
enum: ["default", "eu", "us", "fedramp"],
|
|
1093
|
-
description: "Bucket jurisdiction. Omit for ordinary buckets; set eu, us, or fedramp for jurisdictional buckets.",
|
|
1094
|
-
};
|
|
1095
|
-
const R2_BUCKET_NAME_PROPERTY = {
|
|
1096
|
-
type: "string",
|
|
1097
|
-
minLength: 3,
|
|
1098
|
-
maxLength: 64,
|
|
1099
|
-
description: "R2 bucket name.",
|
|
1100
|
-
};
|
|
1101
|
-
const SCRIPT_NAME_PROPERTY = {
|
|
1102
|
-
type: "string",
|
|
1103
|
-
minLength: 1,
|
|
1104
|
-
description: "Worker script name from list_worker_scripts.",
|
|
1105
|
-
};
|
|
1106
|
-
const NAMESPACE_ID_PROPERTY = {
|
|
1107
|
-
type: "string",
|
|
1108
|
-
minLength: 1,
|
|
1109
|
-
description: "KV namespace id from list_kv_namespaces.",
|
|
1110
|
-
};
|
|
1111
|
-
const PROJECT_NAME_PROPERTY = {
|
|
1112
|
-
type: "string",
|
|
1113
|
-
minLength: 1,
|
|
1114
|
-
description: "Pages project name from list_pages_projects.",
|
|
1115
|
-
};
|
|
1116
|
-
const PAGES_PROJECT_NAME_PROPERTY = {
|
|
1117
|
-
type: "string",
|
|
1118
|
-
minLength: 1,
|
|
1119
|
-
description: "Pages project name.",
|
|
1120
|
-
};
|
|
1121
|
-
const DEPLOYMENT_ID_PROPERTY = {
|
|
1122
|
-
type: "string",
|
|
1123
|
-
minLength: 1,
|
|
1124
|
-
description: "Deployment id from list_pages_deployments.",
|
|
1125
|
-
};
|
|
1126
|
-
const WORKER_DEPLOYMENT_ID_PROPERTY = {
|
|
1127
|
-
type: "string",
|
|
1128
|
-
minLength: 1,
|
|
1129
|
-
description: "Deployment id from list_worker_deployments.",
|
|
1130
|
-
};
|
|
1131
|
-
const SETTING_ID_PROPERTY = {
|
|
1132
|
-
type: "string",
|
|
1133
|
-
minLength: 1,
|
|
1134
|
-
description: "Cloudflare zone setting id, such as ssl, brotli, webmcp_enabled, or webmcp_packs.",
|
|
1135
|
-
};
|
|
1136
|
-
const RECORD_ID_PROPERTY = {
|
|
1137
|
-
type: "string",
|
|
1138
|
-
description: "DNS record id, from list_dns_records.",
|
|
1139
|
-
};
|
|
1140
|
-
const R2_BUCKET_SCHEMA = {
|
|
1141
|
-
type: "object",
|
|
1142
|
-
properties: {
|
|
1143
|
-
name: { type: "string" },
|
|
1144
|
-
location: { type: "string" },
|
|
1145
|
-
storageClass: { type: "string" },
|
|
1146
|
-
jurisdiction: { type: "string", enum: ["default", "eu", "us", "fedramp"] },
|
|
1147
|
-
creationDate: { type: "string" },
|
|
1148
|
-
},
|
|
1149
|
-
required: ["name"],
|
|
1150
|
-
};
|
|
1151
|
-
const R2_OBJECT_SCHEMA = {
|
|
1152
|
-
type: "object",
|
|
1153
|
-
properties: {
|
|
1154
|
-
key: { type: "string" },
|
|
1155
|
-
size: { type: "number" },
|
|
1156
|
-
etag: { type: "string" },
|
|
1157
|
-
lastModified: { type: "string" },
|
|
1158
|
-
storageClass: { type: "string" },
|
|
1159
|
-
httpMetadata: { type: "object" },
|
|
1160
|
-
customMetadata: { type: "object" },
|
|
1161
|
-
},
|
|
1162
|
-
required: ["key"],
|
|
1163
|
-
};
|
|
1164
|
-
function cfTool(name, description, annotations, scopeKind, scopeFallback, properties, required, outputSchema, handler) {
|
|
1165
|
-
return {
|
|
1166
|
-
name,
|
|
1167
|
-
description,
|
|
1168
|
-
annotations,
|
|
1169
|
-
inputSchema: {
|
|
1170
|
-
type: "object",
|
|
1171
|
-
properties: scopeKind
|
|
1172
|
-
? {
|
|
1173
|
-
[scopeKind]: scopeProperty(scopeKind, scopeFallback),
|
|
1174
|
-
...properties,
|
|
1175
|
-
}
|
|
1176
|
-
: properties,
|
|
1177
|
-
required: scopeKind ? [...scopeRequired(scopeKind, scopeFallback), ...required] : required,
|
|
1178
|
-
additionalProperties: false,
|
|
1179
|
-
},
|
|
1180
|
-
outputSchema,
|
|
1181
|
-
handler,
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
function getResult(send, request, project = (result) => result) {
|
|
1185
|
-
return async (args, ctx) => {
|
|
1186
|
-
const { result } = await callCloudflare(send, request(args), ctx);
|
|
1187
|
-
return project(result, args);
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
function buildTools(scope, authentication) {
|
|
1191
|
-
const { send } = scope;
|
|
1192
|
-
const zoneArg = (args) => requireScope(args["zoneId"], scope.zoneId, "zoneId");
|
|
1193
|
-
const accountArg = (args) => requireScope(args["accountId"], scope.accountId, "accountId");
|
|
1194
|
-
const readOnly = { readOnlyHint: true, destructiveHint: false };
|
|
1195
|
-
const tools = [
|
|
1196
|
-
authentication === "apiToken"
|
|
1197
|
-
? cfTool("verify_api_token", "Verify the configured Cloudflare API token and report its status. Use this first when any other tool fails with an authentication error, to separate a bad token from a missing permission.", readOnly, undefined, undefined, {}, [], {
|
|
1198
|
-
type: "object",
|
|
1199
|
-
properties: {
|
|
1200
|
-
id: { type: "string" },
|
|
1201
|
-
status: {
|
|
1202
|
-
type: "string",
|
|
1203
|
-
description: '"active" for a usable token.',
|
|
1204
|
-
},
|
|
1205
|
-
notBefore: { type: "string" },
|
|
1206
|
-
expiresOn: { type: "string" },
|
|
1207
|
-
},
|
|
1208
|
-
required: ["status"],
|
|
1209
|
-
}, async (_args, ctx) => {
|
|
1210
|
-
const { result } = await callCloudflare(send, { method: "GET", path: "/user/tokens/verify" }, ctx);
|
|
1211
|
-
const token = asRecord(result);
|
|
1212
|
-
return compact({
|
|
1213
|
-
id: token["id"],
|
|
1214
|
-
status: token["status"],
|
|
1215
|
-
notBefore: token["not_before"],
|
|
1216
|
-
expiresOn: token["expires_on"],
|
|
1217
|
-
});
|
|
1218
|
-
})
|
|
1219
|
-
: cfTool("verify_global_api_key", "Verify the configured Cloudflare Global API Key and account email by retrieving the authenticated user. Use this first when another tool fails with an authentication error.", readOnly, undefined, undefined, {}, [], {
|
|
1220
|
-
type: "object",
|
|
1221
|
-
properties: {
|
|
1222
|
-
id: { type: "string" },
|
|
1223
|
-
email: { type: "string" },
|
|
1224
|
-
status: {
|
|
1225
|
-
type: "string",
|
|
1226
|
-
description: '"active" when Cloudflare accepts the email and key.',
|
|
1227
|
-
},
|
|
1228
|
-
},
|
|
1229
|
-
required: ["email", "status"],
|
|
1230
|
-
}, async (_args, ctx) => {
|
|
1231
|
-
const { result } = await callCloudflare(send, { method: "GET", path: "/user" }, ctx);
|
|
1232
|
-
const user = asRecord(result);
|
|
1233
|
-
return { id: user["id"], email: user["email"], status: "active" };
|
|
1234
|
-
}),
|
|
1235
|
-
cfTool("cloudflare_api_get", "Call any GET endpoint under Cloudflare's v4 API with this connector's credential. Prefer a named tool when one exists; this read-only hatch covers the products the named surface does not reach, such as Images, Stream, D1, and Queues.", readOnly, undefined, undefined, {
|
|
1236
|
-
path: {
|
|
1237
|
-
type: "string",
|
|
1238
|
-
minLength: 1,
|
|
1239
|
-
description: "Relative path below /client/v4, beginning with '/', for example /accounts/<id>/images/v1 or /zones/<id>/email/routing/rules. Do not include a query string.",
|
|
1240
|
-
},
|
|
1241
|
-
query: QUERY_INPUT_PROPERTY,
|
|
1242
|
-
headers: HEADERS_INPUT_PROPERTY,
|
|
1243
|
-
responseType: {
|
|
1244
|
-
type: "string",
|
|
1245
|
-
enum: ["json", "text", "base64"],
|
|
1246
|
-
description: "How to read a successful response. Defaults to json; use text or base64 for object, log, script, and media downloads.",
|
|
1247
|
-
},
|
|
1248
|
-
}, ["path"], {
|
|
1249
|
-
type: "object",
|
|
1250
|
-
properties: {
|
|
1251
|
-
result: {
|
|
1252
|
-
description: "Cloudflare's unprojected result for the endpoint.",
|
|
1253
|
-
},
|
|
1254
|
-
resultInfo: {
|
|
1255
|
-
type: "object",
|
|
1256
|
-
description: "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
|
|
1257
|
-
},
|
|
1258
|
-
text: { type: "string", description: "Text response body when responseType is text." },
|
|
1259
|
-
base64: { type: "string", description: "Base64 response bytes when responseType is base64." },
|
|
1260
|
-
contentType: { type: "string", description: "Response Content-Type for text/base64 reads." },
|
|
1261
|
-
etag: { type: "string", description: "Response ETag when Cloudflare supplies one." },
|
|
1262
|
-
},
|
|
1263
|
-
required: [],
|
|
1264
|
-
}, async (args, ctx) => {
|
|
1265
|
-
const raw = rawSpec(args);
|
|
1266
|
-
const responseType = optionalString(args, "responseType") ?? "json";
|
|
1267
|
-
const spec = compact({
|
|
1268
|
-
method: "GET",
|
|
1269
|
-
...raw,
|
|
1270
|
-
});
|
|
1271
|
-
if (responseType === "text" || responseType === "base64") {
|
|
1272
|
-
return await callCloudflareContent(send, spec, ctx, responseType);
|
|
1273
|
-
}
|
|
1274
|
-
const { result, resultInfo } = await callCloudflare(send, spec, ctx);
|
|
1275
|
-
return compact({
|
|
1276
|
-
result,
|
|
1277
|
-
resultInfo,
|
|
1278
|
-
});
|
|
1279
|
-
}),
|
|
1280
|
-
cfTool("cloudflare_api_mutate", "Call JSON mutations for Global API Key identity or R2 jurisdiction/storage-class header gaps. Ordinary API-token writes use hosted execute. Bucket PATCH accepts cf-r2-storage-class: Standard or InfrequentAccess; no uploads.", { readOnlyHint: false, destructiveHint: true }, undefined, undefined, {
|
|
1281
|
-
method: {
|
|
1282
|
-
type: "string",
|
|
1283
|
-
enum: ["POST", "PUT", "PATCH", "DELETE"],
|
|
1284
|
-
description: "HTTP mutation method required by the Cloudflare endpoint.",
|
|
1285
|
-
},
|
|
1286
|
-
path: {
|
|
1287
|
-
type: "string",
|
|
1288
|
-
minLength: 1,
|
|
1289
|
-
description: "Relative path below /client/v4, beginning with '/'. Do not include a query string.",
|
|
1290
|
-
},
|
|
1291
|
-
query: QUERY_INPUT_PROPERTY,
|
|
1292
|
-
headers: HEADERS_INPUT_PROPERTY,
|
|
1293
|
-
body: {
|
|
1294
|
-
type: ["object", "array", "string", "number", "boolean", "null"],
|
|
1295
|
-
description: "JSON request body exactly as documented by Cloudflare. Omit for endpoints with no body.",
|
|
1296
|
-
},
|
|
1297
|
-
}, ["method", "path"], {
|
|
1298
|
-
type: "object",
|
|
1299
|
-
properties: {
|
|
1300
|
-
result: {
|
|
1301
|
-
description: "Cloudflare's unprojected result for the endpoint.",
|
|
1302
|
-
},
|
|
1303
|
-
resultInfo: {
|
|
1304
|
-
type: "object",
|
|
1305
|
-
description: "Cloudflare's unprojected pagination metadata, when the endpoint returns it.",
|
|
1306
|
-
},
|
|
1307
|
-
},
|
|
1308
|
-
required: ["result"],
|
|
1309
|
-
}, async (args, ctx) => {
|
|
1310
|
-
const method = String(args["method"]);
|
|
1311
|
-
const spec = rawSpec(args);
|
|
1312
|
-
if (authentication === "apiToken" && !needsMutationHeaders(method, spec)) {
|
|
1313
|
-
throw new ConnectorCallError("invalid_args", "Use the Cloudflare MCP execute tool for JSON mutations with an API token. This REST tool is reserved for Global API Key identity, R2 bucket operations requiring cf-r2-jurisdiction, or bucket PATCH with cf-r2-storage-class set to Standard or InfrequentAccess.");
|
|
1314
|
-
}
|
|
1315
|
-
const { result, resultInfo } = await callCloudflare(send, compact({
|
|
1316
|
-
method,
|
|
1317
|
-
...spec,
|
|
1318
|
-
body: args["body"],
|
|
1319
|
-
}), ctx);
|
|
1320
|
-
return compact({
|
|
1321
|
-
result,
|
|
1322
|
-
resultInfo,
|
|
1323
|
-
});
|
|
1324
|
-
}),
|
|
1325
|
-
cfTool("cloudflare_api_upload", "Upload raw text, base64 bytes, or multipart form data to a Cloudflare v4 POST or PUT endpoint. Covers Worker modules, R2/KV objects, Images, Stream, and Pages upload endpoints. Reads no local files; content must be supplied explicitly.", { readOnlyHint: false, destructiveHint: true }, undefined, undefined, {
|
|
1326
|
-
method: {
|
|
1327
|
-
type: "string",
|
|
1328
|
-
enum: ["POST", "PUT"],
|
|
1329
|
-
description: "Upload method the Cloudflare endpoint requires.",
|
|
1330
|
-
},
|
|
1331
|
-
path: {
|
|
1332
|
-
type: "string",
|
|
1333
|
-
minLength: 1,
|
|
1334
|
-
description: "Path below /client/v4, beginning with '/'. No query string.",
|
|
1335
|
-
},
|
|
1336
|
-
query: QUERY_INPUT_PROPERTY,
|
|
1337
|
-
headers: HEADERS_INPUT_PROPERTY,
|
|
1338
|
-
contentType: {
|
|
1339
|
-
type: "string",
|
|
1340
|
-
minLength: 1,
|
|
1341
|
-
description: "Content-Type for a raw text or base64 body. Omit for multipart.",
|
|
1342
|
-
},
|
|
1343
|
-
textBody: {
|
|
1344
|
-
type: "string",
|
|
1345
|
-
description: "Raw UTF-8 body. Exclusive with base64Body and fields/files.",
|
|
1346
|
-
},
|
|
1347
|
-
base64Body: {
|
|
1348
|
-
type: "string",
|
|
1349
|
-
description: "Base64-encoded body bytes. Exclusive with textBody and fields/files.",
|
|
1350
|
-
},
|
|
1351
|
-
fields: {
|
|
1352
|
-
type: "array",
|
|
1353
|
-
description: "String fields of a multipart/form-data request.",
|
|
1354
|
-
items: {
|
|
1355
|
-
type: "object",
|
|
1356
|
-
properties: {
|
|
1357
|
-
name: { type: "string", minLength: 1 },
|
|
1358
|
-
value: { type: "string" },
|
|
1359
|
-
contentType: { type: "string", minLength: 1 },
|
|
1360
|
-
fileName: { type: "string", minLength: 1 },
|
|
1361
|
-
},
|
|
1362
|
-
required: ["name", "value"],
|
|
1363
|
-
additionalProperties: false,
|
|
1364
|
-
},
|
|
1365
|
-
},
|
|
1366
|
-
files: {
|
|
1367
|
-
type: "array",
|
|
1368
|
-
description: "Multipart file parts. Each needs exactly one of text or base64.",
|
|
1369
|
-
items: {
|
|
1370
|
-
type: "object",
|
|
1371
|
-
properties: {
|
|
1372
|
-
name: { type: "string", minLength: 1 },
|
|
1373
|
-
fileName: { type: "string", minLength: 1 },
|
|
1374
|
-
contentType: { type: "string", minLength: 1 },
|
|
1375
|
-
text: { type: "string" },
|
|
1376
|
-
base64: { type: "string" },
|
|
1377
|
-
},
|
|
1378
|
-
required: ["name", "fileName", "contentType"],
|
|
1379
|
-
additionalProperties: false,
|
|
1380
|
-
},
|
|
1381
|
-
},
|
|
1382
|
-
}, ["method", "path"], {
|
|
1383
|
-
type: "object",
|
|
1384
|
-
properties: {
|
|
1385
|
-
result: {
|
|
1386
|
-
description: "Cloudflare's unprojected upload result.",
|
|
1387
|
-
},
|
|
1388
|
-
},
|
|
1389
|
-
required: ["result"],
|
|
1390
|
-
}, async (args, ctx) => {
|
|
1391
|
-
const spec = rawSpec(args);
|
|
1392
|
-
if (authentication === "apiToken" && !isUploadEndpoint(args["method"], spec.path)) {
|
|
1393
|
-
throw new ConnectorCallError("invalid_args", "Use the Cloudflare MCP execute tool for this mutation. REST uploads are confined to Workers, KV values, R2 objects, Images, Stream and Pages upload endpoints.");
|
|
1394
|
-
}
|
|
1395
|
-
const upload = uploadBody(args);
|
|
1396
|
-
const { result } = await callCloudflare(send, compact({
|
|
1397
|
-
method: String(args["method"]),
|
|
1398
|
-
...spec,
|
|
1399
|
-
headers: spec.headers !== undefined || upload.headers !== undefined
|
|
1400
|
-
? { ...spec.headers, ...upload.headers }
|
|
1401
|
-
: undefined,
|
|
1402
|
-
rawBody: upload.rawBody,
|
|
1403
|
-
}), ctx);
|
|
1404
|
-
return { result };
|
|
1405
|
-
}),
|
|
1406
|
-
cfTool("list_accounts", "List Cloudflare accounts this token can see. Supplies the accountId that the Workers, KV, R2, and Pages tools need.", readOnly, undefined, undefined, {
|
|
1407
|
-
name: {
|
|
1408
|
-
type: "string",
|
|
1409
|
-
description: "Filter by exact account name.",
|
|
1410
|
-
},
|
|
1411
|
-
...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
|
|
1412
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1413
|
-
}, [], listOutputSchema("accounts", ACCOUNT_SCHEMA), async (args, ctx) => {
|
|
1414
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1415
|
-
method: "GET",
|
|
1416
|
-
path: "/accounts",
|
|
1417
|
-
query: {
|
|
1418
|
-
name: optionalString(args, "name"),
|
|
1419
|
-
page: optionalNumber(args, "page"),
|
|
1420
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1421
|
-
},
|
|
1422
|
-
}, ctx);
|
|
1423
|
-
return pagedList("accounts", projectAccount)(result, resultInfo, args["raw"] === true);
|
|
1424
|
-
}),
|
|
1425
|
-
cfTool("list_zones", "List zones (domains) this token can see, with their ids and status. This is the zoneId discovery step for every DNS and cache tool.", readOnly, undefined, undefined, {
|
|
1426
|
-
name: {
|
|
1427
|
-
type: "string",
|
|
1428
|
-
description: "Filter by zone name, e.g. example.com.",
|
|
1429
|
-
},
|
|
1430
|
-
accountId: {
|
|
1431
|
-
type: "string",
|
|
1432
|
-
description: "Restrict to one account. Defaults to every account the token can see.",
|
|
1433
|
-
},
|
|
1434
|
-
status: {
|
|
1435
|
-
type: "string",
|
|
1436
|
-
enum: ["initializing", "pending", "active", "moved"],
|
|
1437
|
-
description: "Filter by zone status.",
|
|
1438
|
-
},
|
|
1439
|
-
...pagingInputProperties(5, 50, { defaultPerPage: 20 }),
|
|
1440
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1441
|
-
}, [], listOutputSchema("zones", ZONE_SCHEMA), async (args, ctx) => {
|
|
1442
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1443
|
-
method: "GET",
|
|
1444
|
-
path: "/zones",
|
|
1445
|
-
query: {
|
|
1446
|
-
name: optionalString(args, "name"),
|
|
1447
|
-
// Undefaulted on purpose: list_zones is the discovery step,
|
|
1448
|
-
// and quietly filtering it by a configured accountId would
|
|
1449
|
-
// be a restriction in all but name — one with no argument
|
|
1450
|
-
// that escapes it, since an empty accountId would fall back
|
|
1451
|
-
// to the default again.
|
|
1452
|
-
"account.id": optionalString(args, "accountId"),
|
|
1453
|
-
status: optionalString(args, "status"),
|
|
1454
|
-
page: optionalNumber(args, "page"),
|
|
1455
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1456
|
-
},
|
|
1457
|
-
}, ctx);
|
|
1458
|
-
return pagedList("zones", projectZone)(result, resultInfo, args["raw"] === true);
|
|
1459
|
-
}),
|
|
1460
|
-
cfTool("get_zone", "Fetch one zone's settings summary by id: status, plan, name servers, and owning account.", readOnly, "zoneId", scope.zoneId, {
|
|
1461
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1462
|
-
}, [], ZONE_SCHEMA, getResult(send, (args) => ({ method: "GET", path: `/zones/${encodeURIComponent(zoneArg(args))}` }), (result, args) => (args["raw"] === true ? result : projectZone(result)))),
|
|
1463
|
-
// No bulk `list_zone_settings` on purpose (#361): Cloudflare's published
|
|
1464
|
-
// document marks GET /zones/{zoneId}/settings and its PATCH sibling
|
|
1465
|
-
// deprecated with no bulk replacement, and the tool projected nothing while
|
|
1466
|
-
// *growing* the payload 22.7% by wrapping an unpaginated settings array in a
|
|
1467
|
-
// page object. Read one setting here; an operator who genuinely wants the
|
|
1468
|
-
// whole set names /zones/{zoneId}/settings through cloudflare_api_get, at
|
|
1469
|
-
// the caller's risk rather than promised by connecta's catalog.
|
|
1470
|
-
cfTool("get_zone_setting", "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.", readOnly, "zoneId", scope.zoneId, {
|
|
1471
|
-
settingId: SETTING_ID_PROPERTY,
|
|
1472
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1473
|
-
}, ["settingId"], ZONE_SETTING_SCHEMA, getResult(send, (args) => ({
|
|
1474
|
-
method: "GET",
|
|
1475
|
-
path: `/zones/${encodeURIComponent(zoneArg(args))}/settings/${encodeURIComponent(requireString(args, "settingId"))}`,
|
|
1476
|
-
}), (result, args) => (args["raw"] === true ? result : projectZoneSetting(result)))),
|
|
1477
|
-
cfTool("list_zone_rulesets", "List zone rulesets for WAF, redirects, transforms, cache rules, configuration rules, and other Ruleset Engine phases.", readOnly, "zoneId", scope.zoneId, {
|
|
1478
|
-
perPage: {
|
|
1479
|
-
type: "integer",
|
|
1480
|
-
minimum: 1,
|
|
1481
|
-
maximum: 50,
|
|
1482
|
-
description: "Rulesets per request, 1 to 50.",
|
|
1483
|
-
},
|
|
1484
|
-
cursor: CURSOR_INPUT_PROPERTY,
|
|
1485
|
-
}, [], {
|
|
1486
|
-
type: "object",
|
|
1487
|
-
properties: {
|
|
1488
|
-
rulesets: { type: "array", items: RULESET_SCHEMA },
|
|
1489
|
-
nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
|
|
1490
|
-
},
|
|
1491
|
-
required: ["rulesets"],
|
|
1492
|
-
}, async (args, ctx) => {
|
|
1493
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1494
|
-
method: "GET",
|
|
1495
|
-
path: `/zones/${encodeURIComponent(zoneArg(args))}/rulesets`,
|
|
1496
|
-
query: {
|
|
1497
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1498
|
-
cursor: optionalString(args, "cursor"),
|
|
1499
|
-
},
|
|
1500
|
-
}, ctx);
|
|
1501
|
-
const cursor = resultInfo?.cursors?.after;
|
|
1502
|
-
return {
|
|
1503
|
-
rulesets: asArray(result).map(projectRuleset),
|
|
1504
|
-
...cursorResult(cursor),
|
|
1505
|
-
};
|
|
1506
|
-
}),
|
|
1507
|
-
cfTool("get_zone_ruleset", "Get one zone ruleset including its ordered rules, expressions, actions, parameters, and enabled state.", readOnly, "zoneId", scope.zoneId, {
|
|
1508
|
-
rulesetId: {
|
|
1509
|
-
type: "string",
|
|
1510
|
-
minLength: 1,
|
|
1511
|
-
description: "Ruleset id from list_zone_rulesets.",
|
|
1512
|
-
},
|
|
1513
|
-
}, ["rulesetId"], RULESET_SCHEMA, getResult(send, (args) => ({
|
|
1514
|
-
method: "GET",
|
|
1515
|
-
path: `/zones/${encodeURIComponent(zoneArg(args))}/rulesets/${encodeURIComponent(requireString(args, "rulesetId"))}`,
|
|
1516
|
-
}), projectRuleset)),
|
|
1517
|
-
cfTool("list_dns_records", "List DNS records in a zone, filtered by name, type, or content. Returns record ids, which update_dns_record and delete_dns_record require.", readOnly, "zoneId", scope.zoneId, {
|
|
1518
|
-
name: {
|
|
1519
|
-
type: "string",
|
|
1520
|
-
description: "Exact record name, fully qualified, e.g. www.example.com.",
|
|
1521
|
-
},
|
|
1522
|
-
type: {
|
|
1523
|
-
type: "string",
|
|
1524
|
-
enum: [...CLOUDFLARE_DNS_RECORD_TYPES],
|
|
1525
|
-
description: "Filter by record type.",
|
|
1526
|
-
},
|
|
1527
|
-
content: {
|
|
1528
|
-
type: "string",
|
|
1529
|
-
description: "Exact record content, e.g. an IP address.",
|
|
1530
|
-
},
|
|
1531
|
-
order: {
|
|
1532
|
-
type: "string",
|
|
1533
|
-
enum: ["type", "name", "content", "ttl", "proxied"],
|
|
1534
|
-
description: "Sort field.",
|
|
1535
|
-
},
|
|
1536
|
-
direction: {
|
|
1537
|
-
type: "string",
|
|
1538
|
-
enum: ["asc", "desc"],
|
|
1539
|
-
description: "Sort direction for `order`. Defaults to asc.",
|
|
1540
|
-
},
|
|
1541
|
-
// Cloudflare documents 1 to 5,000,000 here with a default of 100; the
|
|
1542
|
-
// ceiling is nominal, so this connection caps it at a page size that
|
|
1543
|
-
// actually returns.
|
|
1544
|
-
...pagingInputProperties(1, 1000, {
|
|
1545
|
-
defaultPerPage: 100,
|
|
1546
|
-
bounds: "clamped",
|
|
1547
|
-
}),
|
|
1548
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1549
|
-
}, [], listOutputSchema("records", DNS_RECORD_SCHEMA), async (args, ctx) => {
|
|
1550
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1551
|
-
method: "GET",
|
|
1552
|
-
path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records`,
|
|
1553
|
-
query: {
|
|
1554
|
-
name: optionalString(args, "name"),
|
|
1555
|
-
type: optionalString(args, "type"),
|
|
1556
|
-
content: optionalString(args, "content"),
|
|
1557
|
-
order: optionalString(args, "order"),
|
|
1558
|
-
direction: optionalString(args, "direction"),
|
|
1559
|
-
page: optionalNumber(args, "page"),
|
|
1560
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1561
|
-
},
|
|
1562
|
-
}, ctx);
|
|
1563
|
-
return pagedList("records", projectDnsRecord)(result, resultInfo, args["raw"] === true);
|
|
1564
|
-
}),
|
|
1565
|
-
cfTool("get_dns_record", "Fetch one DNS record by its record id.", readOnly, "zoneId", scope.zoneId, {
|
|
1566
|
-
recordId: RECORD_ID_PROPERTY,
|
|
1567
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1568
|
-
}, ["recordId"], DNS_RECORD_SCHEMA, getResult(send, (args) => ({
|
|
1569
|
-
method: "GET",
|
|
1570
|
-
path: `/zones/${encodeURIComponent(zoneArg(args))}/dns_records/${encodeURIComponent(String(args["recordId"]))}`,
|
|
1571
|
-
}), (result, args) => (args["raw"] === true ? result : projectDnsRecord(result)))),
|
|
1572
|
-
cfTool("list_worker_scripts", "List Workers scripts deployed in an account, with their last-modified times.", readOnly, "accountId", scope.accountId, {
|
|
1573
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1574
|
-
}, [], listOutputSchema("scripts", {
|
|
1575
|
-
type: "object",
|
|
1576
|
-
properties: {
|
|
1577
|
-
id: { type: "string", description: "Script name." },
|
|
1578
|
-
createdOn: { type: "string" },
|
|
1579
|
-
modifiedOn: { type: "string" },
|
|
1580
|
-
usageModel: { type: "string" },
|
|
1581
|
-
},
|
|
1582
|
-
required: ["id"],
|
|
1583
|
-
}), async (args, ctx) => {
|
|
1584
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1585
|
-
method: "GET",
|
|
1586
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts`,
|
|
1587
|
-
}, ctx);
|
|
1588
|
-
return pagedList("scripts", projectWorkerScript)(result, resultInfo, args["raw"] === true);
|
|
1589
|
-
}),
|
|
1590
|
-
cfTool("get_worker_settings", "Get a Worker's compatibility date and flags, bindings, limits, observability, placement, usage model, and other script settings.", readOnly, "accountId", scope.accountId, {
|
|
1591
|
-
scriptName: SCRIPT_NAME_PROPERTY,
|
|
1592
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1593
|
-
}, ["scriptName"], WORKER_SETTINGS_SCHEMA, getResult(send, (args) => ({
|
|
1594
|
-
method: "GET",
|
|
1595
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(requireString(args, "scriptName"))}/settings`,
|
|
1596
|
-
}), (result, args) => (args["raw"] === true ? result : projectWorkerSettings(result)))),
|
|
1597
|
-
cfTool("list_worker_deployments", "List deployments of a Worker script, including version traffic allocations and deployment strategy.", readOnly, "accountId", scope.accountId, {
|
|
1598
|
-
scriptName: SCRIPT_NAME_PROPERTY,
|
|
1599
|
-
}, ["scriptName"], listOutputSchema("deployments", WORKER_DEPLOYMENT_SCHEMA), async (args, ctx) => {
|
|
1600
|
-
const { result } = await callCloudflare(send, {
|
|
1601
|
-
method: "GET",
|
|
1602
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(requireString(args, "scriptName"))}/deployments`,
|
|
1603
|
-
}, ctx);
|
|
1604
|
-
const record = asRecord(result);
|
|
1605
|
-
const deployments = Array.isArray(result) ? result : asArray(record["deployments"]);
|
|
1606
|
-
return { deployments: deployments.map(projectWorkerDeployment) };
|
|
1607
|
-
}),
|
|
1608
|
-
cfTool("get_worker_deployment", "Get one Worker deployment and its version traffic allocations.", readOnly, "accountId", scope.accountId, {
|
|
1609
|
-
scriptName: SCRIPT_NAME_PROPERTY,
|
|
1610
|
-
deploymentId: WORKER_DEPLOYMENT_ID_PROPERTY,
|
|
1611
|
-
}, ["scriptName", "deploymentId"], WORKER_DEPLOYMENT_SCHEMA, getResult(send, (args) => ({
|
|
1612
|
-
method: "GET",
|
|
1613
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/workers/scripts/${encodeURIComponent(requireString(args, "scriptName"))}/deployments/${encodeURIComponent(requireString(args, "deploymentId"))}`,
|
|
1614
|
-
}), projectWorkerDeployment)),
|
|
1615
|
-
cfTool("list_kv_namespaces", "List Workers KV namespaces in an account, with the namespace ids bindings refer to.", readOnly, "accountId", scope.accountId, {
|
|
1616
|
-
...pagingInputProperties(1, 1000, { defaultPerPage: 20 }),
|
|
1617
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1618
|
-
}, [], listOutputSchema("namespaces", {
|
|
1619
|
-
type: "object",
|
|
1620
|
-
properties: {
|
|
1621
|
-
id: { type: "string" },
|
|
1622
|
-
title: { type: "string" },
|
|
1623
|
-
supportsUrlEncoding: { type: "boolean" },
|
|
1624
|
-
jurisdiction: { type: "string", enum: ["eu", "fedramp", "us"] },
|
|
1625
|
-
},
|
|
1626
|
-
required: ["id", "title"],
|
|
1627
|
-
}), async (args, ctx) => {
|
|
1628
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1629
|
-
method: "GET",
|
|
1630
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces`,
|
|
1631
|
-
query: {
|
|
1632
|
-
page: optionalNumber(args, "page"),
|
|
1633
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1634
|
-
},
|
|
1635
|
-
}, ctx);
|
|
1636
|
-
return pagedList("namespaces", projectKvNamespace)(result, resultInfo, args["raw"] === true);
|
|
1637
|
-
}),
|
|
1638
|
-
cfTool("get_kv_namespace", "Get one Workers KV namespace by id.", readOnly, "accountId", scope.accountId, {
|
|
1639
|
-
namespaceId: NAMESPACE_ID_PROPERTY,
|
|
1640
|
-
}, ["namespaceId"], KV_NAMESPACE_SCHEMA, getResult(send, (args) => ({
|
|
1641
|
-
method: "GET",
|
|
1642
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}`,
|
|
1643
|
-
}), projectKvNamespace)),
|
|
1644
|
-
cfTool("list_kv_keys", "List keys and metadata in a Workers KV namespace by prefix, using cursor pagination.", readOnly, "accountId", scope.accountId, {
|
|
1645
|
-
namespaceId: NAMESPACE_ID_PROPERTY,
|
|
1646
|
-
prefix: {
|
|
1647
|
-
type: "string",
|
|
1648
|
-
description: "Return only keys beginning with this prefix.",
|
|
1649
|
-
},
|
|
1650
|
-
limit: {
|
|
1651
|
-
type: "integer",
|
|
1652
|
-
minimum: 10,
|
|
1653
|
-
maximum: 1000,
|
|
1654
|
-
description: "Keys per request, 10 to 1000. Defaults to 1000.",
|
|
1655
|
-
},
|
|
1656
|
-
cursor: CURSOR_INPUT_PROPERTY,
|
|
1657
|
-
}, ["namespaceId"], {
|
|
1658
|
-
type: "object",
|
|
1659
|
-
properties: {
|
|
1660
|
-
keys: {
|
|
1661
|
-
type: "array",
|
|
1662
|
-
items: {
|
|
1663
|
-
type: "object",
|
|
1664
|
-
properties: {
|
|
1665
|
-
name: { type: "string" },
|
|
1666
|
-
expiration: { type: "number" },
|
|
1667
|
-
metadata: {},
|
|
1668
|
-
},
|
|
1669
|
-
required: ["name"],
|
|
1670
|
-
},
|
|
1671
|
-
},
|
|
1672
|
-
nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
|
|
1673
|
-
},
|
|
1674
|
-
required: ["keys"],
|
|
1675
|
-
}, async (args, ctx) => {
|
|
1676
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1677
|
-
method: "GET",
|
|
1678
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}/keys`,
|
|
1679
|
-
query: {
|
|
1680
|
-
prefix: optionalString(args, "prefix"),
|
|
1681
|
-
limit: optionalNumber(args, "limit"),
|
|
1682
|
-
cursor: optionalString(args, "cursor"),
|
|
1683
|
-
},
|
|
1684
|
-
}, ctx);
|
|
1685
|
-
const cursor = resultInfo?.cursor;
|
|
1686
|
-
return {
|
|
1687
|
-
keys: asArray(result).map(projectKvKey),
|
|
1688
|
-
...cursorResult(cursor),
|
|
1689
|
-
};
|
|
1690
|
-
}),
|
|
1691
|
-
cfTool("bulk_get_kv_values", "Read up to 100 Workers KV values in one request. This JSON endpoint is suitable for text and JSON values; use the raw API for specialized response types.", readOnly, "accountId", scope.accountId, {
|
|
1692
|
-
namespaceId: NAMESPACE_ID_PROPERTY,
|
|
1693
|
-
keys: {
|
|
1694
|
-
type: "array",
|
|
1695
|
-
minItems: 1,
|
|
1696
|
-
maxItems: 100,
|
|
1697
|
-
items: { type: "string", minLength: 1, maxLength: 512 },
|
|
1698
|
-
description: "Key names to retrieve, up to 100.",
|
|
1699
|
-
},
|
|
1700
|
-
withMetadata: {
|
|
1701
|
-
type: "boolean",
|
|
1702
|
-
description: "Include each key's metadata and expiration when true.",
|
|
1703
|
-
},
|
|
1704
|
-
type: {
|
|
1705
|
-
type: "string",
|
|
1706
|
-
enum: ["text", "json"],
|
|
1707
|
-
description: "Return strings as stored, or parse JSON values before returning them.",
|
|
1708
|
-
},
|
|
1709
|
-
}, ["namespaceId", "keys"], KV_BULK_VALUES_SCHEMA, async (args, ctx) => {
|
|
1710
|
-
const { result } = await callCloudflare(send, {
|
|
1711
|
-
method: "POST",
|
|
1712
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/storage/kv/namespaces/${encodeURIComponent(requireString(args, "namespaceId"))}/bulk/get`,
|
|
1713
|
-
body: compact({
|
|
1714
|
-
keys: args["keys"],
|
|
1715
|
-
withMetadata: args["withMetadata"],
|
|
1716
|
-
type: args["type"],
|
|
207
|
+
if (pin && zoneId && pin.zoneIds && !pin.accountIds && !pin.zoneIds.includes(zoneId)) {
|
|
208
|
+
throw new Error(`cloudflare("${id}") zoneId ${zoneId} is outside its pin.`);
|
|
209
|
+
}
|
|
210
|
+
const purpose = options.purpose.trim();
|
|
211
|
+
const rest = cloudflareRest({
|
|
212
|
+
auth,
|
|
213
|
+
baseUrl: options.baseUrl?.trim() || CLOUDFLARE_API_BASE,
|
|
214
|
+
accountId,
|
|
215
|
+
zoneId,
|
|
216
|
+
pin,
|
|
217
|
+
});
|
|
218
|
+
return apiConnector(id, {
|
|
219
|
+
...provider.connectorOptions,
|
|
220
|
+
title: options.title ?? (auth === "globalApiKey" ? "Cloudflare (Global API Key)" : "Cloudflare"),
|
|
221
|
+
description: `Cloudflare v4 API over REST (${auth === "globalApiKey" ? "Global API Key" : "API token"}${pin ? ", pinned" : ""}) — ${purpose}`,
|
|
222
|
+
credential: auth === "globalApiKey" ? GLOBAL_API_KEY_CREDENTIAL : API_TOKEN_CREDENTIAL,
|
|
223
|
+
...(auth === "globalApiKey"
|
|
224
|
+
? { testCredentials: rest.testCredentials }
|
|
225
|
+
: { testCredential: rest.testCredential }),
|
|
226
|
+
callAdmission: admissionPolicy(maxConcurrency),
|
|
227
|
+
usageGuide: {
|
|
228
|
+
content: keyUsageGuide(auth, {
|
|
229
|
+
purpose,
|
|
230
|
+
...(accountId ? { accountId } : {}),
|
|
231
|
+
...(zoneId ? { zoneId } : {}),
|
|
232
|
+
...(pin ? { pin } : {}),
|
|
233
|
+
...(options.instructions !== undefined ? { instructions: options.instructions } : {}),
|
|
1717
234
|
}),
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
cfTool("list_r2_buckets", "List R2 buckets in an account, with location and storage class.", readOnly, "accountId", scope.accountId, {
|
|
1722
|
-
nameContains: {
|
|
1723
|
-
type: "string",
|
|
1724
|
-
description: "Filter to buckets whose name contains this string.",
|
|
1725
|
-
},
|
|
1726
|
-
perPage: {
|
|
1727
|
-
type: "integer",
|
|
1728
|
-
minimum: 1,
|
|
1729
|
-
maximum: 1000,
|
|
1730
|
-
description: "Buckets per request, 1 to 1000. Defaults to 20.",
|
|
1731
|
-
},
|
|
1732
|
-
cursor: CURSOR_INPUT_PROPERTY,
|
|
1733
|
-
jurisdiction: R2_JURISDICTION_PROPERTY,
|
|
1734
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1735
|
-
}, [], {
|
|
1736
|
-
type: "object",
|
|
1737
|
-
properties: {
|
|
1738
|
-
buckets: {
|
|
1739
|
-
type: "array",
|
|
1740
|
-
items: R2_BUCKET_SCHEMA,
|
|
1741
|
-
},
|
|
1742
|
-
nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
|
|
1743
|
-
},
|
|
1744
|
-
required: ["buckets"],
|
|
1745
|
-
}, async (args, ctx) => {
|
|
1746
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1747
|
-
method: "GET",
|
|
1748
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets`,
|
|
1749
|
-
query: {
|
|
1750
|
-
name_contains: optionalString(args, "nameContains"),
|
|
1751
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1752
|
-
cursor: optionalString(args, "cursor"),
|
|
1753
|
-
},
|
|
1754
|
-
headers: r2Headers(args),
|
|
1755
|
-
}, ctx);
|
|
1756
|
-
// R2 nests its list under `buckets` rather than returning a bare array,
|
|
1757
|
-
// and its result_info carries a cursor instead of page counters.
|
|
1758
|
-
const cursor = resultInfo?.cursor;
|
|
1759
|
-
const next = cursorResult(cursor);
|
|
1760
|
-
if (args["raw"] === true)
|
|
1761
|
-
return { buckets: result, ...next };
|
|
1762
|
-
return {
|
|
1763
|
-
buckets: asArray(asRecord(result)["buckets"]).map(projectR2Bucket),
|
|
1764
|
-
...next,
|
|
1765
|
-
};
|
|
1766
|
-
}),
|
|
1767
|
-
cfTool("get_r2_bucket", "Get one R2 bucket's location, jurisdiction, storage class, and creation time.", readOnly, "accountId", scope.accountId, {
|
|
1768
|
-
bucketName: R2_BUCKET_NAME_PROPERTY,
|
|
1769
|
-
jurisdiction: R2_JURISDICTION_PROPERTY,
|
|
1770
|
-
}, ["bucketName"], R2_BUCKET_SCHEMA, getResult(send, (args) => ({
|
|
1771
|
-
method: "GET",
|
|
1772
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}`,
|
|
1773
|
-
headers: r2Headers(args),
|
|
1774
|
-
}), projectR2Bucket)),
|
|
1775
|
-
cfTool("list_r2_objects", "List object keys and metadata in an R2 bucket by prefix, with delimiter grouping and cursor pagination.", readOnly, "accountId", scope.accountId, {
|
|
1776
|
-
bucketName: R2_BUCKET_NAME_PROPERTY,
|
|
1777
|
-
jurisdiction: R2_JURISDICTION_PROPERTY,
|
|
1778
|
-
prefix: {
|
|
1779
|
-
type: "string",
|
|
1780
|
-
description: "Return only object keys beginning with this prefix.",
|
|
1781
|
-
},
|
|
1782
|
-
delimiter: {
|
|
1783
|
-
type: "string",
|
|
1784
|
-
minLength: 1,
|
|
1785
|
-
maxLength: 1,
|
|
1786
|
-
description: "One character used to group path-like keys, usually '/'.",
|
|
1787
|
-
},
|
|
1788
|
-
startAfter: {
|
|
1789
|
-
type: "string",
|
|
1790
|
-
description: "Begin after this key in lexicographic order.",
|
|
1791
|
-
},
|
|
1792
|
-
perPage: {
|
|
1793
|
-
type: "integer",
|
|
1794
|
-
minimum: 1,
|
|
1795
|
-
maximum: 1000,
|
|
1796
|
-
description: "Objects per request, 1 to 1000.",
|
|
235
|
+
summary: auth === "globalApiKey"
|
|
236
|
+
? "GLOBAL API KEY: the user's whole estate, limited by pin. Search operations, read details, then read or write."
|
|
237
|
+
: "Scoped API token over REST. Search operations, read details, then read or write; uploads and GraphQL are named tools.",
|
|
1797
238
|
},
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
properties: {
|
|
1802
|
-
objects: { type: "array", items: R2_OBJECT_SCHEMA },
|
|
1803
|
-
commonPrefixes: { type: "array", items: { type: "string" } },
|
|
1804
|
-
nextCursor: NEXT_CURSOR_OUTPUT_PROPERTY,
|
|
1805
|
-
truncated: { type: "boolean" },
|
|
1806
|
-
},
|
|
1807
|
-
required: ["objects", "truncated"],
|
|
1808
|
-
}, async (args, ctx) => {
|
|
1809
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1810
|
-
method: "GET",
|
|
1811
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}/objects`,
|
|
1812
|
-
headers: r2Headers(args),
|
|
1813
|
-
query: {
|
|
1814
|
-
prefix: optionalString(args, "prefix"),
|
|
1815
|
-
delimiter: optionalString(args, "delimiter"),
|
|
1816
|
-
start_after: optionalString(args, "startAfter"),
|
|
1817
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1818
|
-
cursor: optionalString(args, "cursor"),
|
|
1819
|
-
},
|
|
1820
|
-
}, ctx);
|
|
1821
|
-
const cursor = resultInfo?.cursor;
|
|
1822
|
-
return {
|
|
1823
|
-
objects: asArray(result).map(projectR2Object),
|
|
1824
|
-
...(Array.isArray(resultInfo?.delimited) ? { commonPrefixes: resultInfo.delimited } : {}),
|
|
1825
|
-
...cursorResult(cursor),
|
|
1826
|
-
truncated: resultInfo?.is_truncated === true,
|
|
1827
|
-
};
|
|
1828
|
-
}),
|
|
1829
|
-
// No `get_r2_metrics`, `set_r2_cors`, or `delete_r2_cors` on purpose (#350).
|
|
1830
|
-
// A named tool is a permanent line item in every deployment's catalog, and
|
|
1831
|
-
// these lost the comparison against the escape hatches: `get_r2_metrics`
|
|
1832
|
-
// took an account id, put it in a path, and returned the response untouched,
|
|
1833
|
-
// which `cloudflare_api_get` at /accounts/{accountId}/r2/metrics already
|
|
1834
|
-
// does. `set_r2_cors` declared its rule list as free-form objects — the
|
|
1835
|
-
// untyped body refused everywhere else — and returned Cloudflare's response
|
|
1836
|
-
// unprojected, so it beat the raw route on nothing. `delete_r2_cors`
|
|
1837
|
-
// validated fine and went anyway, because naming only the delete would mean
|
|
1838
|
-
// one CORS policy is set through the raw route and cleared through a named
|
|
1839
|
-
// tool. Read with `get_r2_cors`; write with `cloudflare_api_mutate` at
|
|
1840
|
-
// PUT/DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors.
|
|
1841
|
-
cfTool("get_r2_cors", "Get the browser CORS rules configured on an R2 bucket.", readOnly, "accountId", scope.accountId, {
|
|
1842
|
-
bucketName: R2_BUCKET_NAME_PROPERTY,
|
|
1843
|
-
jurisdiction: R2_JURISDICTION_PROPERTY,
|
|
1844
|
-
}, ["bucketName"], R2_CORS_SCHEMA, getResult(send, (args) => ({
|
|
1845
|
-
method: "GET",
|
|
1846
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/r2/buckets/${encodeURIComponent(requireString(args, "bucketName"))}/cors`,
|
|
1847
|
-
headers: r2Headers(args),
|
|
1848
|
-
}), projectR2Cors)),
|
|
1849
|
-
cfTool("list_pages_projects", "List Cloudflare Pages projects in an account, with their production branch and latest deployment.", readOnly, "accountId", scope.accountId, {
|
|
1850
|
-
...pagingInputProperties(1, 100, { bounds: "undocumented" }),
|
|
1851
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1852
|
-
}, [], listOutputSchema("projects", PAGES_PROJECT_SCHEMA), async (args, ctx) => {
|
|
1853
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1854
|
-
method: "GET",
|
|
1855
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects`,
|
|
1856
|
-
query: {
|
|
1857
|
-
page: optionalNumber(args, "page"),
|
|
1858
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1859
|
-
},
|
|
1860
|
-
}, ctx);
|
|
1861
|
-
return pagedList("projects", projectPagesProject)(result, resultInfo, args["raw"] === true);
|
|
1862
|
-
}),
|
|
1863
|
-
cfTool("get_pages_project", "Get one Pages project, including build configuration, deployment configuration, domains, and latest deployment.", readOnly, "accountId", scope.accountId, {
|
|
1864
|
-
projectName: PROJECT_NAME_PROPERTY,
|
|
1865
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1866
|
-
}, ["projectName"], PAGES_PROJECT_SCHEMA, getResult(send, (args) => ({
|
|
1867
|
-
method: "GET",
|
|
1868
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}`,
|
|
1869
|
-
}), (result, args) => (args["raw"] === true ? result : projectPagesProject(result)))),
|
|
1870
|
-
cfTool("list_pages_deployments", "List production and preview deployments for a Pages project.", readOnly, "accountId", scope.accountId, {
|
|
1871
|
-
projectName: PROJECT_NAME_PROPERTY,
|
|
1872
|
-
env: {
|
|
1873
|
-
type: "string",
|
|
1874
|
-
enum: ["production", "preview"],
|
|
1875
|
-
description: "Optional deployment environment filter.",
|
|
1876
|
-
},
|
|
1877
|
-
...pagingInputProperties(1, 100, { bounds: "undocumented" }),
|
|
1878
|
-
}, ["projectName"], listOutputSchema("deployments", PAGES_DEPLOYMENT_SCHEMA), async (args, ctx) => {
|
|
1879
|
-
const { result, resultInfo } = await callCloudflare(send, {
|
|
1880
|
-
method: "GET",
|
|
1881
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments`,
|
|
1882
|
-
query: {
|
|
1883
|
-
env: optionalString(args, "env"),
|
|
1884
|
-
page: optionalNumber(args, "page"),
|
|
1885
|
-
per_page: optionalNumber(args, "perPage"),
|
|
1886
|
-
},
|
|
1887
|
-
}, ctx);
|
|
1888
|
-
return {
|
|
1889
|
-
deployments: asArray(result).map(projectPagesDeployment),
|
|
1890
|
-
page: pageInfo(resultInfo),
|
|
1891
|
-
};
|
|
1892
|
-
}),
|
|
1893
|
-
cfTool("get_pages_deployment", "Get one Pages deployment including its environment, URLs, stages, source, and build configuration.", readOnly, "accountId", scope.accountId, {
|
|
1894
|
-
projectName: PROJECT_NAME_PROPERTY,
|
|
1895
|
-
deploymentId: DEPLOYMENT_ID_PROPERTY,
|
|
1896
|
-
raw: RAW_INPUT_PROPERTY,
|
|
1897
|
-
}, ["projectName", "deploymentId"], PAGES_DEPLOYMENT_SCHEMA, getResult(send, (args) => ({
|
|
1898
|
-
method: "GET",
|
|
1899
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/deployments/${encodeURIComponent(requireString(args, "deploymentId"))}`,
|
|
1900
|
-
}), (result, args) => (args["raw"] === true ? result : projectPagesDeployment(result)))),
|
|
1901
|
-
cfTool("list_pages_domains", "List custom domains attached to a Pages project and their validation status.", readOnly, "accountId", scope.accountId, {
|
|
1902
|
-
projectName: PAGES_PROJECT_NAME_PROPERTY,
|
|
1903
|
-
}, ["projectName"], listOutputSchema("domains", PAGES_DOMAIN_SCHEMA), async (args, ctx) => {
|
|
1904
|
-
const { result } = await callCloudflare(send, {
|
|
1905
|
-
method: "GET",
|
|
1906
|
-
path: `/accounts/${encodeURIComponent(accountArg(args))}/pages/projects/${encodeURIComponent(requireString(args, "projectName"))}/domains`,
|
|
1907
|
-
}, ctx);
|
|
1908
|
-
return { domains: asArray(result).map(projectPagesDomain) };
|
|
1909
|
-
}),
|
|
1910
|
-
];
|
|
1911
|
-
return tools;
|
|
1912
|
-
}
|
|
1913
|
-
function apiUsageGuide(purpose, scope, instructions, authentication) {
|
|
1914
|
-
const accountInstructions = instructions?.trim();
|
|
1915
|
-
const zoneLine = scope.zoneId
|
|
1916
|
-
? `This connector defaults to zone \`${scope.zoneId}\`; omit \`zoneId\` unless the request names a different domain.`
|
|
1917
|
-
: "This connector declares no default zone. Start with `list_zones` (filter by `name`) and carry the returned `id` into every zone-scoped call.";
|
|
1918
|
-
const accountLine = scope.accountId
|
|
1919
|
-
? `It defaults to account \`${scope.accountId}\`; omit \`accountId\` unless the request names a different account.`
|
|
1920
|
-
: "It declares no default account. `list_accounts` supplies the `accountId` the Workers, KV, R2, and Pages tools need.";
|
|
1921
|
-
const authenticationLine = authentication === "apiToken"
|
|
1922
|
-
? "The API token is operator-managed and scoped by permission. An `auth_required` failure means the token is missing or invalid. A `provider_permission_denied` failure asks an administrator to grant resource permission and token scope. Call `verify_api_token` first."
|
|
1923
|
-
: "The Global API Key and account email are operator-managed. The key has the same access as its Cloudflare user. An `auth_required` failure means one field is missing or the pair is invalid. A `provider_permission_denied` failure asks an administrator to grant the user access. Call `verify_global_api_key` first.";
|
|
1924
|
-
return `# Cloudflare usage
|
|
1925
|
-
|
|
1926
|
-
Account purpose: ${purpose}
|
|
1927
|
-
|
|
1928
|
-
- ${zoneLine}
|
|
1929
|
-
- ${accountLine}${skill.fragments.guide_0}${authenticationLine}${skill.fragments.guide_1}${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
|
|
239
|
+
tools: [...restTools(rest.vendor), ...rest.tools],
|
|
240
|
+
});
|
|
241
|
+
};
|
|
1930
242
|
}
|
|
1931
243
|
/**
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
* No schema digest is asserted without a captured schema to review.
|
|
244
|
+
* A maintained Cloudflare connection. `auth` selects the implementation:
|
|
245
|
+
* OAuth reaches Cloudflare's hosted MCP server; an API token or a Global API
|
|
246
|
+
* Key reaches Connecta's REST connector to the v4 API.
|
|
1936
247
|
*/
|
|
1937
|
-
const CLOUDFLARE_MCP_CLASSIFICATION = {
|
|
1938
|
-
tools: {
|
|
1939
|
-
"search": {
|
|
1940
|
-
"verdict": "read",
|
|
1941
|
-
"reason": "Searches the Cloudflare OpenAPI contract without executing API methods.",
|
|
1942
|
-
},
|
|
1943
|
-
"execute": {
|
|
1944
|
-
"verdict": "destructive",
|
|
1945
|
-
"reason": "Can mix HTTP methods across the Cloudflare API; no input schema proves a program only reads.",
|
|
1946
|
-
},
|
|
1947
|
-
},
|
|
1948
|
-
};
|
|
1949
|
-
function mcpUsageGuide(purpose, instructions) {
|
|
1950
|
-
const accountInstructions = instructions?.trim();
|
|
1951
|
-
return `# Cloudflare MCP usage
|
|
1952
|
-
|
|
1953
|
-
Official whole-API MCP interface: ${purpose}${skill.fragments.guide_2}${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
|
|
1954
|
-
}
|
|
1955
|
-
function cloudflareMcp(id, purpose, options, provider) {
|
|
1956
|
-
const connector = remoteMcp(id, {
|
|
1957
|
-
url: CLOUDFLARE_MCP_ENDPOINT,
|
|
1958
|
-
...provider.connectorOptions,
|
|
1959
|
-
title: options.title ?? "Cloudflare (MCP)",
|
|
1960
|
-
description: `Cloudflare's official whole-API MCP interface: ${purpose}`,
|
|
1961
|
-
auth: withCredentialDefaults(options.auth ?? { type: "oauth" }, {
|
|
1962
|
-
credential: {
|
|
1963
|
-
label: "Cloudflare API token",
|
|
1964
|
-
description: "A scoped Cloudflare API token. Connecta sends it as a bearer token to mcp.cloudflare.com and stores it encrypted.",
|
|
1965
|
-
placeholder: "Paste Cloudflare API token",
|
|
1966
|
-
},
|
|
1967
|
-
}),
|
|
1968
|
-
requireHttps: true,
|
|
1969
|
-
classify: provider.classify,
|
|
1970
|
-
usageGuide: {
|
|
1971
|
-
content: mcpUsageGuide(purpose, options.instructions),
|
|
1972
|
-
summary: "Official whole-API MCP. Search the OpenAPI document; execute programs always classify as writes.",
|
|
1973
|
-
required: true,
|
|
1974
|
-
},
|
|
1975
|
-
});
|
|
1976
|
-
return connector;
|
|
1977
|
-
}
|
|
1978
|
-
function cloudflareApi(id, purpose, options) {
|
|
1979
|
-
const maxConcurrency = options.maxConcurrency ?? 6;
|
|
1980
|
-
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
1981
|
-
throw new Error("cloudflare() maxConcurrency must be a positive integer.");
|
|
1982
|
-
}
|
|
1983
|
-
const authentication = options.authentication ?? "apiToken";
|
|
1984
|
-
if (authentication !== "apiToken" && authentication !== "globalApiKey") {
|
|
1985
|
-
throw new Error('cloudflare() authentication must be "apiToken" or "globalApiKey".');
|
|
1986
|
-
}
|
|
1987
|
-
const scope = {
|
|
1988
|
-
send: cloudflareTransport(options.baseUrl?.trim() || CLOUDFLARE_API_BASE, authentication),
|
|
1989
|
-
accountId: options.accountId?.trim() || undefined,
|
|
1990
|
-
zoneId: options.zoneId?.trim() || undefined,
|
|
1991
|
-
};
|
|
1992
|
-
return api(id, {
|
|
1993
|
-
...(options.authScope ? { authScope: options.authScope } : {}),
|
|
1994
|
-
title: options.title ?? "Cloudflare",
|
|
1995
|
-
description: `Cloudflare control-plane access for zones, DNS, Workers, KV, R2, Pages, media, email, and other v4 APIs — ${purpose}`,
|
|
1996
|
-
credential: credentialConfig(authentication, options.credential),
|
|
1997
|
-
callAdmission: admissionPolicy(maxConcurrency),
|
|
1998
|
-
usageGuide: {
|
|
1999
|
-
content: apiUsageGuide(purpose, scope, options.instructions, authentication),
|
|
2000
|
-
// Explicit rather than derived: the first content line is the zone
|
|
2001
|
-
// scoping rule, which varies per deployment and reads as an instruction
|
|
2002
|
-
// rather than as the routing fact a browsing agent needs.
|
|
2003
|
-
summary: "Zone and account scoping, named-vs-raw routing, two pagination shapes, and lean-vs-raw results.",
|
|
2004
|
-
// Deliberately not `required`. Every named tool's schema is complete
|
|
2005
|
-
// enough to call it correctly on its own, and the scoping convention the
|
|
2006
|
-
// guide carries is repeated on each `zoneId` and `accountId` property —
|
|
2007
|
-
// so forcing the guide into context before every operation would spend
|
|
2008
|
-
// tokens on a sequence the schemas already express.
|
|
2009
|
-
},
|
|
2010
|
-
...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
|
|
2011
|
-
tools: buildTools(scope, authentication),
|
|
2012
|
-
...(authentication === "apiToken"
|
|
2013
|
-
? {
|
|
2014
|
-
async testCredential(value, ctx) {
|
|
2015
|
-
return await testCloudflareCredential(scope.send, { method: "GET", path: "/user/tokens/verify" }, {
|
|
2016
|
-
...ctx,
|
|
2017
|
-
credential: {
|
|
2018
|
-
get: async () => value,
|
|
2019
|
-
getAll: async () => ({ value }),
|
|
2020
|
-
},
|
|
2021
|
-
}, (result) => {
|
|
2022
|
-
const status = asRecord(result)["status"];
|
|
2023
|
-
return status === "active"
|
|
2024
|
-
? { ok: true, message: "Token verified: active." }
|
|
2025
|
-
: { ok: false, message: `Token status is "${String(status)}".` };
|
|
2026
|
-
});
|
|
2027
|
-
},
|
|
2028
|
-
}
|
|
2029
|
-
: {
|
|
2030
|
-
async testCredentials(values, ctx) {
|
|
2031
|
-
return await testCloudflareCredential(scope.send, { method: "GET", path: "/user" }, {
|
|
2032
|
-
...ctx,
|
|
2033
|
-
credential: {
|
|
2034
|
-
get: async (field) => (field ? (values[field] ?? null) : null),
|
|
2035
|
-
getAll: async () => values,
|
|
2036
|
-
},
|
|
2037
|
-
}, (result) => ({
|
|
2038
|
-
ok: true,
|
|
2039
|
-
message: `Global API Key verified for ${String(asRecord(result)["email"])}.`,
|
|
2040
|
-
}));
|
|
2041
|
-
},
|
|
2042
|
-
}),
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
/** The closed options cloudflare() accepts; see `assertKnownOptions`. */
|
|
2046
|
-
const CLOUDFLARE_OPTIONS = variants("surface", {
|
|
2047
|
-
api: optionsOf()({
|
|
2048
|
-
...keys("title", "authScope", "purpose", "instructions", "maxResultBytes"),
|
|
2049
|
-
...keys("surface", "accountId", "zoneId", "baseUrl", "authentication", "maxConcurrency"),
|
|
2050
|
-
credential: CREDENTIAL,
|
|
2051
|
-
}).shape,
|
|
2052
|
-
mcp: optionsOf()({
|
|
2053
|
-
...PROVIDER_COMMON,
|
|
2054
|
-
...keys("surface"),
|
|
2055
|
-
auth: REMOTE_MCP_AUTH,
|
|
2056
|
-
}).shape,
|
|
2057
|
-
}, "mcp");
|
|
2058
|
-
/** A maintained Cloudflare connection using the selected provider interface. */
|
|
2059
248
|
export const cloudflare = defineProvider({
|
|
2060
249
|
name: "cloudflare",
|
|
2061
250
|
title: "Cloudflare",
|
|
2062
|
-
kind: "
|
|
251
|
+
kind: "dual",
|
|
2063
252
|
readme: "Cloudflare",
|
|
2064
|
-
bundle: {
|
|
253
|
+
bundle: {
|
|
254
|
+
baselineGzip: 334055,
|
|
255
|
+
maxGzip: 394055,
|
|
256
|
+
note: "./providers/cloudflare remeasures at 334,055 B gzip (#801), from 151,186 B on main: auth now selects the implementation, so the entry carries the hosted MCP client and Connecta's REST connector (api(), the schema validator, the shared REST module, about 22 KB gzip as in ./providers/stripe) plus the pinned Cloudflare operation index: 3,336 non-deprecated operations from a 27 MB document. The index was shrunk before the cap moved, from 157 KB to 125 KB gzip as source: request details to depth 2 without descriptions or enums over 50 values, no operation ids (they restate summaries), no plain-string path parameters (the template names them), and a 6,000-character per-operation budget that lowers one sprawling operation's depth. Depth 1 would save about 18 KB more at the cost of nested validation such as DNS record alternatives. Removing 23 hand-written reads offsets part of the growth; the reviewed value-safety table adds about 12 KB. The value-safety consolidation (#801) moved the spec-derived candidate record out of the index and grew the table by 259 verdicts, a net +3,646 B (337,701 B), inside the cap. The cap uses the existing baseline + 60,000 B policy.",
|
|
257
|
+
},
|
|
2065
258
|
skill,
|
|
2066
259
|
options: CLOUDFLARE_OPTIONS,
|
|
2067
260
|
classify: CLOUDFLARE_MCP_CLASSIFICATION,
|
|
2068
|
-
create:
|
|
261
|
+
create: byAuth({
|
|
262
|
+
oauth: cloudflareOAuth,
|
|
263
|
+
apiToken: cloudflareKey("apiToken"),
|
|
264
|
+
globalApiKey: cloudflareKey("globalApiKey"),
|
|
265
|
+
}),
|
|
2069
266
|
});
|
|
2070
|
-
function cloudflareConnector(id, options, provider) {
|
|
2071
|
-
const purpose = options.purpose.trim();
|
|
2072
|
-
return options.surface === "api"
|
|
2073
|
-
? cloudflareApi(id, purpose, options)
|
|
2074
|
-
: cloudflareMcp(id, purpose, options, provider);
|
|
2075
|
-
}
|
|
2076
267
|
/** @deprecated Read `cloudflare.definition.classify` instead. Kept for existing imports. */
|
|
2077
268
|
export const CLOUDFLARE_MCP_VETTED_CATALOG = reviewedCatalog(cloudflare.definition.classify, 'defineProvider("cloudflare")');
|