feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels (#6740)

* feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels

Extends the Cloudflare integration past DNS/zones/cache with the security and
Zero Trust surface:

- Rulesets engine (zone-scoped): list rulesets, get a ruleset, read a phase
  entry point, and create/update/delete rules. WAF managed-rule overrides are
  surfaced through the http_request_firewall_managed entry point, since
  Cloudflare has no dedicated overrides endpoint.
- Rate limiting (zone-scoped) via the current Rulesets-based http_ratelimit
  phase, not the deprecated rate_limits endpoint.
- Cloudflare Access (account-scoped): applications, application policies,
  groups, identity providers, and service tokens.
- R2 buckets, Workers scripts/routes, and cloudflared Tunnels.

Destructive operations (delete application, delete policy, revoke service
token, delete rule, delete bucket) spell out their blast radius, and every
tool branches on the envelope's success flag rather than the HTTP status.

Security events are intentionally omitted: Cloudflare exposes them only
through the GraphQL firewallEventsAdaptive dataset, whose field list is not
documented outside schema introspection.

* fix(cloudflare): correct docs drift and remove any from the tool layer

Validation pass over all 47 Cloudflare tools against developers.cloudflare.com.

- Two tool descriptions still escaped a quote as \'. That reaches the model
  verbatim and truncates the generated MDX cell — the get_zone_settings
  `value` output row was missing from the published docs entirely. Both are
  now template literals, and the row is back.
- list_rulesets ignored pagination. The endpoint pages by cursor via
  result_info.cursors.after (not page/per_page), so a zone with many rulesets
  silently truncated with no way to page. Expose per_page + cursor and return
  the next cursor.
- The managed-ruleset override description claimed action and enabled were
  the overridable properties. They are the ones the Rulesets engine documents
  at every level, but individual managed rulesets add more: an OWASP Core
  Ruleset rule override also takes score_threshold. Corrected in both the
  tool output description and the block's action-parameters wand prompt.
  (sensitivity_level is a DDoS override, not a WAF one — deliberately absent.)
- list_tunnels/get_tunnel dropped the documented `metadata` field.
- list_r2_buckets appended order=name whenever any filter was set. `order`
  only qualifies `direction`, and `name` is its sole documented value.
- Path-interpolated IDs are trimmed, so a pasted ID with trailing whitespace
  no longer 404s.
- Replaced every `any` in the integration with checked types: a shared
  CloudflareEnvelope plus per-resource raw payload interfaces, read through
  readCloudflareResponse. The mappers in utils.ts were the widest hole —
  typing them caught four real output-shape mismatches (identity provider
  read_only, service token enabled, DNS record meta/priority, certificate
  geo_restrictions) that `any` had been hiding.
- BlockMeta only described DNS and zone work. Added templates and skills for
  the WAF, rate limiting, and Zero Trust Access surfaces the block now has.

Confirmed against the docs and left unchanged: rulesets/rate limiting are
zone-scoped and Access/R2/Workers scripts/Tunnels are account-scoped while
Workers routes are zone-scoped; tunnels live under /accounts/{id}/cfd_tunnel;
the ratelimit object is a sibling of action/expression, not nested in
action_parameters; every rate limiting period and mitigation_timeout option
matches the documented set; R2 delete returns an empty result so echoing the
requested bucket name is correct; app-nested Access policy endpoints are
current, not deprecated; and every tool fails on a 200 carrying success:false.

* fix(cloudflare): stop per-operation subblock defaults colliding on a shared id

Subblock initial values are seeded into block state keyed by subblock id —
both stores/workflows/utils.ts and lib/workflows/defaults.ts assign
`subBlocks[subBlock.id]` in a plain forEach — so two controls sharing an id
leave one stored value and the last definition in file order wins. Four ids
were duplicated with differing defaults:

- `type` was defined four times. The Access "Application Type" control is
  last, so every new block seeded `type = 'self_hosted'` and the three DNS
  record controls inherited it — Create DNS Record sent a Zero Trust
  application type as its record type. The subblock added on this branch
  broke a default on tools that shipped long before it.
- `status` was defined three times. The empty tunnel filter is last, so
  List Certificates lost its `all` default.
- `proxied` was defined three times. An empty filter is last, so Create DNS
  Record lost its explicit `false`.
- `action` was defined twice. The rate limiting dropdown is last, so the
  ruleset-rule action input was seeded `block`, quietly making "block live
  traffic" the default for a WAF custom rule the user never configured.

Give the colliding controls their own ids and map them back to the tool
params per operation, ahead of the coercions that read them, so each
operation keeps its own default. The other 17 duplicated ids agree on their
value and are left shared.

Adds tests covering each separated default plus a sweep asserting no id
carries two different seeded values, so a future duplicate goes red.

* fix(cloudflare): generate array include rules and allow bootstrapping a phase ruleset

The Access policy include wand asked for a JSON object while the tool parses
the field with parseJsonArrayParam, so generated rules failed validation.
Switch it to json-array, whose prompt reinforcement omits the object braces.

Rate limiting and WAF custom rules could only be appended to an existing
ruleset, but a zone that has never had a rule in a phase has no entry point
ruleset and returns 404, leaving no way to add the first rule. Add
cloudflare_create_ruleset for the documented POST /zones/{id}/rulesets
bootstrap, seeded with optional initial rules.

* fix(cloudflare): correct verified API defects and stop filters leaking into writes

Independent re-validation of all 48 tools against developers.cloudflare.com
turned up defects that the shipped tools would have hit on their happy path.

Delete DNS record reported every success as a failure. That endpoint is the
one Cloudflare v4 response with no envelope — its documented body is
`{"result":{"id":...}}` with no `success` — so `!data.success` was always
true. Branch on an explicit `=== false` instead.

The two replace-semantics PATCH endpoints could silently destroy live config.
Update rate limit rule defaulted a missing action to `block`, converting an
existing `log` or challenge rule into a hard block on real traffic; update
ruleset rule left action and expression optional and had no `ratelimit` or
`logging` passthrough, so updating a rate limiting rule stopped it rate
limiting. Both now require the fields the replacement needs, and the ruleset
rule carries the two nested objects through.

Access applications were unbuildable for most types: `domain` was required,
but it does not exist on the saas, app_launcher, warp, biso, dash_sso,
infrastructure, mcp, mcp_portal, or proxy_endpoint request variants. The
application type enum was also six values behind. Access group `is_default`
is an array of rule objects, not a boolean.

Purge cache merged every supplied target into one body, but the purge body is
a one-of over the five target kinds; it now names the conflict instead.

The remaining fixes are documentation drift: the priority field is MX and URI
only (an SRV record carries priority inside its content), the certificate
status filter documents only "all", the Worker tag filter takes tag:allowed
pairs, and the managed-rule override list conflated the DDoS-only
sensitivity_level with the WAF rule-level set.

Separately, controls that share a subBlock id share one stored value, and
`shouldSerializeSubBlock` short-circuits on `mode: 'advanced'` before it
evaluates `condition` — so a hidden list filter was reaching a write. A
`list_dns_records` content filter could overwrite a record's content, cache
tags could be written onto a DNS record, and the zone status enum could reach
the tunnel list, whose enum is disjoint. Filters that differ from the value
they collided with now carry their own id, remapped through one table before
any coercion. Sharings that mean the same thing everywhere are unchanged.

Aliases are cleared by explicit assignment rather than destructuring, because
the executor merges the mapper's output over the raw inputs and a merely
omitted key survives as its raw subBlock string. The tests assert on that
merged result, and three mechanical invariants now go red on a new collision:
no id spans a read filter and a written value, no dropdown id carries two
option sets, and no hidden advanced control feeds an operation that cannot
render it. That last one found the name filter reaching three list operations.

* docs(cloudflare): point self_hosted_domains at its replacement

Cloudflare deprecated the field in favour of destinations, which the tools
already surface. The output stays — Cloudflare still returns it — but the
description now says which one to read.

* refactor(cloudflare): drop a dead exception from the empty-type guard

create_dns_record now takes its record type from the recordType control,
whose dropdown has no empty option, so the operation can never reach this
guard with an empty type. Clear it unconditionally.

* fix(cloudflare): point the canvas sentences at the renamed filter controls

The list filters that were split off their write-side twin kept their old ids
in canvasPresentation, so seven clauses referenced a control that is no longer
visible for that operation — check:canvas-sentences catches exactly this, and
a broken clause fails silently on the card rather than throwing.

* fix(cloudflare): stop the rate limiting action defaulting on a replacing update

Making action required on update_rate_limit_rule was only half the fix: the
Action dropdown still seeded block for the update operation too, so an update
that edited only the threshold kept sending block and converted a live log or
challenge rule into a hard block — exactly the harm the required flag was
meant to prevent. The update now has its own control with no seeded value, so
the action is something the caller states rather than inherits.

The certificate status filter also still offered Active and Pending, which
Cloudflare does not document for that endpoint; the only documented value is
all, and omitting it returns active packs.

* fix(cloudflare): stop the Access replacements seeding a type and a decision

Same class as the rate limiting action: both Access updates are full
replacements, and the shared controls seeded self_hosted and allow for the
update operations too. Editing only a policy's include rules would silently
convert a live deny, bypass, or non_identity policy to allow — widening who
gets in — and editing an application would rewrite what it IS.

Each update now has its own required control with no seeded value, so the
type and the decision are stated rather than inherited. Regression tests
cover both, and the canvas sentence follows the renamed decision control.
This commit is contained in:
Waleed
2026-08-15 18:50:05 -07:00
committed by GitHub
parent cabd2e2fc1
commit 8a44621382
59 changed files with 9985 additions and 82 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+144 -4
View File
@@ -1,5 +1,5 @@
{
"updatedAt": "2026-08-16",
"updatedAt": "2026-08-15",
"integrations": [
{
"type": "onepassword",
@@ -3811,8 +3811,8 @@
"type": "cloudflare",
"slug": "cloudflare",
"name": "Cloudflare",
"description": "Manage DNS, domains, certificates, and cache",
"longDescription": "Integrate Cloudflare into the workflow. Manage zones (domains), DNS records, SSL/TLS certificates, zone settings, DNS analytics, and cache purging via the Cloudflare API.",
"description": "Manage DNS, WAF, Zero Trust access, and edge infrastructure",
"longDescription": "Integrate Cloudflare into the workflow. Manage zones (domains), DNS records, SSL/TLS certificates, zone settings, DNS analytics, and cache purging. Configure WAF rulesets, managed rule overrides, and rate limiting rules through the current Rulesets engine. Administer Cloudflare Access (Zero Trust) applications, policies, groups, identity providers, and service tokens, and inspect R2 buckets, Workers scripts and routes, and Cloudflare Tunnels.",
"bgColor": "#F5F6FA",
"iconName": "CloudflareIcon",
"docsUrl": "https://docs.sim.ai/integrations/cloudflare",
@@ -3868,9 +3868,149 @@
{
"name": "Purge Cache",
"description": "Purges cached content for a zone. Can purge everything or specific files/tags/hosts/prefixes."
},
{
"name": "List Rulesets",
"description": "Lists every ruleset defined on a zone across all phases (WAF custom rules, managed rules, rate limiting, transform rules, and more). The list response deliberately omits the rules inside each ruleset — use \"Get Ruleset\" to read them. Requires an API token with Zone WAF Read (or another matching ruleset Read permission)."
},
{
"name": "Get Ruleset",
"description": "Reads a single zone ruleset including every rule it contains, in evaluation order. Requires an API token with Zone WAF Read (or another matching ruleset Read permission)."
},
{
"name": "Get Phase Entry Point Ruleset",
"description": "Reads the entry point ruleset for a phase on a zone, including all of its rules. This is how you find the ruleset ID you need before adding, updating, or deleting a rule — for example http_request_firewall_custom for WAF custom rules, http_request_firewall_managed for managed-ruleset deployments and overrides, or http_ratelimit for rate limiting rules. Requires an API token with Zone WAF Read (or another matching ruleset Read permission)."
},
{
"name": "Create Ruleset",
"description": ""
},
{
"name": "Create Ruleset Rule",
"description": "Adds a rule to a zone ruleset. Use \"Get Phase Entry Point Ruleset\" first to find the ruleset ID for the phase you want (for example http_request_firewall_custom for a WAF custom rule, or http_request_firewall_managed with action \"execute\" to deploy a managed ruleset). The rule is appended to the end of the ruleset unless a position is given. Requires an API token with Zone WAF Edit (or another matching ruleset Write permission)."
},
{
"name": "Update Ruleset Rule",
"description": "Updates a rule in a zone ruleset. Cloudflare replaces the rule definition rather than merging it, so you must send every field you want the rule to keep — any field you omit is reset to its default. Read the current rule with \"Get Ruleset\" first. Requires an API token with Zone WAF Edit (or another matching ruleset Write permission)."
},
{
"name": "Delete Ruleset Rule",
"description": "Permanently deletes a rule from a zone ruleset. This takes effect immediately on live traffic and cannot be undone — deleting a WAF custom rule, a managed-ruleset deployment, or a rate limiting rule removes that protection from the zone. Also use this to delete rate limiting rules, which live in the http_ratelimit phase ruleset. Requires an API token with Zone WAF Edit (or another matching ruleset Write permission)."
},
{
"name": "List Managed Ruleset Overrides",
"description": "Lists the WAF managed rulesets deployed on a zone together with the overrides applied to each one. Cloudflare has no dedicated overrides endpoint — overrides live on the \"execute\" rules of the http_request_firewall_managed phase entry point ruleset, which this reads. Requires an API token with Zone WAF Read."
},
{
"name": "List Rate Limiting Rules",
"description": "Lists the rate limiting rules on a zone by reading the http_ratelimit phase entry point ruleset. This uses the current Rulesets-based rate limiting API; the legacy rate_limits endpoint is no longer available. The returned ruleset ID is what \"Create Rate Limiting Rule\", \"Update Rate Limiting Rule\", and \"Delete Ruleset Rule\" need. Requires an API token with Zone WAF Read."
},
{
"name": "Create Rate Limiting Rule",
"description": "Creates a rate limiting rule in the http_ratelimit phase entry point ruleset of a zone, using the current Rulesets-based rate limiting API (the legacy rate_limits endpoint is no longer available). Run \"List Rate Limiting Rules\" first to get the ruleset ID. Requires an API token with Zone WAF Edit."
},
{
"name": "Update Rate Limiting Rule",
"description": "Updates a rate limiting rule in the http_ratelimit phase entry point ruleset of a zone, using the current Rulesets-based rate limiting API. Cloudflare replaces the rule definition rather than merging it, so send the complete rule — every field you omit is reset. Run \"List Rate Limiting Rules\" first to read the current definition and get the ruleset ID. Requires an API token with Zone WAF Edit."
},
{
"name": "List Access Applications",
"description": "Lists the Cloudflare Access (Zero Trust) applications protecting an account. Requires an API token with Account Access: Apps and Policies Read."
},
{
"name": "Get Access Application",
"description": "Reads a single Cloudflare Access (Zero Trust) application, including its attached policies. Requires an API token with Account Access: Apps and Policies Read."
},
{
"name": "Create Access Application",
"description": "Creates a Cloudflare Access (Zero Trust) application that puts an identity check in front of a hostname. Until at least one policy is attached the application denies everyone, so pair this with \"Create Access Policy\". Requires an API token with Account Access: Apps and Policies Edit."
},
{
"name": "Update Access Application",
"description": "Updates a Cloudflare Access (Zero Trust) application. This replaces the application definition rather than merging it, so send every field the application should keep — anything you omit reverts to its default, which can widen or break access. Read the current configuration with \"Get Access Application\" first. Requires an API token with Account Access: Apps and Policies Edit."
},
{
"name": "Delete Access Application",
"description": "Permanently deletes a Cloudflare Access (Zero Trust) application and every policy attached to it. The hostname it protected is immediately left without an Access identity check, so anyone who can reach it can reach the origin. This cannot be undone. Requires an API token with Account Access: Apps and Policies Edit."
},
{
"name": "List Access Policies",
"description": "Lists the Cloudflare Access (Zero Trust) policies attached to an application, in precedence order. Requires an API token with Account Access: Apps and Policies Read."
},
{
"name": "Create Access Policy",
"description": "Creates a Cloudflare Access (Zero Trust) policy on an application, deciding who may reach it. A policy takes effect on live traffic as soon as it is created — an allow policy with a broad include rule grants access immediately. Requires an API token with Account Access: Apps and Policies Edit."
},
{
"name": "Update Access Policy",
"description": "Updates a Cloudflare Access (Zero Trust) policy on an application. This replaces the policy definition rather than merging it, so send every rule the policy should keep — omitted exclude or require rules are dropped, which can widen who gets in. The change applies to live traffic immediately. Read the current policy with \"List Access Policies\" first. Requires an API token with Account Access: Apps and Policies Edit."
},
{
"name": "Delete Access Policy",
"description": "Permanently deletes a Cloudflare Access (Zero Trust) policy from an application. This changes who can reach the application the moment it runs: removing an allow policy locks out everyone it covered, and removing a deny or require policy drops that restriction. This cannot be undone. Requires an API token with Account Access: Apps and Policies Edit."
},
{
"name": "List Access Groups",
"description": "Lists the reusable Cloudflare Access (Zero Trust) groups in an account. Groups bundle identity rules that policies can reference by ID. Requires an API token with Account Access: Organizations, Identity Providers, and Groups Read."
},
{
"name": "List Access Identity Providers",
"description": "Lists the identity providers configured for Cloudflare Access (Zero Trust) in an account, such as Okta, Entra ID, Google Workspace, or a one-time PIN. Use the returned IDs to restrict an application with allowed_idps. Requires an API token with Account Access: Organizations, Identity Providers, and Groups Read."
},
{
"name": "List Access Service Tokens",
"description": "Lists the Cloudflare Access (Zero Trust) service tokens in an account, which let machines authenticate to Access-protected applications. Client secrets are never returned by this endpoint — only on creation. Requires an API token with Account Access: Service Tokens Read."
},
{
"name": "Create Access Service Token",
"description": "Creates a Cloudflare Access (Zero Trust) service token so a machine can authenticate to Access-protected applications. This is the only response that ever contains the client secret — Cloudflare will not return it again, so capture it in the same run. Requires an API token with Account Access: Service Tokens Edit."
},
{
"name": "Revoke Access Service Token",
"description": "Permanently deletes a Cloudflare Access (Zero Trust) service token, revoking it. Every machine or integration still presenting that client ID and secret is locked out of the Access-protected applications immediately, and the secret cannot be recovered. This cannot be undone. Requires an API token with Account Access: Service Tokens Edit."
},
{
"name": "List R2 Buckets",
"description": "Lists the R2 object storage buckets in an account. Requires an API token with Account Workers R2 Storage Read."
},
{
"name": "Get R2 Bucket",
"description": "Reads the metadata of a single R2 object storage bucket. Requires an API token with Account Workers R2 Storage Read."
},
{
"name": "Create R2 Bucket",
"description": "Creates an R2 object storage bucket in an account. The location hint and jurisdiction are fixed at creation and cannot be changed later. Requires an API token with Account Workers R2 Storage Edit."
},
{
"name": "Delete R2 Bucket",
"description": "Permanently deletes an R2 object storage bucket. Cloudflare only deletes an empty bucket, and the deletion cannot be undone. Requires an API token with Account Workers R2 Storage Edit."
},
{
"name": "List Worker Scripts",
"description": "Lists the Workers scripts deployed in an account. Requires an API token with Account Workers Scripts Read."
},
{
"name": "Get Worker Script Settings",
"description": "Reads the deployment settings of a single Workers script — bindings, compatibility date and flags, limits, observability, placement, and tail consumers. The plain \"get script\" endpoint in the Cloudflare API returns raw JavaScript source rather than JSON, so this settings endpoint is the structured way to inspect one script. Requires an API token with Account Workers Scripts Read."
},
{
"name": "List Worker Routes",
"description": "Lists the Workers routes on a zone, showing which URL patterns are handled by which Worker script. Unlike the Workers script endpoints, routes are zone-scoped. Requires an API token with Zone Workers Routes Read."
},
{
"name": "List Tunnels",
"description": "Lists the Cloudflare Tunnels (cloudflared) in an account, with their health status and active connections. Requires an API token with Account Cloudflare Tunnel Read."
},
{
"name": "Get Tunnel",
"description": "Reads a single Cloudflare Tunnel (cloudflared), including its health status and active connector connections. Requires an API token with Account Cloudflare Tunnel Read."
},
{
"name": "Get Tunnel Configuration",
"description": "Reads the configuration of a remotely-managed Cloudflare Tunnel — its ingress rules, origin request settings, and WARP routing. Only tunnels whose configuration source is \"cloudflare\" have a remote configuration; locally-managed tunnels keep it in their own config file. Requires an API token with Account Cloudflare Tunnel Read."
}
],
"operationCount": 13,
"operationCount": 48,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
@@ -0,0 +1,332 @@
/**
* @vitest-environment node
*
* Guards the per-operation subBlock defaults in the Cloudflare block.
*
* SubBlock initial values are seeded into block state keyed by subBlock id
* (`stores/workflows/utils.ts` and `lib/workflows/defaults.ts` both assign
* `subBlocks[subBlock.id] = ...` in a plain forEach), so two controls sharing an
* id leave a single stored value and the LAST definition in file order wins.
* These tests assert the seeded default reaching each tool for operations whose
* control is deliberately not last, so re-introducing a collision goes red.
*/
import { describe, expect, it } from 'vitest'
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
import * as cloudflareTools from '@/tools/cloudflare'
const apiKey = 'cf-token'
const mapParams = CloudflareBlock.tools.config?.params
/**
* Reproduces what the stores seed into block state: every subBlock default,
* keyed by id, with later definitions overwriting earlier ones.
*/
function seededDefaults(): Record<string, unknown> {
const seeded: Record<string, unknown> = {}
for (const subBlock of CloudflareBlock.subBlocks) {
if (typeof subBlock.value === 'function') {
seeded[subBlock.id] = (subBlock.value as (p: Record<string, never>) => unknown)({})
}
}
return seeded
}
/**
* Returns what the tool actually receives. The executor merges the mapper's
* output OVER the raw inputs (`finalInputs = { ...inputs, ...transformedParams }`
* in `executor/handlers/generic/generic-handler.ts`), so a key the mapper merely
* omits survives as its raw subBlock string. Asserting on the mapper's return
* alone would let an alias or a stale filter through unnoticed.
*/
function mapFor(operation: string, extra: Record<string, unknown> = {}) {
const inputs = { ...seededDefaults(), apiKey, operation, ...extra }
return { ...inputs, ...(mapParams?.(inputs as never) as Record<string, unknown>) }
}
describe('subBlock ids that share a tool param keep their own default', () => {
it('does not leak the Access application type onto DNS record creation', () => {
// The Access "Application Type" control is defined after every DNS type
// control, so a shared id would seed create_dns_record with self_hosted.
const mapped = mapFor('create_dns_record', {
zoneId: 'zone1',
name: 'www.example.com',
content: '203.0.113.10',
})
expect(mapped.type).toBe('A')
expect(mapped.type).not.toBe('self_hosted')
})
it('keeps the Access application type when creating an application', () => {
expect(mapFor('create_access_application', { accountId: 'acct1' }).type).toBe('self_hosted')
})
it('never seeds a type or decision onto an Access resource it is about to replace', () => {
// Both Access updates are full replacements, so a seeded value rewrites what
// the live resource IS as soon as anything else is edited — a policy would
// flip from deny to allow, an application from saas to self_hosted.
const app = mapFor('update_access_application', { accountId: 'acct1', appId: 'app1' })
expect(app.type).toBeUndefined()
expect(
mapFor('update_access_application', {
accountId: 'acct1',
appId: 'app1',
updateAppType: 'saas',
}).type
).toBe('saas')
const policy = mapFor('update_access_policy', { accountId: 'acct1', policyId: 'p1' })
expect(policy.decision).toBeUndefined()
expect(
mapFor('update_access_policy', {
accountId: 'acct1',
policyId: 'p1',
updatePolicyDecision: 'deny',
}).decision
).toBe('deny')
})
it('leaves the DNS record type unset on the filter operations', () => {
expect(mapFor('list_dns_records', { zoneId: 'zone1' }).type).toBeUndefined()
expect(mapFor('update_dns_record', { zoneId: 'zone1', recordId: 'rec1' }).type).toBeUndefined()
})
it('preserves the certificate status default past the later status filters', () => {
// list_tunnels defines the last `status` control and defaults it to empty.
expect(mapFor('list_certificates', { zoneId: 'zone1' }).status).toBe('all')
expect(mapFor('list_zones').status).toBeUndefined()
expect(mapFor('list_tunnels', { accountId: 'acct1' }).status).toBeUndefined()
})
it('preserves the created-record proxied default past the later proxied filters', () => {
const mapped = mapFor('create_dns_record', {
zoneId: 'zone1',
name: 'www.example.com',
content: '203.0.113.10',
})
expect(mapped.proxied).toBe(false)
expect(mapFor('list_dns_records', { zoneId: 'zone1' }).proxied).toBeUndefined()
})
it('does not seed a block action onto a WAF custom rule', () => {
// The rate limiting action dropdown defaults to block and is defined after
// the ruleset-rule action input; sharing an id would make every new WAF
// custom rule silently default to blocking traffic.
const mapped = mapFor('create_ruleset_rule', {
zoneId: 'zone1',
rulesetId: 'rs1',
expression: 'true',
})
expect(mapped.action).toBeUndefined()
})
it('keeps the block default when creating a rate limiting rule', () => {
expect(mapFor('create_rate_limit_rule', { zoneId: 'zone1', rulesetId: 'rs1' }).action).toBe(
'block'
)
})
it('never seeds an action onto a rate limiting rule it is about to replace', () => {
// The update endpoint replaces the rule, so a seeded block would convert a
// live log or challenge rule into a hard block the moment anything else is
// edited. The user has to state the action instead.
expect(
mapFor('update_rate_limit_rule', { zoneId: 'zone1', rulesetId: 'rs1', ruleId: 'r1' }).action
).toBeUndefined()
expect(
mapFor('update_rate_limit_rule', {
zoneId: 'zone1',
rulesetId: 'rs1',
ruleId: 'r1',
updateRateLimitAction: 'log',
}).action
).toBe('log')
})
it('strips the aliased control ids so they never reach a tool as params', () => {
const mapped = mapFor('create_dns_record', { zoneId: 'zone1' })
for (const alias of [
'recordType',
'recordProxied',
'certificateStatus',
'appType',
'updateAppType',
'updatePolicyDecision',
'rateLimitAction',
'updateRateLimitAction',
'rulesetName',
'zoneNameFilter',
'zoneType',
'dnsTypeFilter',
'dnsNameFilter',
'dnsContentFilter',
'dnsOrder',
'dnsProxiedFilter',
'purgeTags',
'workerTagFilter',
'accessAppTags',
'listNameFilter',
'accessAppDomainFilter',
'tunnelStatus',
]) {
expect(mapped[alias], `alias ${alias} reached the tool`).toBeUndefined()
}
})
it('sends the ruleset name as the name param when creating a ruleset', () => {
const mapped = mapFor('create_ruleset', {
zoneId: 'zone1',
phase: 'http_ratelimit',
rulesetName: 'Zone rate limiting ruleset',
})
expect(mapped.name).toBe('Zone rate limiting ruleset')
expect(mapped.rulesetName).toBeUndefined()
})
})
/**
* `shouldSerializeSubBlock` (serializer/index.ts) short-circuits on
* `mode: 'advanced'` BEFORE evaluating `condition`, so an advanced control whose
* stored value is non-empty is serialized even when the selected operation does
* not render it. That is harmless while every operation that consumes the id
* also exposes a control for it — the user can see and change the value — and it
* is a silent cross-operation write when it does not.
*/
/**
* Two mechanical guards on subBlock id reuse. Block state is keyed by subBlock
* id, so controls sharing an id share one stored value — fine when they mean the
* same thing, a silent cross-operation bug when they do not. These encode the
* two shapes that divergence takes here.
*/
describe('a shared subBlock id means the same thing everywhere', () => {
/**
* Ids that legitimately span reads and writes because they address the
* resource rather than carry payload: credentials, account/zone scope, the
* R2 jurisdiction routing header, the ruleset phase, and resource ids.
*/
const ADDRESSING_IDS = new Set([
'apiKey',
'operation',
'accountId',
'zoneId',
'jurisdiction',
'phase',
'appId',
'bucketName',
'rulesetId',
])
const WRITE_PREFIXES = ['create_', 'update_', 'delete_', 'purge_', 'revoke_']
function operationsFor(subBlock: (typeof CloudflareBlock.subBlocks)[number]): string[] {
const condition = subBlock.condition
if (!condition || typeof condition !== 'object' || !('field' in condition)) return []
if (condition.field !== 'operation') return []
const value = condition.value
return Array.isArray(value) ? value.map(String) : [String(value)]
}
it('never shares an id between a read filter and a written value', () => {
const kindsById = new Map<string, Set<string>>()
for (const subBlock of CloudflareBlock.subBlocks) {
if (ADDRESSING_IDS.has(subBlock.id)) continue
for (const operation of operationsFor(subBlock)) {
const kind = WRITE_PREFIXES.some((prefix) => operation.startsWith(prefix))
? 'write'
: 'read'
const kinds = kindsById.get(subBlock.id) ?? new Set<string>()
kinds.add(kind)
kindsById.set(subBlock.id, kinds)
}
}
const mixed = [...kindsById.entries()]
.filter(([, kinds]) => kinds.size > 1)
.map(([id]) => id)
.sort()
expect(mixed).toEqual([])
})
it('never gives one dropdown id two different option sets', () => {
const optionsById = new Map<string, Set<string>>()
for (const subBlock of CloudflareBlock.subBlocks) {
if (subBlock.type !== 'dropdown' || !Array.isArray(subBlock.options)) continue
const signature = JSON.stringify(
subBlock.options.map((option) =>
typeof option === 'string' ? option : ((option as { id?: string }).id ?? '')
)
)
const signatures = optionsById.get(subBlock.id) ?? new Set<string>()
signatures.add(signature)
optionsById.set(subBlock.id, signatures)
}
const divergent = [...optionsById.entries()]
.filter(([, signatures]) => signatures.size > 1)
.map(([id, signatures]) => `${id}: ${[...signatures].join(' vs ')}`)
expect(divergent).toEqual([])
})
})
describe('no hidden advanced control feeds an operation that cannot show it', () => {
const STALE = '__stale_value__'
const toolsByOperation = new Map(
Object.values(cloudflareTools).map((tool) => [tool.id.replace(/^cloudflare_/, ''), tool])
)
/** Operations a subBlock's `condition` makes it visible for. */
function conditionOperations(subBlock: (typeof CloudflareBlock.subBlocks)[number]): string[] {
const condition = subBlock.condition
if (!condition || typeof condition !== 'object' || !('field' in condition)) return []
if (condition.field !== 'operation') return []
const value = condition.value
return Array.isArray(value) ? value.map(String) : [String(value)]
}
it('every advanced id reaching a tool is either shown or remapped for that operation', () => {
const operations = [...toolsByOperation.keys()]
const visibleIdsByOperation = new Map<string, Set<string>>(
operations.map((operation) => [operation, new Set<string>()])
)
for (const subBlock of CloudflareBlock.subBlocks) {
for (const operation of conditionOperations(subBlock)) {
visibleIdsByOperation.get(operation)?.add(subBlock.id)
}
}
const leaks: string[] = []
for (const subBlock of CloudflareBlock.subBlocks) {
if (subBlock.mode !== 'advanced') continue
const ownOperations = new Set(conditionOperations(subBlock))
for (const operation of operations) {
if (ownOperations.has(operation)) continue
const tool = toolsByOperation.get(operation)
if (!tool?.params || !(subBlock.id in tool.params)) continue
if (visibleIdsByOperation.get(operation)?.has(subBlock.id)) continue
// The mapper must overwrite or clear the stale value for this operation.
const mapped = mapFor(operation, { [subBlock.id]: STALE })
if (mapped[subBlock.id] === STALE) {
leaks.push(
`"${subBlock.id}" (advanced, shown for ${[...ownOperations].join('/') || 'nothing'}) reaches ${operation} as a param it never renders`
)
}
}
}
expect(leaks).toEqual([])
})
})
@@ -0,0 +1,236 @@
import type {
CloudflareAccessApplicationResponse,
CloudflareCreateAccessApplicationParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyAccessApplication,
mapAccessApplication,
parseCsvParam,
parseJsonArrayParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createAccessApplicationTool: ToolConfig<
CloudflareCreateAccessApplicationParams,
CloudflareAccessApplicationResponse
> = {
id: 'cloudflare_create_access_application',
name: 'Cloudflare Create Access Application',
description:
'Creates a Cloudflare Access (Zero Trust) application that puts an identity check in front of a hostname. Until at least one policy is attached the application denies everyone, so pair this with "Create Access Policy". Requires an API token with Account Access: Apps and Policies Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
type: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Application type: self_hosted, saas, ssh, vnc, app_launcher, warp, biso, bookmark, dash_sso, infrastructure, rdp, mcp, mcp_portal, or proxy_endpoint',
},
domain: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The primary hostname and path secured by Access, e.g. internal.example.com or example.com/admin. Required for the self_hosted, ssh, vnc, rdp, and bookmark types; the saas, app_launcher, warp, biso, dash_sso, infrastructure, mcp, mcp_portal, and proxy_endpoint types do not accept it',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Friendly name shown in the dashboard and App Launcher',
},
sessionDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'How long an Access session stays valid, e.g. 24h or 30m',
},
allowedIdps: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated identity provider IDs users may authenticate with. Leave empty to allow all configured providers',
},
appLauncherVisible: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the application is shown in the App Launcher',
},
autoRedirectToIdentity: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether users skip the identity provider picker',
},
customDenyMessage: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Message shown to users who are denied access',
},
customDenyUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL denied users are redirected to',
},
logoUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Logo image URL shown in the dashboard and App Launcher',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated tag names categorizing the application',
},
policies: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of policies to attach. Entries may be reusable policy IDs or inline policy objects, e.g. ["<POLICY_ID>"]',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps`,
method: 'POST',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = { type: params.type }
/**
* `domain` exists only on the self_hosted, ssh, vnc, rdp, and bookmark
* request variants; the saas, app_launcher, warp, biso, dash_sso,
* infrastructure, mcp, mcp_portal, and proxy_endpoint variants have no
* such field, so sending a blank one makes those app types unbuildable.
*/
if (params.domain) body.domain = params.domain
if (params.name) body.name = params.name
if (params.sessionDuration) body.session_duration = params.sessionDuration
const allowedIdps = parseCsvParam(params.allowedIdps)
if (allowedIdps) body.allowed_idps = allowedIdps
if (params.appLauncherVisible !== undefined) {
body.app_launcher_visible = params.appLauncherVisible
}
if (params.autoRedirectToIdentity !== undefined) {
body.auto_redirect_to_identity = params.autoRedirectToIdentity
}
if (params.customDenyMessage) body.custom_deny_message = params.customDenyMessage
if (params.customDenyUrl) body.custom_deny_url = params.customDenyUrl
if (params.logoUrl) body.logo_url = params.logoUrl
const tags = parseCsvParam(params.tags)
if (tags) body.tags = tags
const policies = parseJsonArrayParam(params.policies, 'Policies')
if (policies) body.policies = policies
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyAccessApplication(),
error: cloudflareErrorMessage(data, 'Failed to create Access application'),
}
}
return { success: true, output: mapAccessApplication(data.result) }
},
outputs: {
id: { type: 'string', description: 'Created Access application identifier' },
name: { type: 'string', description: 'Application name', optional: true },
domain: {
type: 'string',
description: 'Primary hostname and path secured by Access',
optional: true,
},
type: { type: 'string', description: 'Application type', optional: true },
aud: { type: 'string', description: 'Audience tag used to verify Access JWTs', optional: true },
session_duration: {
type: 'string',
description: 'How long an Access session stays valid',
optional: true,
},
allowed_idps: {
type: 'array',
description: 'Identity provider IDs users may authenticate with',
items: { type: 'string', description: 'Identity provider ID' },
optional: true,
},
app_launcher_visible: {
type: 'boolean',
description: 'Whether the app appears in the App Launcher',
optional: true,
},
auto_redirect_to_identity: {
type: 'boolean',
description: 'Whether users skip the identity provider picker',
optional: true,
},
custom_deny_message: {
type: 'string',
description: 'Message shown when access is denied',
optional: true,
},
custom_deny_url: {
type: 'string',
description: 'URL users are redirected to when access is denied',
optional: true,
},
logo_url: { type: 'string', description: 'Logo image URL', optional: true },
self_hosted_domains: {
type: 'array',
description:
'Additional hostnames and paths secured by the application. Cloudflare deprecated this field in favour of destinations, which is the one to read on a current application',
items: { type: 'string', description: 'Hostname and path' },
optional: true,
},
destinations: {
type: 'json',
description: 'Public and private destinations secured by the application',
optional: true,
},
tags: {
type: 'array',
description: 'Tags categorizing the application',
items: { type: 'string', description: 'Tag name' },
optional: true,
},
policies: {
type: 'json',
description: 'Access policies attached to the application',
optional: true,
},
},
}
@@ -0,0 +1,214 @@
import type {
CloudflareAccessPolicyResponse,
CloudflareCreateAccessPolicyParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyAccessPolicy,
mapAccessPolicy,
parseJsonArrayParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createAccessPolicyTool: ToolConfig<
CloudflareCreateAccessPolicyParams,
CloudflareAccessPolicyResponse
> = {
id: 'cloudflare_create_access_policy',
name: 'Cloudflare Create Access Policy',
description:
'Creates a Cloudflare Access (Zero Trust) policy on an application, deciding who may reach it. A policy takes effect on live traffic as soon as it is created — an allow policy with a broad include rule grants access immediately. Requires an API token with Account Access: Apps and Policies Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID to attach the policy to',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the policy',
},
decision: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'What the policy does when it matches: allow, deny, non_identity (service tokens and other non-identity rules), or bypass (skip Access entirely)',
},
include: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of Access rules evaluated with OR logic — matching any one selects the policy. Example: [{"email":{"email":"user@example.com"}}] or [{"email_domain":{"domain":"example.com"}}]',
},
exclude: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of Access rules evaluated with NOT logic — matching any one rejects the request',
},
require: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of Access rules evaluated with AND logic — all of them must match',
},
precedence: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Evaluation order of the policy within the application',
},
sessionDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'How long a session granted by this policy stays valid, e.g. 24h',
},
approvalRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether an approver must grant each access request',
},
isolationRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the session must run in a remote isolated browser',
},
purposeJustificationRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether users must state a reason for access',
},
purposeJustificationPrompt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Prompt shown when a justification is required',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}/policies`,
method: 'POST',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const include = parseJsonArrayParam(params.include, 'Include Rules')
if (!include || include.length === 0) {
throw new Error('Include Rules must contain at least one Access rule')
}
const body: Record<string, unknown> = {
name: params.name,
decision: params.decision,
include,
}
const exclude = parseJsonArrayParam(params.exclude, 'Exclude Rules')
if (exclude) body.exclude = exclude
const require = parseJsonArrayParam(params.require, 'Require Rules')
if (require) body.require = require
if (params.precedence !== undefined) body.precedence = params.precedence
if (params.sessionDuration) body.session_duration = params.sessionDuration
if (params.approvalRequired !== undefined) body.approval_required = params.approvalRequired
if (params.isolationRequired !== undefined) body.isolation_required = params.isolationRequired
if (params.purposeJustificationRequired !== undefined) {
body.purpose_justification_required = params.purposeJustificationRequired
}
if (params.purposeJustificationPrompt) {
body.purpose_justification_prompt = params.purposeJustificationPrompt
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyAccessPolicy(),
error: cloudflareErrorMessage(data, 'Failed to create Access policy'),
}
}
return { success: true, output: mapAccessPolicy(data.result) }
},
outputs: {
id: { type: 'string', description: 'Created policy identifier' },
name: { type: 'string', description: 'Policy name', optional: true },
decision: {
type: 'string',
description: 'Decision the policy applies: allow, deny, non_identity, or bypass',
optional: true,
},
precedence: {
type: 'number',
description: 'Evaluation order of the policy within the application',
optional: true,
},
include: {
type: 'json',
description: 'Rules evaluated with OR logic',
optional: true,
},
exclude: { type: 'json', description: 'Rules evaluated with NOT logic', optional: true },
require: { type: 'json', description: 'Rules evaluated with AND logic', optional: true },
session_duration: {
type: 'string',
description: 'How long a session granted by this policy stays valid',
optional: true,
},
approval_required: {
type: 'boolean',
description: 'Whether an approver must grant each access request',
optional: true,
},
isolation_required: {
type: 'boolean',
description: 'Whether the session must run in a remote browser',
optional: true,
},
purpose_justification_required: {
type: 'boolean',
description: 'Whether users must state a reason for access',
optional: true,
},
purpose_justification_prompt: {
type: 'string',
description: 'Prompt shown when a justification is required',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
}
@@ -0,0 +1,123 @@
import type {
CloudflareCreateAccessServiceTokenParams,
CloudflareCreateAccessServiceTokenResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createAccessServiceTokenTool: ToolConfig<
CloudflareCreateAccessServiceTokenParams,
CloudflareCreateAccessServiceTokenResponse
> = {
id: 'cloudflare_create_access_service_token',
name: 'Cloudflare Create Access Service Token',
description:
'Creates a Cloudflare Access (Zero Trust) service token so a machine can authenticate to Access-protected applications. This is the only response that ever contains the client secret — Cloudflare will not return it again, so capture it in the same run. Requires an API token with Account Access: Service Tokens Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Service tokens are account-scoped',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the service token',
},
duration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"How long the token stays valid before it expires, e.g. 8760h. Defaults to Cloudflare's standard lifetime",
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/service_tokens`,
method: 'POST',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = { name: params.name }
if (params.duration) body.duration = params.duration
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
id: '',
name: null,
client_id: null,
client_secret: null,
duration: null,
enabled: null,
expires_at: null,
last_seen_at: null,
created_at: null,
updated_at: null,
},
error: cloudflareErrorMessage(data, 'Failed to create Access service token'),
}
}
const token = data.result
return {
success: true,
output: {
id: token?.id ?? '',
name: token?.name ?? null,
client_id: token?.client_id ?? null,
client_secret: token?.client_secret ?? null,
duration: token?.duration ?? null,
enabled: token?.enabled ?? null,
expires_at: token?.expires_at ?? null,
last_seen_at: token?.last_seen_at ?? null,
created_at: token?.created_at ?? null,
updated_at: token?.updated_at ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Created service token identifier' },
name: { type: 'string', description: 'Service token name', optional: true },
client_id: {
type: 'string',
description: 'Client ID sent in the CF-Access-Client-Id header',
optional: true,
},
client_secret: {
type: 'string',
description:
'Client secret sent in the CF-Access-Client-Secret header. Returned only once, at creation',
optional: true,
},
duration: {
type: 'string',
description: 'How long the token stays valid before it expires',
optional: true,
},
enabled: { type: 'boolean', description: 'Whether the token is active', optional: true },
expires_at: { type: 'string', description: 'Expiry timestamp', optional: true },
last_seen_at: { type: 'string', description: 'When the token was last used', optional: true },
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
}
+10 -4
View File
@@ -54,7 +54,8 @@ export const createDnsRecordTool: ToolConfig<
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Priority for MX and SRV records',
description:
'Record priority. Cloudflare accepts this top-level field for MX and URI records only; an SRV record carries its priority, weight, port, and target inside the record content instead',
},
comment: {
type: 'string',
@@ -77,14 +78,15 @@ export const createDnsRecordTool: ToolConfig<
},
request: {
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records`,
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/dns_records`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {
const body: Record<string, unknown> = {
type: params.type,
name: params.name,
content: params.content,
@@ -177,7 +179,11 @@ export const createDnsRecordTool: ToolConfig<
proxied: { type: 'boolean', description: 'Whether Cloudflare proxy is enabled' },
ttl: { type: 'number', description: 'Time to live in seconds (1 = automatic)' },
locked: { type: 'boolean', description: 'Whether the record is locked' },
priority: { type: 'number', description: 'Priority for MX and SRV records', optional: true },
priority: {
type: 'number',
description: 'Record priority, returned for MX and URI records',
optional: true,
},
comment: { type: 'string', description: 'Comment associated with the record', optional: true },
tags: {
type: 'array',
@@ -0,0 +1,125 @@
import type {
CloudflareCreateR2BucketParams,
CloudflareR2BucketResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createR2BucketTool: ToolConfig<
CloudflareCreateR2BucketParams,
CloudflareR2BucketResponse
> = {
id: 'cloudflare_create_r2_bucket',
name: 'Cloudflare Create R2 Bucket',
description:
'Creates an R2 object storage bucket in an account. The location hint and jurisdiction are fixed at creation and cannot be changed later. Requires an API token with Account Workers R2 Storage Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. R2 buckets are account-scoped',
},
bucketName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new bucket',
},
locationHint: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Region hint for where the bucket should live: apac, eeur, enam, weur, wnam, or oc. Cannot be changed after creation',
},
storageClass: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Default storage class for objects: Standard or InfrequentAccess',
},
jurisdiction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Data-residency jurisdiction to create the bucket in: default, eu, or fedramp. Cannot be changed after creation',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/r2/buckets`,
method: 'POST',
headers: (params) => {
const headers = cloudflareHeaders(params.apiKey)
if (params.jurisdiction) headers['cf-r2-jurisdiction'] = params.jurisdiction
return headers
},
body: (params) => {
const body: Record<string, unknown> = { name: params.bucketName }
if (params.locationHint) body.locationHint = params.locationHint
if (params.storageClass) body.storageClass = params.storageClass
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
name: '',
creation_date: null,
location: null,
storage_class: null,
jurisdiction: null,
},
error: cloudflareErrorMessage(data, 'Failed to create R2 bucket'),
}
}
const bucket = data.result
return {
success: true,
output: {
name: bucket?.name ?? '',
creation_date: bucket?.creation_date ?? null,
location: bucket?.location ?? null,
storage_class: bucket?.storage_class ?? null,
jurisdiction: bucket?.jurisdiction ?? null,
},
}
},
outputs: {
name: { type: 'string', description: 'Created bucket name' },
creation_date: { type: 'string', description: 'Creation timestamp', optional: true },
location: {
type: 'string',
description: 'Location the bucket was created in',
optional: true,
},
storage_class: {
type: 'string',
description: 'Default storage class (Standard or InfrequentAccess)',
optional: true,
},
jurisdiction: {
type: 'string',
description: 'Data-residency jurisdiction (default, eu, or fedramp)',
optional: true,
},
},
}
@@ -0,0 +1,212 @@
import type {
CloudflareCreateRateLimitRuleParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
parseCsvParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createRateLimitRuleTool: ToolConfig<
CloudflareCreateRateLimitRuleParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_create_rate_limit_rule',
name: 'Cloudflare Create Rate Limiting Rule',
description:
'Creates a rate limiting rule in the http_ratelimit phase entry point ruleset of a zone, using the current Rulesets-based rate limiting API (the legacy rate_limits endpoint is no longer available). Run "List Rate Limiting Rules" first to get the ruleset ID. Requires an API token with Zone WAF Edit.',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID to add the rate limiting rule to',
},
rulesetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The http_ratelimit entry point ruleset ID, as returned by "List Rate Limiting Rules"',
},
expression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Cloudflare filter expression selecting the requests the rule applies to, e.g. (http.request.uri.path matches "^/api/")',
},
characteristics: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated counting characteristics. cf.colo.id is mandatory, plus exactly one of ip.src or cf.unique_visitor_id. Example: cf.colo.id,ip.src',
},
period: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description:
'Counting window in seconds. Cloudflare accepts only 10, 60, 120, 300, 600, or 3600',
},
requestsPerPeriod: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of requests allowed within the counting period before the action fires',
},
action: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Action applied once the limit is exceeded, e.g. block, managed_challenge, js_challenge, challenge, or log. Defaults to block',
},
mitigationTimeout: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Seconds the action stays applied after the limit is exceeded. Cloudflare accepts only 0, 10, 60, 120, 300, 600, 3600, or 86400',
},
counting_expression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Optional expression defining which requests are counted, when it differs from the matching expression',
},
requestsToOrigin: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, only requests that reach the origin are counted',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Human-readable description of the rule',
},
enabled: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the rule is enabled',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/${params.rulesetId.trim()}/rules`,
method: 'POST',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const characteristics = parseCsvParam(params.characteristics)
if (!characteristics) {
throw new Error('Characteristics must list at least one counting characteristic')
}
const ratelimit: Record<string, unknown> = {
characteristics,
period: params.period,
requests_per_period: params.requestsPerPeriod,
}
if (params.mitigationTimeout !== undefined) {
ratelimit.mitigation_timeout = params.mitigationTimeout
}
if (params.counting_expression) ratelimit.counting_expression = params.counting_expression
if (params.requestsToOrigin !== undefined) {
ratelimit.requests_to_origin = params.requestsToOrigin
}
const body: Record<string, unknown> = {
action: params.action || 'block',
expression: params.expression,
ratelimit,
}
if (params.description) body.description = params.description
if (params.enabled !== undefined) body.enabled = params.enabled
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to create rate limiting rule'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset ID of the http_ratelimit entry point' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind' },
phase: { type: 'string', description: 'Phase the ruleset runs in (http_ratelimit)' },
version: { type: 'string', description: 'Ruleset version after the change', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rate limiting rules after the change, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: { type: 'string', description: 'Action applied once the limit is exceeded' },
action_parameters: {
type: 'json',
description: 'Action-specific parameters',
optional: true,
},
expression: { type: 'string', description: 'Filter expression' },
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: { type: 'string', description: 'Rule reference tag', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description: 'Rate limiting configuration applied to the rule',
optional: true,
},
},
},
},
},
}
+167
View File
@@ -0,0 +1,167 @@
import type {
CloudflareCreateRulesetParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
parseJsonArrayParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createRulesetTool: ToolConfig<
CloudflareCreateRulesetParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_create_ruleset',
name: 'Cloudflare Create Ruleset',
description:
'Creates a zone ruleset for a phase, optionally seeded with its first rules. Use this when a phase has no entry point ruleset yet — reading the entry point returns 404 on a zone that has never had a rule in that phase, and rules can only be appended to a ruleset that already exists. Create the entry point with kind "zone" and the target phase (for example http_ratelimit for rate limiting rules or http_request_firewall_custom for WAF custom rules), then use the returned ruleset ID for later rule operations. Requires an API token with Zone WAF Edit (or another matching ruleset Edit permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID to create the ruleset in',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Human-readable name for the ruleset',
},
phase: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ruleset phase, e.g. http_ratelimit, http_request_firewall_custom, http_request_firewall_managed, http_request_transform, http_request_dynamic_redirect',
},
kind: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Ruleset kind: zone, custom, managed, or root. Use zone to create a phase entry point ruleset. Defaults to zone',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description of the ruleset',
},
rules: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of rules to seed the ruleset with, in evaluation order. Each rule takes action, expression, and optionally description, enabled, action_parameters, and ratelimit',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets`,
method: 'POST',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
name: params.name,
kind: params.kind || 'zone',
phase: params.phase,
}
if (params.description) body.description = params.description
const rules = parseJsonArrayParam(params.rules, 'Rules')
body.rules = rules ?? []
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to create ruleset'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind (managed, custom, root, or zone)' },
phase: { type: 'string', description: 'Phase the ruleset runs in' },
version: { type: 'string', description: 'Ruleset version', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rules contained in the ruleset, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: {
type: 'string',
description: 'Action the rule performs (e.g., block, challenge, log, skip, execute)',
},
action_parameters: {
type: 'json',
description:
'Action-specific parameters, including managed-ruleset overrides on execute rules',
optional: true,
},
expression: {
type: 'string',
description:
'Filter expression selecting matching requests. Empty on managed-ruleset rules',
},
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: {
type: 'string',
description: 'Rule reference tag that survives rule updates',
optional: true,
},
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description: 'Rate limiting configuration for rules in the http_ratelimit phase',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,179 @@
import type {
CloudflareCreateRulesetRuleParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
parseJsonObjectParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const createRulesetRuleTool: ToolConfig<
CloudflareCreateRulesetRuleParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_create_ruleset_rule',
name: 'Cloudflare Create Ruleset Rule',
description:
'Adds a rule to a zone ruleset. Use "Get Phase Entry Point Ruleset" first to find the ruleset ID for the phase you want (for example http_request_firewall_custom for a WAF custom rule, or http_request_firewall_managed with action "execute" to deploy a managed ruleset). The rule is appended to the end of the ruleset unless a position is given. Requires an API token with Zone WAF Edit (or another matching ruleset Write permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID that owns the ruleset',
},
rulesetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ruleset ID to add the rule to',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The action the rule performs. Valid values depend on the phase — e.g. block, challenge, js_challenge, managed_challenge, log, skip, or execute (to deploy a managed ruleset)',
},
expression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Cloudflare filter expression selecting matching requests, e.g. (ip.src.country in {"GB" "FR"}). Use "true" to match every request',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Human-readable description of the rule',
},
enabled: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the rule is enabled',
},
ref: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Reference tag that stays stable across rule updates',
},
actionParameters: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON object of action-specific parameters. For an "execute" rule this carries the managed ruleset id and any overrides, e.g. {"id":"<MANAGED_RULESET_ID>","overrides":{"action":"log"}}',
},
position: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON object placing the rule within the ruleset. Exactly one of {"before":"<RULE_ID>"}, {"after":"<RULE_ID>"}, or {"index":<1-based position>}',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/${params.rulesetId.trim()}/rules`,
method: 'POST',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
action: params.action,
expression: params.expression,
}
if (params.description) body.description = params.description
if (params.enabled !== undefined) body.enabled = params.enabled
if (params.ref) body.ref = params.ref
const actionParameters = parseJsonObjectParam(params.actionParameters, 'Action Parameters')
if (actionParameters) body.action_parameters = actionParameters
const position = parseJsonObjectParam(params.position, 'Position')
if (position) body.position = position
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to create ruleset rule'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind (managed, custom, root, or zone)' },
phase: { type: 'string', description: 'Phase the ruleset runs in' },
version: { type: 'string', description: 'Ruleset version after the change', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rules in the ruleset after the change, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: { type: 'string', description: 'Action the rule performs' },
action_parameters: {
type: 'json',
description: 'Action-specific parameters',
optional: true,
},
expression: { type: 'string', description: 'Filter expression' },
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: { type: 'string', description: 'Rule reference tag', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description: 'Rate limiting configuration',
optional: true,
},
},
},
},
},
}
+1 -1
View File
@@ -47,7 +47,7 @@ export const createZoneTool: ToolConfig<CloudflareCreateZoneParams, CloudflareCr
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {
const body: Record<string, unknown> = {
name: params.name,
account: { id: params.accountId },
}
@@ -0,0 +1,63 @@
import type {
CloudflareDeleteAccessApplicationParams,
CloudflareDeletedIdResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteAccessApplicationTool: ToolConfig<
CloudflareDeleteAccessApplicationParams,
CloudflareDeletedIdResponse
> = {
id: 'cloudflare_delete_access_application',
name: 'Cloudflare Delete Access Application',
description:
'Permanently deletes a Cloudflare Access (Zero Trust) application and every policy attached to it. The hostname it protected is immediately left without an Access identity check, so anyone who can reach it can reach the origin. This cannot be undone. Requires an API token with Account Access: Apps and Policies Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID to delete permanently',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}`,
method: 'DELETE',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: { id: '' },
error: cloudflareErrorMessage(data, 'Failed to delete Access application'),
}
}
return { success: true, output: { id: data.result?.id ?? '' } }
},
outputs: {
id: { type: 'string', description: 'Identifier of the deleted Access application' },
},
}
@@ -0,0 +1,69 @@
import type {
CloudflareDeleteAccessPolicyParams,
CloudflareDeletedIdResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteAccessPolicyTool: ToolConfig<
CloudflareDeleteAccessPolicyParams,
CloudflareDeletedIdResponse
> = {
id: 'cloudflare_delete_access_policy',
name: 'Cloudflare Delete Access Policy',
description:
'Permanently deletes a Cloudflare Access (Zero Trust) policy from an application. This changes who can reach the application the moment it runs: removing an allow policy locks out everyone it covered, and removing a deny or require policy drops that restriction. This cannot be undone. Requires an API token with Account Access: Apps and Policies Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID that owns the policy',
},
policyId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access policy ID to delete permanently',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}/policies/${params.policyId.trim()}`,
method: 'DELETE',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: { id: '' },
error: cloudflareErrorMessage(data, 'Failed to delete Access policy'),
}
}
return { success: true, output: { id: data.result?.id ?? '' } }
},
outputs: {
id: { type: 'string', description: 'Identifier of the deleted Access policy' },
},
}
+11 -2
View File
@@ -36,7 +36,7 @@ export const deleteDnsRecordTool: ToolConfig<
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records/${params.recordId}`,
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/dns_records/${params.recordId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -47,7 +47,16 @@ export const deleteDnsRecordTool: ToolConfig<
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
/**
* This endpoint is the one Cloudflare v4 response that does NOT carry the
* shared envelope: its documented body is `{ "result": { "id": ... } }` with
* no `success`, `errors`, or `messages`. Branching on `!data.success` would
* therefore report every successful delete as a failure, so the check must
* be an explicit `=== false` — which still fails a real `success: false`
* body if Cloudflare ever starts sending the full envelope here.
* https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/
*/
if (data.success === false) {
return {
success: false,
output: { id: '' },
@@ -0,0 +1,77 @@
import type {
CloudflareDeleteR2BucketParams,
CloudflareDeleteR2BucketResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteR2BucketTool: ToolConfig<
CloudflareDeleteR2BucketParams,
CloudflareDeleteR2BucketResponse
> = {
id: 'cloudflare_delete_r2_bucket',
name: 'Cloudflare Delete R2 Bucket',
description:
'Permanently deletes an R2 object storage bucket. Cloudflare only deletes an empty bucket, and the deletion cannot be undone. Requires an API token with Account Workers R2 Storage Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. R2 buckets are account-scoped',
},
bucketName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the bucket to delete permanently',
},
jurisdiction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Data-residency jurisdiction the bucket lives in: default, eu, or fedramp',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/r2/buckets/${encodeURIComponent(params.bucketName)}`,
method: 'DELETE',
headers: (params) => {
const headers = cloudflareHeaders(params.apiKey)
if (params.jurisdiction) headers['cf-r2-jurisdiction'] = params.jurisdiction
return headers
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: { name: '' },
error: cloudflareErrorMessage(data, 'Failed to delete R2 bucket'),
}
}
return { success: true, output: { name: params?.bucketName ?? '' } }
},
outputs: {
name: {
type: 'string',
description:
'Name of the deleted bucket. Cloudflare returns an empty result body for this endpoint, so the name is echoed from the request',
},
},
}
@@ -0,0 +1,117 @@
import type {
CloudflareDeleteRulesetRuleParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteRulesetRuleTool: ToolConfig<
CloudflareDeleteRulesetRuleParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_delete_ruleset_rule',
name: 'Cloudflare Delete Ruleset Rule',
description:
'Permanently deletes a rule from a zone ruleset. This takes effect immediately on live traffic and cannot be undone — deleting a WAF custom rule, a managed-ruleset deployment, or a rate limiting rule removes that protection from the zone. Also use this to delete rate limiting rules, which live in the http_ratelimit phase ruleset. Requires an API token with Zone WAF Edit (or another matching ruleset Write permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID that owns the ruleset',
},
rulesetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ruleset ID containing the rule',
},
ruleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The rule ID to delete permanently',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/${params.rulesetId.trim()}/rules/${params.ruleId.trim()}`,
method: 'DELETE',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to delete ruleset rule'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind (managed, custom, root, or zone)' },
phase: { type: 'string', description: 'Phase the ruleset runs in' },
version: { type: 'string', description: 'Ruleset version after the change', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rules remaining in the ruleset, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: { type: 'string', description: 'Action the rule performs' },
action_parameters: {
type: 'json',
description: 'Action-specific parameters',
optional: true,
},
expression: { type: 'string', description: 'Filter expression' },
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: { type: 'string', description: 'Rule reference tag', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: { type: 'json', description: 'Rate limiting configuration', optional: true },
},
},
},
},
}
+1 -1
View File
@@ -27,7 +27,7 @@ export const deleteZoneTool: ToolConfig<CloudflareDeleteZoneParams, CloudflareDe
},
request: {
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}`,
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
+5 -3
View File
@@ -1,7 +1,9 @@
import type {
CloudflareDnsAnalyticsParams,
CloudflareDnsAnalyticsResponse,
CloudflareRawDnsAnalyticsReport,
} from '@/tools/cloudflare/types'
import { readCloudflareResponse } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const dnsAnalyticsTool: ToolConfig<
@@ -78,7 +80,7 @@ export const dnsAnalyticsTool: ToolConfig<
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_analytics/report`
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/dns_analytics/report`
)
if (params.since) url.searchParams.append('since', params.since)
if (params.until) url.searchParams.append('until', params.until)
@@ -97,7 +99,7 @@ export const dnsAnalyticsTool: ToolConfig<
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await readCloudflareResponse<CloudflareRawDnsAnalyticsReport>(response)
if (!data.success) {
return {
@@ -179,7 +181,7 @@ export const dnsAnalyticsTool: ToolConfig<
responseTime99th: result?.max?.responseTime99th ?? 0,
},
data:
result?.data?.map((entry: any) => ({
result?.data?.map((entry) => ({
dimensions: entry.dimensions ?? [],
metrics: entry.metrics ?? [],
})) ?? [],
@@ -0,0 +1,135 @@
import type {
CloudflareAccessApplicationResponse,
CloudflareGetAccessApplicationParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyAccessApplication,
mapAccessApplication,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getAccessApplicationTool: ToolConfig<
CloudflareGetAccessApplicationParams,
CloudflareAccessApplicationResponse
> = {
id: 'cloudflare_get_access_application',
name: 'Cloudflare Get Access Application',
description:
'Reads a single Cloudflare Access (Zero Trust) application, including its attached policies. Requires an API token with Account Access: Apps and Policies Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID (or audience tag) to read',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyAccessApplication(),
error: cloudflareErrorMessage(data, 'Failed to get Access application'),
}
}
return { success: true, output: mapAccessApplication(data.result) }
},
outputs: {
id: { type: 'string', description: 'Access application identifier' },
name: { type: 'string', description: 'Application name', optional: true },
domain: {
type: 'string',
description: 'Primary hostname and path secured by Access',
optional: true,
},
type: {
type: 'string',
description: 'Application type (e.g., self_hosted, saas, ssh, app_launcher, bookmark)',
optional: true,
},
aud: { type: 'string', description: 'Audience tag used to verify Access JWTs', optional: true },
session_duration: {
type: 'string',
description: 'How long an Access session stays valid (e.g., 24h)',
optional: true,
},
allowed_idps: {
type: 'array',
description: 'Identity provider IDs users may authenticate with',
items: { type: 'string', description: 'Identity provider ID' },
optional: true,
},
app_launcher_visible: {
type: 'boolean',
description: 'Whether the app appears in the App Launcher',
optional: true,
},
auto_redirect_to_identity: {
type: 'boolean',
description: 'Whether users skip the identity provider picker',
optional: true,
},
custom_deny_message: {
type: 'string',
description: 'Message shown when access is denied',
optional: true,
},
custom_deny_url: {
type: 'string',
description: 'URL users are redirected to when access is denied',
optional: true,
},
logo_url: { type: 'string', description: 'Logo image URL', optional: true },
self_hosted_domains: {
type: 'array',
description:
'Additional hostnames and paths secured by the application. Cloudflare deprecated this field in favour of destinations, which is the one to read on a current application',
items: { type: 'string', description: 'Hostname and path' },
optional: true,
},
destinations: {
type: 'json',
description: 'Public and private destinations secured by the application',
optional: true,
},
tags: {
type: 'array',
description: 'Tags categorizing the application',
items: { type: 'string', description: 'Tag name' },
optional: true,
},
policies: {
type: 'json',
description: 'Access policies attached to the application',
optional: true,
},
},
}
+104
View File
@@ -0,0 +1,104 @@
import type {
CloudflareGetR2BucketParams,
CloudflareR2BucketResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getR2BucketTool: ToolConfig<CloudflareGetR2BucketParams, CloudflareR2BucketResponse> =
{
id: 'cloudflare_get_r2_bucket',
name: 'Cloudflare Get R2 Bucket',
description:
'Reads the metadata of a single R2 object storage bucket. Requires an API token with Account Workers R2 Storage Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. R2 buckets are account-scoped',
},
bucketName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the bucket to read',
},
jurisdiction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Data-residency jurisdiction the bucket lives in: default, eu, or fedramp',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/r2/buckets/${encodeURIComponent(params.bucketName)}`,
method: 'GET',
headers: (params) => {
const headers = cloudflareHeaders(params.apiKey)
if (params.jurisdiction) headers['cf-r2-jurisdiction'] = params.jurisdiction
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
name: '',
creation_date: null,
location: null,
storage_class: null,
jurisdiction: null,
},
error: cloudflareErrorMessage(data, 'Failed to get R2 bucket'),
}
}
const bucket = data.result
return {
success: true,
output: {
name: bucket?.name ?? '',
creation_date: bucket?.creation_date ?? null,
location: bucket?.location ?? null,
storage_class: bucket?.storage_class ?? null,
jurisdiction: bucket?.jurisdiction ?? null,
},
}
},
outputs: {
name: { type: 'string', description: 'Bucket name' },
creation_date: { type: 'string', description: 'Creation timestamp', optional: true },
location: {
type: 'string',
description:
'Location hint the bucket was created with (apac, eeur, enam, weur, wnam, or oc)',
optional: true,
},
storage_class: {
type: 'string',
description: 'Default storage class (Standard or InfrequentAccess)',
optional: true,
},
jurisdiction: {
type: 'string',
description: 'Data-residency jurisdiction (default, eu, or fedramp)',
optional: true,
},
},
}
+124
View File
@@ -0,0 +1,124 @@
import type {
CloudflareGetRulesetParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getRulesetTool: ToolConfig<CloudflareGetRulesetParams, CloudflareRulesetResponse> = {
id: 'cloudflare_get_ruleset',
name: 'Cloudflare Get Ruleset',
description:
'Reads a single zone ruleset including every rule it contains, in evaluation order. Requires an API token with Zone WAF Read (or another matching ruleset Read permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID that owns the ruleset',
},
rulesetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ruleset ID to read',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/${params.rulesetId.trim()}`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to get ruleset'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind (managed, custom, root, or zone)' },
phase: { type: 'string', description: 'Phase the ruleset runs in' },
version: { type: 'string', description: 'Ruleset version', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rules contained in the ruleset, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: {
type: 'string',
description: 'Action the rule performs (e.g., block, challenge, log, skip, execute)',
},
action_parameters: {
type: 'json',
description:
'Action-specific parameters, including managed-ruleset overrides on execute rules',
optional: true,
},
expression: {
type: 'string',
description:
'Filter expression selecting matching requests. Empty on managed-ruleset rules',
},
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: {
type: 'string',
description: 'Rule reference tag that survives rule updates',
optional: true,
},
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description: 'Rate limiting configuration for rules in the http_ratelimit phase',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,128 @@
import type {
CloudflareGetRulesetEntrypointParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getRulesetEntrypointTool: ToolConfig<
CloudflareGetRulesetEntrypointParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_get_ruleset_entrypoint',
name: 'Cloudflare Get Phase Entry Point Ruleset',
description:
'Reads the entry point ruleset for a phase on a zone, including all of its rules. This is how you find the ruleset ID you need before adding, updating, or deleting a rule — for example http_request_firewall_custom for WAF custom rules, http_request_firewall_managed for managed-ruleset deployments and overrides, or http_ratelimit for rate limiting rules. Requires an API token with Zone WAF Read (or another matching ruleset Read permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID to read the phase entry point for',
},
phase: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ruleset phase, e.g. http_request_firewall_custom, http_request_firewall_managed, http_ratelimit, http_request_transform, http_request_dynamic_redirect',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/phases/${params.phase.trim()}/entrypoint`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to get phase entry point ruleset'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Entry point ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind (managed, custom, root, or zone)' },
phase: { type: 'string', description: 'Phase the ruleset runs in' },
version: { type: 'string', description: 'Ruleset version', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rules contained in the ruleset, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: {
type: 'string',
description: 'Action the rule performs (e.g., block, challenge, log, skip, execute)',
},
action_parameters: {
type: 'json',
description:
'Action-specific parameters, including managed-ruleset overrides on execute rules',
optional: true,
},
expression: {
type: 'string',
description:
'Filter expression selecting matching requests. Empty on managed-ruleset rules',
},
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: {
type: 'string',
description: 'Rule reference tag that survives rule updates',
optional: true,
},
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description: 'Rate limiting configuration for rules in the http_ratelimit phase',
optional: true,
},
},
},
},
},
}
+133
View File
@@ -0,0 +1,133 @@
import type { CloudflareGetTunnelParams, CloudflareTunnelResponse } from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getTunnelTool: ToolConfig<CloudflareGetTunnelParams, CloudflareTunnelResponse> = {
id: 'cloudflare_get_tunnel',
name: 'Cloudflare Get Tunnel',
description:
'Reads a single Cloudflare Tunnel (cloudflared), including its health status and active connector connections. Requires an API token with Account Cloudflare Tunnel Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Tunnels are account-scoped',
},
tunnelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tunnel ID to read',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/cfd_tunnel/${params.tunnelId.trim()}`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
id: '',
name: null,
account_tag: null,
config_src: null,
status: null,
tun_type: null,
remote_config: null,
metadata: null,
created_at: null,
deleted_at: null,
conns_active_at: null,
conns_inactive_at: null,
connections: null,
},
error: cloudflareErrorMessage(data, 'Failed to get tunnel'),
}
}
const tunnel = data.result
return {
success: true,
output: {
id: tunnel?.id ?? '',
name: tunnel?.name ?? null,
account_tag: tunnel?.account_tag ?? null,
config_src: tunnel?.config_src ?? null,
status: tunnel?.status ?? null,
tun_type: tunnel?.tun_type ?? null,
remote_config: tunnel?.remote_config ?? null,
metadata: tunnel?.metadata ?? null,
created_at: tunnel?.created_at ?? null,
deleted_at: tunnel?.deleted_at ?? null,
conns_active_at: tunnel?.conns_active_at ?? null,
conns_inactive_at: tunnel?.conns_inactive_at ?? null,
connections: tunnel?.connections ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Tunnel identifier' },
name: { type: 'string', description: 'Tunnel name', optional: true },
account_tag: { type: 'string', description: 'Account the tunnel belongs to', optional: true },
config_src: {
type: 'string',
description: 'Where the tunnel configuration lives: local or cloudflare',
optional: true,
},
status: {
type: 'string',
description: 'Tunnel health: inactive, degraded, healthy, or down',
optional: true,
},
tun_type: {
type: 'string',
description: 'Tunnel type, e.g. cfd_tunnel, warp_connector, or warp',
optional: true,
},
remote_config: {
type: 'boolean',
description: 'Whether the tunnel is remotely managed',
optional: true,
},
metadata: {
type: 'json',
description: 'Metadata associated with the tunnel',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
deleted_at: { type: 'string', description: 'Deletion timestamp', optional: true },
conns_active_at: {
type: 'string',
description: 'When the tunnel last had active connections',
optional: true,
},
conns_inactive_at: {
type: 'string',
description: 'When the tunnel last lost all connections',
optional: true,
},
connections: {
type: 'json',
description: 'Active connector connections for the tunnel',
optional: true,
},
},
}
@@ -0,0 +1,99 @@
import type {
CloudflareGetTunnelConfigurationParams,
CloudflareGetTunnelConfigurationResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getTunnelConfigurationTool: ToolConfig<
CloudflareGetTunnelConfigurationParams,
CloudflareGetTunnelConfigurationResponse
> = {
id: 'cloudflare_get_tunnel_configuration',
name: 'Cloudflare Get Tunnel Configuration',
description:
'Reads the configuration of a remotely-managed Cloudflare Tunnel — its ingress rules, origin request settings, and WARP routing. Only tunnels whose configuration source is "cloudflare" have a remote configuration; locally-managed tunnels keep it in their own config file. Requires an API token with Account Cloudflare Tunnel Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Tunnels are account-scoped',
},
tunnelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tunnel ID to read the configuration for',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/cfd_tunnel/${params.tunnelId.trim()}/configurations`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
tunnel_id: '',
account_id: '',
version: null,
source: null,
created_at: null,
config: null,
},
error: cloudflareErrorMessage(data, 'Failed to get tunnel configuration'),
}
}
const result = data.result
return {
success: true,
output: {
tunnel_id: result?.tunnel_id ?? '',
account_id: result?.account_id ?? '',
version: result?.version ?? null,
source: result?.source ?? null,
created_at: result?.created_at ?? null,
config: result?.config ?? null,
},
}
},
outputs: {
tunnel_id: { type: 'string', description: 'Tunnel the configuration belongs to' },
account_id: { type: 'string', description: 'Account the tunnel belongs to' },
version: {
type: 'number',
description: 'Configuration version, incremented on every change',
optional: true,
},
source: {
type: 'string',
description: 'Where the configuration is managed: local or cloudflare',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
config: {
type: 'json',
description:
'Tunnel configuration with ingress rules, originRequest defaults, and warp-routing settings',
optional: true,
},
},
}
@@ -0,0 +1,131 @@
import type {
CloudflareGetWorkerScriptSettingsParams,
CloudflareGetWorkerScriptSettingsResponse,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const getWorkerScriptSettingsTool: ToolConfig<
CloudflareGetWorkerScriptSettingsParams,
CloudflareGetWorkerScriptSettingsResponse
> = {
id: 'cloudflare_get_worker_script_settings',
name: 'Cloudflare Get Worker Script Settings',
description:
'Reads the deployment settings of a single Workers script — bindings, compatibility date and flags, limits, observability, placement, and tail consumers. The plain "get script" endpoint in the Cloudflare API returns raw JavaScript source rather than JSON, so this settings endpoint is the structured way to inspect one script. Requires an API token with Account Workers Scripts Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Workers scripts are account-scoped',
},
scriptName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the Workers script to read settings for',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/workers/scripts/${encodeURIComponent(params.scriptName)}/settings`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
bindings: null,
compatibility_date: null,
compatibility_flags: null,
limits: null,
logpush: null,
migrations: null,
observability: null,
placement: null,
tags: null,
tail_consumers: null,
usage_model: null,
},
error: cloudflareErrorMessage(data, 'Failed to get Worker script settings'),
}
}
const settings = data.result
return {
success: true,
output: {
bindings: settings?.bindings ?? null,
compatibility_date: settings?.compatibility_date ?? null,
compatibility_flags: settings?.compatibility_flags ?? null,
limits: settings?.limits ?? null,
logpush: settings?.logpush ?? null,
migrations: settings?.migrations ?? null,
observability: settings?.observability ?? null,
placement: settings?.placement ?? null,
tags: settings?.tags ?? null,
tail_consumers: settings?.tail_consumers ?? null,
usage_model: settings?.usage_model ?? null,
},
}
},
outputs: {
bindings: {
type: 'json',
description: 'Resource bindings available to the script (KV, R2, D1, secrets, and more)',
optional: true,
},
compatibility_date: {
type: 'string',
description: 'Workers runtime compatibility date',
optional: true,
},
compatibility_flags: {
type: 'array',
description: 'Workers runtime compatibility flags',
items: { type: 'string', description: 'Compatibility flag' },
optional: true,
},
limits: { type: 'json', description: 'CPU and other execution limits', optional: true },
logpush: { type: 'boolean', description: 'Whether Workers Logpush is enabled', optional: true },
migrations: { type: 'json', description: 'Durable Object migrations', optional: true },
observability: {
type: 'json',
description: 'Observability and log-sampling configuration',
optional: true,
},
placement: { type: 'json', description: 'Smart placement configuration', optional: true },
tags: {
type: 'array',
description: 'Tags attached to the script',
items: { type: 'string', description: 'Tag name' },
optional: true,
},
tail_consumers: {
type: 'json',
description: "Workers that consume this script's tail events",
optional: true,
},
usage_model: {
type: 'string',
description: 'Billing usage model',
optional: true,
},
},
}
+1 -1
View File
@@ -23,7 +23,7 @@ export const getZoneTool: ToolConfig<CloudflareGetZoneParams, CloudflareGetZoneR
},
request: {
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}`,
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -30,7 +30,7 @@ export const getZoneSettingsTool: ToolConfig<
},
request: {
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}/settings`,
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/settings`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -83,8 +83,7 @@ export const getZoneSettingsTool: ToolConfig<
},
value: {
type: 'string',
description:
'Setting value as a string. Simple values returned as-is (e.g., "full", "on"). Complex values are JSON-stringified (e.g., \'{"css":"on","html":"on","js":"on"}\').',
description: `Setting value as a string. Simple values returned as-is (e.g., "full", "on"). Complex values are JSON-stringified (e.g., {"css":"on","html":"on","js":"on"}).`,
},
editable: {
type: 'boolean',
+70
View File
@@ -1,27 +1,97 @@
import { createAccessApplicationTool } from '@/tools/cloudflare/create_access_application'
import { createAccessPolicyTool } from '@/tools/cloudflare/create_access_policy'
import { createAccessServiceTokenTool } from '@/tools/cloudflare/create_access_service_token'
import { createDnsRecordTool } from '@/tools/cloudflare/create_dns_record'
import { createR2BucketTool } from '@/tools/cloudflare/create_r2_bucket'
import { createRateLimitRuleTool } from '@/tools/cloudflare/create_rate_limit_rule'
import { createRulesetTool } from '@/tools/cloudflare/create_ruleset'
import { createRulesetRuleTool } from '@/tools/cloudflare/create_ruleset_rule'
import { createZoneTool } from '@/tools/cloudflare/create_zone'
import { deleteAccessApplicationTool } from '@/tools/cloudflare/delete_access_application'
import { deleteAccessPolicyTool } from '@/tools/cloudflare/delete_access_policy'
import { deleteDnsRecordTool } from '@/tools/cloudflare/delete_dns_record'
import { deleteR2BucketTool } from '@/tools/cloudflare/delete_r2_bucket'
import { deleteRulesetRuleTool } from '@/tools/cloudflare/delete_ruleset_rule'
import { deleteZoneTool } from '@/tools/cloudflare/delete_zone'
import { dnsAnalyticsTool } from '@/tools/cloudflare/dns_analytics'
import { getAccessApplicationTool } from '@/tools/cloudflare/get_access_application'
import { getR2BucketTool } from '@/tools/cloudflare/get_r2_bucket'
import { getRulesetTool } from '@/tools/cloudflare/get_ruleset'
import { getRulesetEntrypointTool } from '@/tools/cloudflare/get_ruleset_entrypoint'
import { getTunnelTool } from '@/tools/cloudflare/get_tunnel'
import { getTunnelConfigurationTool } from '@/tools/cloudflare/get_tunnel_configuration'
import { getWorkerScriptSettingsTool } from '@/tools/cloudflare/get_worker_script_settings'
import { getZoneTool } from '@/tools/cloudflare/get_zone'
import { getZoneSettingsTool } from '@/tools/cloudflare/get_zone_settings'
import { listAccessApplicationsTool } from '@/tools/cloudflare/list_access_applications'
import { listAccessGroupsTool } from '@/tools/cloudflare/list_access_groups'
import { listAccessIdentityProvidersTool } from '@/tools/cloudflare/list_access_identity_providers'
import { listAccessPoliciesTool } from '@/tools/cloudflare/list_access_policies'
import { listAccessServiceTokensTool } from '@/tools/cloudflare/list_access_service_tokens'
import { listCertificatesTool } from '@/tools/cloudflare/list_certificates'
import { listDnsRecordsTool } from '@/tools/cloudflare/list_dns_records'
import { listManagedRulesetOverridesTool } from '@/tools/cloudflare/list_managed_ruleset_overrides'
import { listR2BucketsTool } from '@/tools/cloudflare/list_r2_buckets'
import { listRateLimitRulesTool } from '@/tools/cloudflare/list_rate_limit_rules'
import { listRulesetsTool } from '@/tools/cloudflare/list_rulesets'
import { listTunnelsTool } from '@/tools/cloudflare/list_tunnels'
import { listWorkerRoutesTool } from '@/tools/cloudflare/list_worker_routes'
import { listWorkerScriptsTool } from '@/tools/cloudflare/list_worker_scripts'
import { listZonesTool } from '@/tools/cloudflare/list_zones'
import { purgeCacheTool } from '@/tools/cloudflare/purge_cache'
import { revokeAccessServiceTokenTool } from '@/tools/cloudflare/revoke_access_service_token'
import { updateAccessApplicationTool } from '@/tools/cloudflare/update_access_application'
import { updateAccessPolicyTool } from '@/tools/cloudflare/update_access_policy'
import { updateDnsRecordTool } from '@/tools/cloudflare/update_dns_record'
import { updateRateLimitRuleTool } from '@/tools/cloudflare/update_rate_limit_rule'
import { updateRulesetRuleTool } from '@/tools/cloudflare/update_ruleset_rule'
import { updateZoneSettingTool } from '@/tools/cloudflare/update_zone_setting'
export const cloudflareCreateAccessApplicationTool = createAccessApplicationTool
export const cloudflareCreateAccessPolicyTool = createAccessPolicyTool
export const cloudflareCreateAccessServiceTokenTool = createAccessServiceTokenTool
export const cloudflareCreateDnsRecordTool = createDnsRecordTool
export const cloudflareCreateR2BucketTool = createR2BucketTool
export const cloudflareCreateRateLimitRuleTool = createRateLimitRuleTool
export const cloudflareCreateRulesetTool = createRulesetTool
export const cloudflareCreateRulesetRuleTool = createRulesetRuleTool
export const cloudflareCreateZoneTool = createZoneTool
export const cloudflareDeleteAccessApplicationTool = deleteAccessApplicationTool
export const cloudflareDeleteAccessPolicyTool = deleteAccessPolicyTool
export const cloudflareDeleteDnsRecordTool = deleteDnsRecordTool
export const cloudflareDeleteR2BucketTool = deleteR2BucketTool
export const cloudflareDeleteRulesetRuleTool = deleteRulesetRuleTool
export const cloudflareDeleteZoneTool = deleteZoneTool
export const cloudflareDnsAnalyticsTool = dnsAnalyticsTool
export const cloudflareGetAccessApplicationTool = getAccessApplicationTool
export const cloudflareGetR2BucketTool = getR2BucketTool
export const cloudflareGetRulesetTool = getRulesetTool
export const cloudflareGetRulesetEntrypointTool = getRulesetEntrypointTool
export const cloudflareGetTunnelTool = getTunnelTool
export const cloudflareGetTunnelConfigurationTool = getTunnelConfigurationTool
export const cloudflareGetWorkerScriptSettingsTool = getWorkerScriptSettingsTool
export const cloudflareGetZoneTool = getZoneTool
export const cloudflareGetZoneSettingsTool = getZoneSettingsTool
export const cloudflareListAccessApplicationsTool = listAccessApplicationsTool
export const cloudflareListAccessGroupsTool = listAccessGroupsTool
export const cloudflareListAccessIdentityProvidersTool = listAccessIdentityProvidersTool
export const cloudflareListAccessPoliciesTool = listAccessPoliciesTool
export const cloudflareListAccessServiceTokensTool = listAccessServiceTokensTool
export const cloudflareListCertificatesTool = listCertificatesTool
export const cloudflareListDnsRecordsTool = listDnsRecordsTool
export const cloudflareListManagedRulesetOverridesTool = listManagedRulesetOverridesTool
export const cloudflareListR2BucketsTool = listR2BucketsTool
export const cloudflareListRateLimitRulesTool = listRateLimitRulesTool
export const cloudflareListRulesetsTool = listRulesetsTool
export const cloudflareListTunnelsTool = listTunnelsTool
export const cloudflareListWorkerRoutesTool = listWorkerRoutesTool
export const cloudflareListWorkerScriptsTool = listWorkerScriptsTool
export const cloudflareListZonesTool = listZonesTool
export const cloudflarePurgeCacheTool = purgeCacheTool
export const cloudflareRevokeAccessServiceTokenTool = revokeAccessServiceTokenTool
export const cloudflareUpdateAccessApplicationTool = updateAccessApplicationTool
export const cloudflareUpdateAccessPolicyTool = updateAccessPolicyTool
export const cloudflareUpdateDnsRecordTool = updateDnsRecordTool
export const cloudflareUpdateRateLimitRuleTool = updateRateLimitRuleTool
export const cloudflareUpdateRulesetRuleTool = updateRulesetRuleTool
export const cloudflareUpdateZoneSettingTool = updateZoneSettingTool
@@ -0,0 +1,204 @@
import type {
CloudflareListAccessApplicationsParams,
CloudflareListAccessApplicationsResponse,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
mapAccessApplication,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listAccessApplicationsTool: ToolConfig<
CloudflareListAccessApplicationsParams,
CloudflareListAccessApplicationsResponse
> = {
id: 'cloudflare_list_access_applications',
name: 'Cloudflare List Access Applications',
description:
'Lists the Cloudflare Access (Zero Trust) applications protecting an account. Requires an API token with Account Access: Apps and Policies Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by application name',
},
domain: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by the primary hostname the application secures',
},
aud: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by application audience (AUD) tag',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Free-text search across applications',
},
exact: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the name and domain filters must match exactly',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of applications per page',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps`
)
appendParam(url, 'name', params.name)
appendParam(url, 'domain', params.domain)
appendParam(url, 'aud', params.aud)
appendParam(url, 'search', params.search)
if (params.exact !== undefined) url.searchParams.append('exact', String(params.exact))
appendParam(url, 'page', params.page)
appendParam(url, 'per_page', params.per_page)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: { applications: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Access applications'),
}
}
const applications = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
applications: applications.map(mapAccessApplication),
total_count: data.result_info?.total_count ?? applications.length,
},
}
},
outputs: {
applications: {
type: 'array',
description: 'Access applications in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Access application identifier' },
name: { type: 'string', description: 'Application name', optional: true },
domain: {
type: 'string',
description: 'Primary hostname and path secured by Access',
optional: true,
},
type: {
type: 'string',
description: 'Application type (e.g., self_hosted, saas, ssh, app_launcher, bookmark)',
optional: true,
},
aud: {
type: 'string',
description: 'Audience tag used to verify Access JWTs',
optional: true,
},
session_duration: {
type: 'string',
description: 'How long an Access session stays valid (e.g., 24h)',
optional: true,
},
allowed_idps: {
type: 'array',
description: 'Identity provider IDs users may authenticate with',
items: { type: 'string', description: 'Identity provider ID' },
optional: true,
},
app_launcher_visible: {
type: 'boolean',
description: 'Whether the app appears in the App Launcher',
optional: true,
},
auto_redirect_to_identity: {
type: 'boolean',
description: 'Whether users skip the identity provider picker',
optional: true,
},
custom_deny_message: {
type: 'string',
description: 'Message shown when access is denied',
optional: true,
},
custom_deny_url: {
type: 'string',
description: 'URL users are redirected to when access is denied',
optional: true,
},
logo_url: { type: 'string', description: 'Logo image URL', optional: true },
self_hosted_domains: {
type: 'array',
description:
'Additional hostnames and paths secured by the application. Cloudflare deprecated this field in favour of destinations, which is the one to read on a current application',
items: { type: 'string', description: 'Hostname and path' },
optional: true,
},
destinations: {
type: 'json',
description: 'Public and private destinations secured by the application',
optional: true,
},
tags: {
type: 'array',
description: 'Tags categorizing the application',
items: { type: 'string', description: 'Tag name' },
optional: true,
},
policies: {
type: 'json',
description: 'Access policies attached to the application',
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Total number of Access applications' },
},
}
@@ -0,0 +1,138 @@
import type {
CloudflareListAccessGroupsParams,
CloudflareListAccessGroupsResponse,
CloudflareRawAccessGroup,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listAccessGroupsTool: ToolConfig<
CloudflareListAccessGroupsParams,
CloudflareListAccessGroupsResponse
> = {
id: 'cloudflare_list_access_groups',
name: 'Cloudflare List Access Groups',
description:
'Lists the reusable Cloudflare Access (Zero Trust) groups in an account. Groups bundle identity rules that policies can reference by ID. Requires an API token with Account Access: Organizations, Identity Providers, and Groups Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access groups are account-scoped',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by group name',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Free-text search across groups',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of groups per page',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/groups`
)
appendParam(url, 'name', params.name)
appendParam(url, 'search', params.search)
appendParam(url, 'page', params.page)
appendParam(url, 'per_page', params.per_page)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawAccessGroup[]>(response)
if (!data.success) {
return {
success: false,
output: { groups: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Access groups'),
}
}
const groups = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
groups: groups.map((group) => ({
id: group.id ?? '',
name: group.name ?? null,
is_default: group.is_default ?? null,
include: group.include ?? null,
exclude: group.exclude ?? null,
require: group.require ?? null,
created_at: group.created_at ?? null,
updated_at: group.updated_at ?? null,
})),
total_count: data.result_info?.total_count ?? groups.length,
},
}
},
outputs: {
groups: {
type: 'array',
description: 'Access groups in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Access group identifier' },
name: { type: 'string', description: 'Group name', optional: true },
is_default: {
type: 'json',
description:
'Rules that place this group in every Access application by default. Cloudflare returns an array of rule objects here, not a boolean',
optional: true,
},
include: {
type: 'json',
description: 'Rules evaluated with OR logic',
optional: true,
},
exclude: { type: 'json', description: 'Rules evaluated with NOT logic', optional: true },
require: { type: 'json', description: 'Rules evaluated with AND logic', optional: true },
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
},
},
total_count: { type: 'number', description: 'Total number of Access groups' },
},
}
@@ -0,0 +1,112 @@
import type {
CloudflareListAccessIdentityProvidersParams,
CloudflareListAccessIdentityProvidersResponse,
CloudflareRawAccessIdentityProvider,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listAccessIdentityProvidersTool: ToolConfig<
CloudflareListAccessIdentityProvidersParams,
CloudflareListAccessIdentityProvidersResponse
> = {
id: 'cloudflare_list_access_identity_providers',
name: 'Cloudflare List Access Identity Providers',
description:
'Lists the identity providers configured for Cloudflare Access (Zero Trust) in an account, such as Okta, Entra ID, Google Workspace, or a one-time PIN. Use the returned IDs to restrict an application with allowed_idps. Requires an API token with Account Access: Organizations, Identity Providers, and Groups Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Identity providers are account-scoped',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/identity_providers`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawAccessIdentityProvider[]>(response)
if (!data.success) {
return {
success: false,
output: { identity_providers: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Access identity providers'),
}
}
const providers = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
identity_providers: providers.map((provider) => ({
id: provider.id ?? '',
name: provider.name ?? null,
type: provider.type ?? null,
read_only: provider.read_only ?? null,
config: provider.config ?? null,
scim_config: provider.scim_config ?? null,
})),
total_count: data.result_info?.total_count ?? providers.length,
},
}
},
outputs: {
identity_providers: {
type: 'array',
description: 'Identity providers configured for Access',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Identity provider identifier' },
name: {
type: 'string',
description: 'Display name shown to users on the login page',
optional: true,
},
type: {
type: 'string',
description: 'Provider type, e.g. azureAD, okta, google, saml, oidc, or onetimepin',
optional: true,
},
read_only: {
type: 'boolean',
description: 'Whether the provider is immutable through the API',
optional: true,
},
config: {
type: 'json',
description: 'Provider-specific configuration parameters',
optional: true,
},
scim_config: {
type: 'json',
description: 'SCIM user and group provisioning configuration',
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Total number of identity providers' },
},
}
@@ -0,0 +1,157 @@
import type {
CloudflareListAccessPoliciesParams,
CloudflareListAccessPoliciesResponse,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
mapAccessPolicy,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listAccessPoliciesTool: ToolConfig<
CloudflareListAccessPoliciesParams,
CloudflareListAccessPoliciesResponse
> = {
id: 'cloudflare_list_access_policies',
name: 'Cloudflare List Access Policies',
description:
'Lists the Cloudflare Access (Zero Trust) policies attached to an application, in precedence order. Requires an API token with Account Access: Apps and Policies Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID whose policies should be listed',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of policies per page',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}/policies`
)
appendParam(url, 'page', params.page)
appendParam(url, 'per_page', params.per_page)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: { policies: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Access policies'),
}
}
const policies = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
policies: policies.map(mapAccessPolicy),
total_count: data.result_info?.total_count ?? policies.length,
},
}
},
outputs: {
policies: {
type: 'array',
description: 'Access policies attached to the application',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Policy identifier' },
name: { type: 'string', description: 'Policy name', optional: true },
decision: {
type: 'string',
description: 'Decision the policy applies: allow, deny, non_identity, or bypass',
optional: true,
},
precedence: {
type: 'number',
description: 'Evaluation order of the policy within the application',
optional: true,
},
include: {
type: 'json',
description: 'Rules evaluated with OR logic — matching any one selects the policy',
optional: true,
},
exclude: {
type: 'json',
description: 'Rules evaluated with NOT logic — matching any one rejects the request',
optional: true,
},
require: {
type: 'json',
description: 'Rules evaluated with AND logic — all must match',
optional: true,
},
session_duration: {
type: 'string',
description: 'How long a session granted by this policy stays valid',
optional: true,
},
approval_required: {
type: 'boolean',
description: 'Whether an approver must grant each access request',
optional: true,
},
isolation_required: {
type: 'boolean',
description: 'Whether the session must run in a remote browser',
optional: true,
},
purpose_justification_required: {
type: 'boolean',
description: 'Whether users must state a reason for access',
optional: true,
},
purpose_justification_prompt: {
type: 'string',
description: 'Prompt shown when a justification is required',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
},
},
total_count: { type: 'number', description: 'Total number of policies' },
},
}
@@ -0,0 +1,143 @@
import type {
CloudflareListAccessServiceTokensParams,
CloudflareListAccessServiceTokensResponse,
CloudflareRawAccessServiceToken,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listAccessServiceTokensTool: ToolConfig<
CloudflareListAccessServiceTokensParams,
CloudflareListAccessServiceTokensResponse
> = {
id: 'cloudflare_list_access_service_tokens',
name: 'Cloudflare List Access Service Tokens',
description:
'Lists the Cloudflare Access (Zero Trust) service tokens in an account, which let machines authenticate to Access-protected applications. Client secrets are never returned by this endpoint — only on creation. Requires an API token with Account Access: Service Tokens Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Service tokens are account-scoped',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by service token name',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Free-text search across service tokens',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of service tokens per page',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/service_tokens`
)
appendParam(url, 'name', params.name)
appendParam(url, 'search', params.search)
appendParam(url, 'page', params.page)
appendParam(url, 'per_page', params.per_page)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawAccessServiceToken[]>(response)
if (!data.success) {
return {
success: false,
output: { service_tokens: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Access service tokens'),
}
}
const tokens = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
service_tokens: tokens.map((token) => ({
id: token.id ?? '',
name: token.name ?? null,
client_id: token.client_id ?? null,
duration: token.duration ?? null,
enabled: token.enabled ?? null,
expires_at: token.expires_at ?? null,
last_seen_at: token.last_seen_at ?? null,
created_at: token.created_at ?? null,
updated_at: token.updated_at ?? null,
})),
total_count: data.result_info?.total_count ?? tokens.length,
},
}
},
outputs: {
service_tokens: {
type: 'array',
description: 'Access service tokens in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Service token identifier' },
name: { type: 'string', description: 'Service token name', optional: true },
client_id: {
type: 'string',
description: 'Client ID sent in the CF-Access-Client-Id header',
optional: true,
},
duration: {
type: 'string',
description: 'How long the token stays valid before it expires',
optional: true,
},
enabled: { type: 'boolean', description: 'Whether the token is active', optional: true },
expires_at: { type: 'string', description: 'Expiry timestamp', optional: true },
last_seen_at: {
type: 'string',
description: 'When the token was last used',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
},
},
total_count: { type: 'number', description: 'Total number of service tokens' },
},
}
+11 -8
View File
@@ -1,7 +1,9 @@
import type {
CloudflareListCertificatesParams,
CloudflareListCertificatesResponse,
CloudflareRawCertificatePack,
} from '@/tools/cloudflare/types'
import { readCloudflareResponse } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listCertificatesTool: ToolConfig<
@@ -24,7 +26,8 @@ export const listCertificatesTool: ToolConfig<
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter certificate packs by status (e.g., "all", "active", "pending")',
description:
'Set to "all" to include every certificate pack regardless of status. Cloudflare documents no other value for this filter; omitting it returns only active packs',
},
page: {
type: 'number',
@@ -55,7 +58,7 @@ export const listCertificatesTool: ToolConfig<
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/ssl/certificate_packs`
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/ssl/certificate_packs`
)
if (params.status) url.searchParams.append('status', params.status)
if (params.page) url.searchParams.append('page', String(params.page))
@@ -71,7 +74,7 @@ export const listCertificatesTool: ToolConfig<
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await readCloudflareResponse<CloudflareRawCertificatePack[]>(response)
if (!data.success) {
return {
@@ -85,14 +88,14 @@ export const listCertificatesTool: ToolConfig<
success: true,
output: {
certificates:
data.result?.map((cert: any) => ({
data.result?.map((cert) => ({
id: cert.id ?? '',
type: cert.type ?? '',
hosts: cert.hosts ?? [],
primary_certificate: cert.primary_certificate ?? '',
status: cert.status ?? '',
certificates:
cert.certificates?.map((c: any) => ({
cert.certificates?.map((c) => ({
id: c.id ?? '',
hosts: c.hosts ?? [],
issuer: c.issuer ?? '',
@@ -111,11 +114,11 @@ export const listCertificatesTool: ToolConfig<
validity_days: cert.validity_days ?? 0,
certificate_authority: cert.certificate_authority ?? '',
validation_errors:
cert.validation_errors?.map((e: any) => ({
cert.validation_errors?.map((e) => ({
message: e.message ?? '',
})) ?? [],
validation_records:
cert.validation_records?.map((r: any) => ({
cert.validation_records?.map((r) => ({
cname: r.cname ?? '',
cname_target: r.cname_target ?? '',
emails: r.emails ?? [],
@@ -126,7 +129,7 @@ export const listCertificatesTool: ToolConfig<
txt_value: r.txt_value ?? '',
})) ?? [],
dcv_delegation_records:
cert.dcv_delegation_records?.map((r: any) => ({
cert.dcv_delegation_records?.map((r) => ({
cname: r.cname ?? '',
cname_target: r.cname_target ?? '',
emails: r.emails ?? [],
@@ -1,7 +1,9 @@
import type {
CloudflareListDnsRecordsParams,
CloudflareListDnsRecordsResponse,
CloudflareRawDnsRecord,
} from '@/tools/cloudflare/types'
import { readCloudflareResponse } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listDnsRecordsTool: ToolConfig<
@@ -109,7 +111,9 @@ export const listDnsRecordsTool: ToolConfig<
request: {
url: (params) => {
const url = new URL(`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records`)
const url = new URL(
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/dns_records`
)
if (params.type) url.searchParams.append('type', params.type)
if (params.name) url.searchParams.append('name.exact', params.name)
if (params.content) url.searchParams.append('content.exact', params.content)
@@ -133,7 +137,7 @@ export const listDnsRecordsTool: ToolConfig<
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await readCloudflareResponse<CloudflareRawDnsRecord[]>(response)
if (!data.success) {
return {
@@ -147,7 +151,7 @@ export const listDnsRecordsTool: ToolConfig<
success: true,
output: {
records:
data.result?.map((record: any) => ({
data.result?.map((record) => ({
id: record.id ?? '',
zone_id: record.zone_id ?? '',
zone_name: record.zone_name ?? '',
@@ -0,0 +1,116 @@
import type {
CloudflareListManagedRulesetOverridesParams,
CloudflareListManagedRulesetOverridesResponse,
CloudflareRawRuleset,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listManagedRulesetOverridesTool: ToolConfig<
CloudflareListManagedRulesetOverridesParams,
CloudflareListManagedRulesetOverridesResponse
> = {
id: 'cloudflare_list_managed_ruleset_overrides',
name: 'Cloudflare List Managed Ruleset Overrides',
description:
'Lists the WAF managed rulesets deployed on a zone together with the overrides applied to each one. Cloudflare has no dedicated overrides endpoint — overrides live on the "execute" rules of the http_request_firewall_managed phase entry point ruleset, which this reads. Requires an API token with Zone WAF Read.',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID to read managed ruleset deployments and overrides for',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/phases/http_request_firewall_managed/entrypoint`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawRuleset>(response)
if (!data.success) {
return {
success: false,
output: { ruleset_id: '', deployments: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list managed ruleset overrides'),
}
}
const rules = Array.isArray(data.result?.rules) ? data.result.rules : []
const deployments = rules
.filter((rule) => rule.action === 'execute')
.map((rule) => ({
rule_id: rule.id ?? '',
managed_ruleset_id: rule.action_parameters?.id ?? null,
description: rule.description ?? '',
expression: rule.expression ?? '',
enabled: rule.enabled ?? false,
overrides: rule.action_parameters?.overrides ?? null,
}))
return {
success: true,
output: {
ruleset_id: data.result?.id ?? '',
deployments,
total_count: deployments.length,
},
}
},
outputs: {
ruleset_id: {
type: 'string',
description:
'Ruleset ID of the http_request_firewall_managed entry point, needed to edit a deployment rule',
},
deployments: {
type: 'array',
description: 'Managed rulesets deployed on the zone and the overrides applied to each',
items: {
type: 'object',
properties: {
rule_id: {
type: 'string',
description: 'ID of the execute rule that deploys the managed ruleset',
},
managed_ruleset_id: {
type: 'string',
description: 'ID of the deployed managed ruleset',
optional: true,
},
description: { type: 'string', description: 'Description of the deployment rule' },
expression: {
type: 'string',
description: 'Filter expression scoping which requests the managed ruleset runs on',
},
enabled: { type: 'boolean', description: 'Whether the deployment is enabled' },
overrides: {
type: 'json',
description:
'Overrides applied to the managed ruleset, at three levels. Cloudflare documents action, enabled, and sensitivity_level at the ruleset (top) level; category, action, enabled, and sensitivity_level per category; and id, action, enabled, score_threshold, and sensitivity_level per rule. Rule overrides beat category overrides, which beat the ruleset-level override. sensitivity_level applies only to the DDoS phases, so for a WAF managed ruleset the rule-level properties are action, enabled, and score_threshold',
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Number of managed ruleset deployments found' },
},
}
@@ -0,0 +1,160 @@
import type {
CloudflareListR2BucketsParams,
CloudflareListR2BucketsResponse,
CloudflareRawR2Bucket,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listR2BucketsTool: ToolConfig<
CloudflareListR2BucketsParams,
CloudflareListR2BucketsResponse
> = {
id: 'cloudflare_list_r2_buckets',
name: 'Cloudflare List R2 Buckets',
description:
'Lists the R2 object storage buckets in an account. Requires an API token with Account Workers R2 Storage Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. R2 buckets are account-scoped',
},
name_contains: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return buckets whose name contains this substring',
},
start_after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Bucket name to start listing after',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned by a previous call',
},
direction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort direction by bucket name: asc or desc',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of buckets per page',
},
jurisdiction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Data-residency jurisdiction to list within: default, eu, or fedramp',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/r2/buckets`
)
appendParam(url, 'name_contains', params.name_contains)
appendParam(url, 'start_after', params.start_after)
appendParam(url, 'cursor', params.cursor)
appendParam(url, 'direction', params.direction)
appendParam(url, 'per_page', params.per_page)
// `direction` only means something alongside an ordering field, and `name`
// is the sole value Cloudflare documents for `order`.
if (params.direction) url.searchParams.append('order', 'name')
return url.toString()
},
method: 'GET',
headers: (params) => {
const headers = cloudflareHeaders(params.apiKey)
if (params.jurisdiction) headers['cf-r2-jurisdiction'] = params.jurisdiction
return headers
},
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<{ buckets?: CloudflareRawR2Bucket[] }>(response)
if (!data.success) {
return {
success: false,
output: { buckets: [], cursor: null },
error: cloudflareErrorMessage(data, 'Failed to list R2 buckets'),
}
}
const buckets = Array.isArray(data.result?.buckets) ? data.result.buckets : []
return {
success: true,
output: {
buckets: buckets.map((bucket) => ({
name: bucket.name ?? '',
creation_date: bucket.creation_date ?? null,
location: bucket.location ?? null,
storage_class: bucket.storage_class ?? null,
jurisdiction: bucket.jurisdiction ?? null,
})),
cursor: data.result_info?.cursor ?? null,
},
}
},
outputs: {
buckets: {
type: 'array',
description: 'R2 buckets in the account',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Bucket name' },
creation_date: { type: 'string', description: 'Creation timestamp', optional: true },
location: {
type: 'string',
description:
'Location hint the bucket was created with (apac, eeur, enam, weur, wnam, or oc)',
optional: true,
},
storage_class: {
type: 'string',
description: 'Default storage class (Standard or InfrequentAccess)',
optional: true,
},
jurisdiction: {
type: 'string',
description: 'Data-residency jurisdiction (default, eu, or fedramp)',
optional: true,
},
},
},
},
cursor: {
type: 'string',
description: 'Pagination cursor to pass to the next call',
optional: true,
},
},
}
@@ -0,0 +1,119 @@
import type {
CloudflareListRateLimitRulesParams,
CloudflareRulesetResponse,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listRateLimitRulesTool: ToolConfig<
CloudflareListRateLimitRulesParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_list_rate_limit_rules',
name: 'Cloudflare List Rate Limiting Rules',
description:
'Lists the rate limiting rules on a zone by reading the http_ratelimit phase entry point ruleset. This uses the current Rulesets-based rate limiting API; the legacy rate_limits endpoint is no longer available. The returned ruleset ID is what "Create Rate Limiting Rule", "Update Rate Limiting Rule", and "Delete Ruleset Rule" need. Requires an API token with Zone WAF Read.',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID to list rate limiting rules for',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/phases/http_ratelimit/entrypoint`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to list rate limiting rules'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: {
type: 'string',
description: 'Ruleset ID of the http_ratelimit entry point, needed to create or edit rules',
},
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind' },
phase: { type: 'string', description: 'Phase the ruleset runs in (http_ratelimit)' },
version: { type: 'string', description: 'Ruleset version', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rate limiting rules, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: {
type: 'string',
description: 'Action applied once the rate limit is exceeded',
},
action_parameters: {
type: 'json',
description: 'Action-specific parameters, such as a custom block response',
optional: true,
},
expression: {
type: 'string',
description: 'Filter expression selecting the requests the rule applies to',
},
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: { type: 'string', description: 'Rule reference tag', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description:
'Rate limiting configuration (characteristics, period, requests_per_period, mitigation_timeout, counting_expression, requests_to_origin)',
optional: true,
},
},
},
},
},
}
+131
View File
@@ -0,0 +1,131 @@
import type {
CloudflareListRulesetsParams,
CloudflareListRulesetsResponse,
CloudflareRawRuleset,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listRulesetsTool: ToolConfig<
CloudflareListRulesetsParams,
CloudflareListRulesetsResponse
> = {
id: 'cloudflare_list_rulesets',
name: 'Cloudflare List Rulesets',
description:
'Lists every ruleset defined on a zone across all phases (WAF custom rules, managed rules, rate limiting, transform rules, and more). The list response deliberately omits the rules inside each ruleset — use "Get Ruleset" to read them. Requires an API token with Zone WAF Read (or another matching ruleset Read permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID to list rulesets for',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of rulesets to return per page',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Cursor for the next page, taken from the cursor output of a previous call. This endpoint paginates by cursor, not by page number',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets`
)
appendParam(url, 'per_page', params.per_page)
appendParam(url, 'cursor', params.cursor)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawRuleset[]>(response)
if (!data.success) {
return {
success: false,
output: { rulesets: [], total_count: 0, cursor: null },
error: cloudflareErrorMessage(data, 'Failed to list rulesets'),
}
}
const rulesets = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
rulesets: rulesets.map((ruleset) => ({
id: ruleset.id ?? '',
name: ruleset.name ?? '',
description: ruleset.description ?? '',
kind: ruleset.kind ?? '',
phase: ruleset.phase ?? '',
version: ruleset.version ?? null,
last_updated: ruleset.last_updated ?? null,
})),
total_count: rulesets.length,
cursor: data.result_info?.cursors?.after ?? null,
},
}
},
outputs: {
rulesets: {
type: 'array',
description: 'Rulesets defined on the zone',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: {
type: 'string',
description: 'Ruleset kind (managed, custom, root, or zone)',
},
phase: {
type: 'string',
description:
'Phase the ruleset runs in (e.g., http_request_firewall_custom, http_request_firewall_managed, http_ratelimit)',
},
version: { type: 'string', description: 'Ruleset version', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Number of rulesets returned on this page' },
cursor: {
type: 'string',
description: 'Cursor to pass to the next call to read the following page, when more remain',
optional: true,
},
},
}
+226
View File
@@ -0,0 +1,226 @@
import type {
CloudflareListTunnelsParams,
CloudflareListTunnelsResponse,
CloudflareRawTunnel,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listTunnelsTool: ToolConfig<
CloudflareListTunnelsParams,
CloudflareListTunnelsResponse
> = {
id: 'cloudflare_list_tunnels',
name: 'Cloudflare List Tunnels',
description:
'Lists the Cloudflare Tunnels (cloudflared) in an account, with their health status and active connections. Requires an API token with Account Cloudflare Tunnel Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Tunnels are account-scoped',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by exact tunnel name',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by tunnel health: inactive, degraded, healthy, or down',
},
uuid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by tunnel UUID',
},
is_deleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to return deleted tunnels instead of active ones',
},
include_prefix: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only include tunnels whose name starts with this prefix',
},
exclude_prefix: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Exclude tunnels whose name starts with this prefix',
},
existed_at: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return tunnels that existed at this RFC 3339 timestamp',
},
was_active_at: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return tunnels that were active at this RFC 3339 timestamp',
},
was_inactive_at: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return tunnels that were inactive at this RFC 3339 timestamp',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of tunnels per page',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/cfd_tunnel`
)
appendParam(url, 'name', params.name)
appendParam(url, 'status', params.status)
appendParam(url, 'uuid', params.uuid)
if (params.is_deleted !== undefined) {
url.searchParams.append('is_deleted', String(params.is_deleted))
}
appendParam(url, 'include_prefix', params.include_prefix)
appendParam(url, 'exclude_prefix', params.exclude_prefix)
appendParam(url, 'existed_at', params.existed_at)
appendParam(url, 'was_active_at', params.was_active_at)
appendParam(url, 'was_inactive_at', params.was_inactive_at)
appendParam(url, 'page', params.page)
appendParam(url, 'per_page', params.per_page)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawTunnel[]>(response)
if (!data.success) {
return {
success: false,
output: { tunnels: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list tunnels'),
}
}
const tunnels = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
tunnels: tunnels.map((tunnel) => ({
id: tunnel.id ?? '',
name: tunnel.name ?? null,
account_tag: tunnel.account_tag ?? null,
config_src: tunnel.config_src ?? null,
status: tunnel.status ?? null,
tun_type: tunnel.tun_type ?? null,
remote_config: tunnel.remote_config ?? null,
metadata: tunnel.metadata ?? null,
created_at: tunnel.created_at ?? null,
deleted_at: tunnel.deleted_at ?? null,
conns_active_at: tunnel.conns_active_at ?? null,
conns_inactive_at: tunnel.conns_inactive_at ?? null,
connections: tunnel.connections ?? null,
})),
total_count: data.result_info?.total_count ?? tunnels.length,
},
}
},
outputs: {
tunnels: {
type: 'array',
description: 'Cloudflare Tunnels in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tunnel identifier' },
name: { type: 'string', description: 'Tunnel name', optional: true },
account_tag: {
type: 'string',
description: 'Account the tunnel belongs to',
optional: true,
},
config_src: {
type: 'string',
description: 'Where the tunnel configuration lives: local or cloudflare',
optional: true,
},
status: {
type: 'string',
description: 'Tunnel health: inactive, degraded, healthy, or down',
optional: true,
},
tun_type: {
type: 'string',
description: 'Tunnel type, e.g. cfd_tunnel, warp_connector, or warp',
optional: true,
},
remote_config: {
type: 'boolean',
description: 'Whether the tunnel is remotely managed',
optional: true,
},
metadata: {
type: 'json',
description: 'Metadata associated with the tunnel',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
deleted_at: { type: 'string', description: 'Deletion timestamp', optional: true },
conns_active_at: {
type: 'string',
description: 'When the tunnel last had active connections',
optional: true,
},
conns_inactive_at: {
type: 'string',
description: 'When the tunnel last lost all connections',
optional: true,
},
connections: {
type: 'json',
description: 'Active connector connections for the tunnel',
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Total number of tunnels' },
},
}
@@ -0,0 +1,94 @@
import type {
CloudflareListWorkerRoutesParams,
CloudflareListWorkerRoutesResponse,
CloudflareRawWorkerRoute,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listWorkerRoutesTool: ToolConfig<
CloudflareListWorkerRoutesParams,
CloudflareListWorkerRoutesResponse
> = {
id: 'cloudflare_list_worker_routes',
name: 'Cloudflare List Worker Routes',
description:
'Lists the Workers routes on a zone, showing which URL patterns are handled by which Worker script. Unlike the Workers script endpoints, routes are zone-scoped. Requires an API token with Zone Workers Routes Read.',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The zone ID to list Workers routes for. Routes are zone-scoped, not account-scoped',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/workers/routes`,
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawWorkerRoute[]>(response)
if (!data.success) {
return {
success: false,
output: { routes: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Worker routes'),
}
}
const routes = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
routes: routes.map((route) => ({
id: route.id ?? '',
pattern: route.pattern ?? '',
script: route.script ?? null,
})),
total_count: routes.length,
},
}
},
outputs: {
routes: {
type: 'array',
description: 'Workers routes on the zone',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Route identifier' },
pattern: {
type: 'string',
description: 'URL pattern the route matches, e.g. example.com/*',
},
script: {
type: 'string',
description: 'Name of the Workers script handling the route',
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Number of routes returned' },
},
}
@@ -0,0 +1,158 @@
import type {
CloudflareListWorkerScriptsParams,
CloudflareListWorkerScriptsResponse,
CloudflareRawWorkerScript,
} from '@/tools/cloudflare/types'
import {
appendParam,
cloudflareErrorMessage,
cloudflareHeaders,
readCloudflareResponse,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listWorkerScriptsTool: ToolConfig<
CloudflareListWorkerScriptsParams,
CloudflareListWorkerScriptsResponse
> = {
id: 'cloudflare_list_worker_scripts',
name: 'Cloudflare List Worker Scripts',
description:
'Lists the Workers scripts deployed in an account. Requires an API token with Account Workers Scripts Read.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Workers scripts are account-scoped',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter scripts by tag. Cloudflare expects a comma-separated list of tag:allowed pairs where allowed is yes or no, e.g. team:core:yes,deprecated:no',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) => {
const url = new URL(
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/workers/scripts`
)
appendParam(url, 'tags', params.tags)
return url.toString()
},
method: 'GET',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await readCloudflareResponse<CloudflareRawWorkerScript[]>(response)
if (!data.success) {
return {
success: false,
output: { scripts: [], total_count: 0 },
error: cloudflareErrorMessage(data, 'Failed to list Worker scripts'),
}
}
const scripts = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
scripts: scripts.map((script) => ({
id: script.id ?? '',
tag: script.tag ?? null,
etag: script.etag ?? null,
created_on: script.created_on ?? null,
modified_on: script.modified_on ?? null,
usage_model: script.usage_model ?? null,
placement_mode: script.placement_mode ?? null,
logpush: script.logpush ?? null,
has_assets: script.has_assets ?? null,
has_modules: script.has_modules ?? null,
compatibility_date: script.compatibility_date ?? null,
compatibility_flags: script.compatibility_flags ?? null,
routes: script.routes ?? null,
tail_consumers: script.tail_consumers ?? null,
})),
total_count: scripts.length,
},
}
},
outputs: {
scripts: {
type: 'array',
description: 'Workers scripts in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Script name' },
tag: {
type: 'string',
description: 'Immutable script identifier, distinct from the script name',
optional: true,
},
etag: { type: 'string', description: 'Hash of the script content', optional: true },
created_on: { type: 'string', description: 'Creation timestamp', optional: true },
modified_on: { type: 'string', description: 'Last deployment timestamp', optional: true },
usage_model: {
type: 'string',
description: 'Billing usage model (standard, bundled, or unbound)',
optional: true,
},
placement_mode: {
type: 'string',
description: 'Smart placement mode (smart or targeted)',
optional: true,
},
logpush: {
type: 'boolean',
description: 'Whether Workers Logpush is enabled',
optional: true,
},
has_assets: {
type: 'boolean',
description: 'Whether the script ships static assets',
optional: true,
},
has_modules: {
type: 'boolean',
description: 'Whether the script uses ES modules',
optional: true,
},
compatibility_date: {
type: 'string',
description: 'Workers runtime compatibility date',
optional: true,
},
compatibility_flags: {
type: 'array',
description: 'Workers runtime compatibility flags',
items: { type: 'string', description: 'Compatibility flag' },
optional: true,
},
routes: { type: 'json', description: 'Routes the script is bound to', optional: true },
tail_consumers: {
type: 'json',
description: "Workers that consume this script's tail events",
optional: true,
},
},
},
},
total_count: { type: 'number', description: 'Number of scripts returned' },
},
}
+4 -2
View File
@@ -1,7 +1,9 @@
import type {
CloudflareListZonesParams,
CloudflareListZonesResponse,
CloudflareRawZone,
} from '@/tools/cloudflare/types'
import { readCloudflareResponse } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const listZonesTool: ToolConfig<CloudflareListZonesParams, CloudflareListZonesResponse> = {
@@ -88,7 +90,7 @@ export const listZonesTool: ToolConfig<CloudflareListZonesParams, CloudflareList
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await readCloudflareResponse<CloudflareRawZone[]>(response)
if (!data.success) {
return {
@@ -102,7 +104,7 @@ export const listZonesTool: ToolConfig<CloudflareListZonesParams, CloudflareList
success: true,
output: {
zones:
data.result?.map((zone: any) => ({
data.result?.map((zone) => ({
id: zone.id ?? '',
name: zone.name ?? '',
status: zone.status ?? '',
+16 -1
View File
@@ -59,7 +59,8 @@ export const purgeCacheTool: ToolConfig<CloudflarePurgeCacheParams, CloudflarePu
},
request: {
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}/purge_cache`,
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/purge_cache`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -106,6 +107,20 @@ export const purgeCacheTool: ToolConfig<CloudflarePurgeCacheParams, CloudflarePu
)
}
/**
* Cloudflare's purge body is a one-of over the five target kinds — each
* is its own request schema, and combining two in a single call is not a
* documented shape. Rejecting here names the conflicting fields instead
* of letting the API answer with a generic parse error.
* https://developers.cloudflare.com/api/resources/cache/methods/purge/
*/
const targets = Object.keys(body)
if (targets.length > 1) {
throw new Error(
`Only one purge target kind is allowed per request, but ${targets.join(' and ')} were provided. Run a separate purge for each.`
)
}
return body
},
},
@@ -0,0 +1,99 @@
import type {
CloudflareAccessServiceTokenResponse,
CloudflareRevokeAccessServiceTokenParams,
} from '@/tools/cloudflare/types'
import { cloudflareErrorMessage, cloudflareHeaders } from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const revokeAccessServiceTokenTool: ToolConfig<
CloudflareRevokeAccessServiceTokenParams,
CloudflareAccessServiceTokenResponse
> = {
id: 'cloudflare_revoke_access_service_token',
name: 'Cloudflare Revoke Access Service Token',
description:
'Permanently deletes a Cloudflare Access (Zero Trust) service token, revoking it. Every machine or integration still presenting that client ID and secret is locked out of the Access-protected applications immediately, and the secret cannot be recovered. This cannot be undone. Requires an API token with Account Access: Service Tokens Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Service tokens are account-scoped',
},
serviceTokenId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The service token ID to revoke permanently',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/service_tokens/${params.serviceTokenId.trim()}`,
method: 'DELETE',
headers: (params) => cloudflareHeaders(params.apiKey),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
id: '',
name: null,
client_id: null,
duration: null,
enabled: null,
expires_at: null,
last_seen_at: null,
created_at: null,
updated_at: null,
},
error: cloudflareErrorMessage(data, 'Failed to revoke Access service token'),
}
}
const token = data.result
return {
success: true,
output: {
id: token?.id ?? '',
name: token?.name ?? null,
client_id: token?.client_id ?? null,
duration: token?.duration ?? null,
enabled: token?.enabled ?? null,
expires_at: token?.expires_at ?? null,
last_seen_at: token?.last_seen_at ?? null,
created_at: token?.created_at ?? null,
updated_at: token?.updated_at ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Identifier of the revoked service token' },
name: { type: 'string', description: 'Service token name', optional: true },
client_id: {
type: 'string',
description: 'Client ID that is no longer accepted',
optional: true,
},
duration: { type: 'string', description: 'Configured token lifetime', optional: true },
enabled: { type: 'boolean', description: 'Whether the token was active', optional: true },
expires_at: { type: 'string', description: 'Expiry timestamp', optional: true },
last_seen_at: { type: 'string', description: 'When the token was last used', optional: true },
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
}
+906 -1
View File
@@ -4,6 +4,327 @@ interface CloudflareBaseParams {
apiKey: string
}
/**
* Pagination metadata Cloudflare attaches to list endpoints. Page-based
* endpoints populate `page`/`per_page`/`total_count`; the Rulesets engine and
* R2 use cursors instead, so every field is optional.
*/
export interface CloudflareResultInfo {
page?: number
per_page?: number
count?: number
total_count?: number
cursor?: string
cursors?: {
before?: string
after?: string
}
}
/**
* The Cloudflare v4 response envelope. Every endpoint answers with this shape,
* and a `200 OK` carrying `success: false` is a failure — callers must branch
* on `success` rather than the HTTP status. `result` is absent on failures.
*/
export interface CloudflareEnvelope<TResult = unknown> {
success?: boolean
errors?: Array<{ code?: number; message?: string }>
messages?: unknown[]
result?: TResult
result_info?: CloudflareResultInfo
}
/** Raw rule payload inside a ruleset, as returned by the Rulesets API. */
export interface CloudflareRawRule {
id?: string
version?: string
action?: string
action_parameters?: {
id?: string
overrides?: Record<string, unknown>
[key: string]: unknown
}
expression?: string
description?: string
enabled?: boolean
ref?: string
last_updated?: string
categories?: string[]
logging?: Record<string, unknown>
ratelimit?: Record<string, unknown>
}
/** Raw ruleset payload. `rules` is omitted by the list-rulesets endpoint. */
export interface CloudflareRawRuleset {
id?: string
name?: string
description?: string
kind?: string
phase?: string
version?: string
last_updated?: string
rules?: CloudflareRawRule[]
}
/** Raw Cloudflare Access application payload. */
export interface CloudflareRawAccessApplication {
id?: string
name?: string
domain?: string
type?: string
aud?: string
session_duration?: string
allowed_idps?: string[]
app_launcher_visible?: boolean
auto_redirect_to_identity?: boolean
custom_deny_message?: string
custom_deny_url?: string
logo_url?: string
self_hosted_domains?: string[]
destinations?: unknown[]
tags?: string[]
policies?: unknown[]
}
/** Raw Cloudflare Access policy payload. */
export interface CloudflareRawAccessPolicy {
id?: string
name?: string
decision?: string
precedence?: number
include?: unknown[]
exclude?: unknown[]
require?: unknown[]
session_duration?: string
approval_required?: boolean
isolation_required?: boolean
purpose_justification_required?: boolean
purpose_justification_prompt?: string
created_at?: string
updated_at?: string
}
/** Raw Cloudflare Access group payload. */
export interface CloudflareRawAccessGroup {
id?: string
name?: string
is_default?: unknown[]
include?: unknown[]
exclude?: unknown[]
require?: unknown[]
created_at?: string
updated_at?: string
}
/** Raw Cloudflare Access identity provider payload. */
export interface CloudflareRawAccessIdentityProvider {
id?: string
name?: string
type?: string
read_only?: boolean
config?: Record<string, unknown>
scim_config?: Record<string, unknown>
}
/** Raw Cloudflare Access service token payload. `client_secret` only on create. */
export interface CloudflareRawAccessServiceToken {
id?: string
name?: string
client_id?: string
client_secret?: string
duration?: string
enabled?: boolean
expires_at?: string
last_seen_at?: string
created_at?: string
updated_at?: string
}
/** Raw R2 bucket payload. */
export interface CloudflareRawR2Bucket {
name?: string
creation_date?: string
location?: string
storage_class?: string
jurisdiction?: string
}
/** Raw Workers script payload from the list-scripts endpoint. */
export interface CloudflareRawWorkerScript {
id?: string
tag?: string
etag?: string
created_on?: string
modified_on?: string
usage_model?: string
placement_mode?: string
logpush?: boolean
has_assets?: boolean
has_modules?: boolean
compatibility_date?: string
compatibility_flags?: string[]
routes?: unknown[]
tail_consumers?: unknown[]
}
/** Raw Workers route payload. */
export interface CloudflareRawWorkerRoute {
id?: string
pattern?: string
script?: string
}
/** Raw Cloudflare Tunnel (cloudflared) payload. */
export interface CloudflareRawTunnel {
id?: string
name?: string
account_tag?: string
config_src?: string
status?: string
tun_type?: string
remote_config?: boolean
metadata?: Record<string, unknown>
created_at?: string
deleted_at?: string
conns_active_at?: string
conns_inactive_at?: string
connections?: unknown[]
}
/** Raw zone payload from the zones endpoints. */
export interface CloudflareRawZone {
id?: string
name?: string
status?: string
paused?: boolean
type?: string
name_servers?: string[]
original_name_servers?: string[]
created_on?: string
modified_on?: string
activated_on?: string
development_mode?: number
plan?: {
id?: string
name?: string
price?: number
is_subscribed?: boolean
frequency?: string
currency?: string
legacy_id?: string
}
account?: { id?: string; name?: string }
owner?: { id?: string; name?: string; type?: string }
meta?: {
cdn_only?: boolean
custom_certificate_quota?: number
dns_only?: boolean
foundation_dns?: boolean
page_rule_quota?: number
phishing_detected?: boolean
step?: number
}
vanity_name_servers?: string[]
permissions?: string[]
}
/** Raw DNS record payload. */
export interface CloudflareRawDnsRecord {
id?: string
zone_id?: string
zone_name?: string
type?: string
name?: string
content?: string
proxiable?: boolean
proxied?: boolean
ttl?: number
locked?: boolean
priority?: number
comment?: string
tags?: string[]
comment_modified_on?: string
tags_modified_on?: string
meta?: CloudflareDnsRecordMeta
created_on?: string
modified_on?: string
}
/** Raw hostname-validation record shared by certificate pack validation fields. */
export interface CloudflareRawValidationRecord {
cname?: string
cname_target?: string
emails?: string[]
http_body?: string
http_url?: string
status?: string
txt_name?: string
txt_value?: string
}
/** Raw certificate inside a certificate pack. */
export interface CloudflareRawCertificate {
id?: string
hosts?: string[]
issuer?: string
signature?: string
status?: string
bundle_method?: string
zone_id?: string
uploaded_on?: string
modified_on?: string
expires_on?: string
priority?: number
geo_restrictions?: CloudflareCertificateGeoRestrictions
}
/** Raw certificate pack payload. */
export interface CloudflareRawCertificatePack {
id?: string
type?: string
hosts?: string[]
primary_certificate?: string
status?: string
certificates?: CloudflareRawCertificate[]
cloudflare_branding?: boolean
validation_method?: string
validity_days?: number
certificate_authority?: string
validation_errors?: Array<{ message?: string }>
validation_records?: CloudflareRawValidationRecord[]
dcv_delegation_records?: CloudflareRawValidationRecord[]
}
/** Raw DNS analytics aggregate block (`totals`, `min`, and `max` share this shape). */
export interface CloudflareRawDnsAnalyticsAggregate {
queryCount?: number
uncachedCount?: number
staleCount?: number
responseTimeAvg?: number
responseTimeMedian?: number
responseTime90th?: number
responseTime99th?: number
}
/** Raw DNS analytics report payload. */
export interface CloudflareRawDnsAnalyticsReport {
totals?: CloudflareRawDnsAnalyticsAggregate
min?: CloudflareRawDnsAnalyticsAggregate
max?: CloudflareRawDnsAnalyticsAggregate
data?: Array<{ dimensions?: string[]; metrics?: number[] }>
data_lag?: number
rows?: number
query?: {
since?: string
until?: string
metrics?: string[]
dimensions?: string[]
filters?: string
sort?: string[]
limit?: number
}
}
export interface CloudflareListZonesParams extends CloudflareBaseParams {
name?: string
status?: string
@@ -179,7 +500,7 @@ interface CloudflareDnsRecord {
proxied: boolean
ttl: number
locked: boolean
priority?: number
priority?: number | null
comment?: string | null
tags: string[]
comment_modified_on?: string | null
@@ -438,7 +759,591 @@ export interface CloudflareUpdateZoneSettingResponse extends ToolResponse {
}
}
export interface CloudflareListRulesetsParams extends CloudflareBaseParams {
zoneId: string
per_page?: number
cursor?: string
}
interface CloudflareRulesetSummary {
id: string
name: string
description: string
kind: string
phase: string
version: string | null
last_updated: string | null
}
export interface CloudflareListRulesetsResponse extends ToolResponse {
output: {
rulesets: CloudflareRulesetSummary[]
total_count: number
cursor: string | null
}
}
interface CloudflareRule {
id: string
version: string | null
action: string
action_parameters: Record<string, unknown> | null
expression: string
description: string
enabled: boolean
ref: string | null
last_updated: string | null
categories: string[]
logging: Record<string, unknown> | null
ratelimit: Record<string, unknown> | null
}
interface CloudflareRuleset extends CloudflareRulesetSummary {
rules: CloudflareRule[]
}
export interface CloudflareGetRulesetParams extends CloudflareBaseParams {
zoneId: string
rulesetId: string
}
export interface CloudflareRulesetResponse extends ToolResponse {
output: CloudflareRuleset
}
export interface CloudflareCreateRulesetRuleParams extends CloudflareBaseParams {
zoneId: string
rulesetId: string
action: string
expression: string
description?: string
enabled?: boolean
ref?: string
position?: string
actionParameters?: string
}
export interface CloudflareUpdateRulesetRuleParams extends CloudflareBaseParams {
zoneId: string
rulesetId: string
ruleId: string
action: string
expression: string
description?: string
enabled?: boolean
ref?: string
actionParameters?: string
ratelimit?: string
logging?: string
}
export interface CloudflareDeleteRulesetRuleParams extends CloudflareBaseParams {
zoneId: string
rulesetId: string
ruleId: string
}
export interface CloudflareGetRulesetEntrypointParams extends CloudflareBaseParams {
zoneId: string
phase: string
}
export interface CloudflareCreateRulesetParams extends CloudflareBaseParams {
zoneId: string
name: string
phase: string
kind?: string
description?: string
rules?: unknown
}
export interface CloudflareListRateLimitRulesParams extends CloudflareBaseParams {
zoneId: string
}
export interface CloudflareListManagedRulesetOverridesParams extends CloudflareBaseParams {
zoneId: string
}
export interface CloudflareListManagedRulesetOverridesResponse extends ToolResponse {
output: {
ruleset_id: string
deployments: Array<{
rule_id: string
managed_ruleset_id: string | null
description: string
expression: string
enabled: boolean
overrides: Record<string, unknown> | null
}>
total_count: number
}
}
export interface CloudflareCreateRateLimitRuleParams extends CloudflareBaseParams {
zoneId: string
rulesetId: string
expression: string
characteristics: string
period: number
requestsPerPeriod: number
action?: string
mitigationTimeout?: number
counting_expression?: string
requestsToOrigin?: boolean
description?: string
enabled?: boolean
}
export interface CloudflareUpdateRateLimitRuleParams extends CloudflareBaseParams {
zoneId: string
rulesetId: string
ruleId: string
expression: string
characteristics: string
period: number
requestsPerPeriod: number
action: string
mitigationTimeout?: number
counting_expression?: string
requestsToOrigin?: boolean
description?: string
enabled?: boolean
}
interface CloudflareAccessApplication {
id: string
name: string | null
domain: string | null
type: string | null
aud: string | null
session_duration: string | null
allowed_idps: string[] | null
app_launcher_visible: boolean | null
auto_redirect_to_identity: boolean | null
custom_deny_message: string | null
custom_deny_url: string | null
logo_url: string | null
self_hosted_domains: string[] | null
destinations: unknown[] | null
tags: string[] | null
policies: unknown[] | null
}
export interface CloudflareListAccessApplicationsParams extends CloudflareBaseParams {
accountId: string
name?: string
domain?: string
aud?: string
search?: string
exact?: boolean
page?: number
per_page?: number
}
export interface CloudflareListAccessApplicationsResponse extends ToolResponse {
output: {
applications: CloudflareAccessApplication[]
total_count: number
}
}
export interface CloudflareGetAccessApplicationParams extends CloudflareBaseParams {
accountId: string
appId: string
}
export interface CloudflareAccessApplicationResponse extends ToolResponse {
output: CloudflareAccessApplication
}
export interface CloudflareCreateAccessApplicationParams extends CloudflareBaseParams {
accountId: string
type: string
domain?: string
name?: string
sessionDuration?: string
allowedIdps?: string
appLauncherVisible?: boolean
autoRedirectToIdentity?: boolean
customDenyMessage?: string
customDenyUrl?: string
logoUrl?: string
tags?: string
policies?: string
}
export interface CloudflareUpdateAccessApplicationParams
extends CloudflareCreateAccessApplicationParams {
appId: string
}
export interface CloudflareDeleteAccessApplicationParams extends CloudflareBaseParams {
accountId: string
appId: string
}
export interface CloudflareDeletedIdResponse extends ToolResponse {
output: {
id: string
}
}
interface CloudflareAccessPolicy {
id: string
name: string | null
decision: string | null
precedence: number | null
include: unknown[] | null
exclude: unknown[] | null
require: unknown[] | null
session_duration: string | null
approval_required: boolean | null
isolation_required: boolean | null
purpose_justification_required: boolean | null
purpose_justification_prompt: string | null
created_at: string | null
updated_at: string | null
}
export interface CloudflareListAccessPoliciesParams extends CloudflareBaseParams {
accountId: string
appId: string
page?: number
per_page?: number
}
export interface CloudflareListAccessPoliciesResponse extends ToolResponse {
output: {
policies: CloudflareAccessPolicy[]
total_count: number
}
}
export interface CloudflareCreateAccessPolicyParams extends CloudflareBaseParams {
accountId: string
appId: string
name: string
decision: string
include: string
exclude?: string
require?: string
precedence?: number
sessionDuration?: string
approvalRequired?: boolean
isolationRequired?: boolean
purposeJustificationRequired?: boolean
purposeJustificationPrompt?: string
}
export interface CloudflareUpdateAccessPolicyParams extends CloudflareCreateAccessPolicyParams {
policyId: string
}
export interface CloudflareAccessPolicyResponse extends ToolResponse {
output: CloudflareAccessPolicy
}
export interface CloudflareDeleteAccessPolicyParams extends CloudflareBaseParams {
accountId: string
appId: string
policyId: string
}
export interface CloudflareListAccessGroupsParams extends CloudflareBaseParams {
accountId: string
name?: string
search?: string
page?: number
per_page?: number
}
export interface CloudflareListAccessGroupsResponse extends ToolResponse {
output: {
groups: Array<{
id: string
name: string | null
is_default: unknown[] | null
include: unknown[] | null
exclude: unknown[] | null
require: unknown[] | null
created_at: string | null
updated_at: string | null
}>
total_count: number
}
}
export interface CloudflareListAccessIdentityProvidersParams extends CloudflareBaseParams {
accountId: string
}
export interface CloudflareListAccessIdentityProvidersResponse extends ToolResponse {
output: {
identity_providers: Array<{
id: string
name: string | null
type: string | null
read_only: boolean | null
config: Record<string, unknown> | null
scim_config: Record<string, unknown> | null
}>
total_count: number
}
}
export interface CloudflareListAccessServiceTokensParams extends CloudflareBaseParams {
accountId: string
name?: string
search?: string
page?: number
per_page?: number
}
interface CloudflareAccessServiceToken {
id: string
name: string | null
client_id: string | null
duration: string | null
enabled: boolean | null
expires_at: string | null
last_seen_at: string | null
created_at: string | null
updated_at: string | null
}
export interface CloudflareListAccessServiceTokensResponse extends ToolResponse {
output: {
service_tokens: CloudflareAccessServiceToken[]
total_count: number
}
}
export interface CloudflareCreateAccessServiceTokenParams extends CloudflareBaseParams {
accountId: string
name: string
duration?: string
}
export interface CloudflareCreateAccessServiceTokenResponse extends ToolResponse {
output: CloudflareAccessServiceToken & {
client_secret: string | null
}
}
export interface CloudflareRevokeAccessServiceTokenParams extends CloudflareBaseParams {
accountId: string
serviceTokenId: string
}
export interface CloudflareAccessServiceTokenResponse extends ToolResponse {
output: CloudflareAccessServiceToken
}
interface CloudflareR2Bucket {
name: string
creation_date: string | null
location: string | null
storage_class: string | null
jurisdiction: string | null
}
export interface CloudflareListR2BucketsParams extends CloudflareBaseParams {
accountId: string
name_contains?: string
start_after?: string
cursor?: string
direction?: string
per_page?: number
jurisdiction?: string
}
export interface CloudflareListR2BucketsResponse extends ToolResponse {
output: {
buckets: CloudflareR2Bucket[]
cursor: string | null
}
}
export interface CloudflareGetR2BucketParams extends CloudflareBaseParams {
accountId: string
bucketName: string
jurisdiction?: string
}
export interface CloudflareCreateR2BucketParams extends CloudflareBaseParams {
accountId: string
bucketName: string
locationHint?: string
storageClass?: string
jurisdiction?: string
}
export interface CloudflareR2BucketResponse extends ToolResponse {
output: CloudflareR2Bucket
}
export interface CloudflareDeleteR2BucketParams extends CloudflareBaseParams {
accountId: string
bucketName: string
jurisdiction?: string
}
export interface CloudflareDeleteR2BucketResponse extends ToolResponse {
output: {
name: string
}
}
export interface CloudflareListWorkerScriptsParams extends CloudflareBaseParams {
accountId: string
tags?: string
}
export interface CloudflareListWorkerScriptsResponse extends ToolResponse {
output: {
scripts: Array<{
id: string
tag: string | null
etag: string | null
created_on: string | null
modified_on: string | null
usage_model: string | null
placement_mode: string | null
logpush: boolean | null
has_assets: boolean | null
has_modules: boolean | null
compatibility_date: string | null
compatibility_flags: string[] | null
routes: unknown[] | null
tail_consumers: unknown[] | null
}>
total_count: number
}
}
export interface CloudflareGetWorkerScriptSettingsParams extends CloudflareBaseParams {
accountId: string
scriptName: string
}
export interface CloudflareGetWorkerScriptSettingsResponse extends ToolResponse {
output: {
bindings: unknown[] | null
compatibility_date: string | null
compatibility_flags: string[] | null
limits: Record<string, unknown> | null
logpush: boolean | null
migrations: Record<string, unknown> | null
observability: Record<string, unknown> | null
placement: Record<string, unknown> | null
tags: string[] | null
tail_consumers: unknown[] | null
usage_model: string | null
}
}
export interface CloudflareListWorkerRoutesParams extends CloudflareBaseParams {
zoneId: string
}
export interface CloudflareListWorkerRoutesResponse extends ToolResponse {
output: {
routes: Array<{
id: string
pattern: string
script: string | null
}>
total_count: number
}
}
interface CloudflareTunnel {
id: string
name: string | null
account_tag: string | null
config_src: string | null
status: string | null
tun_type: string | null
remote_config: boolean | null
metadata: Record<string, unknown> | null
created_at: string | null
deleted_at: string | null
conns_active_at: string | null
conns_inactive_at: string | null
connections: unknown[] | null
}
export interface CloudflareListTunnelsParams extends CloudflareBaseParams {
accountId: string
name?: string
status?: string
uuid?: string
is_deleted?: boolean
include_prefix?: string
exclude_prefix?: string
existed_at?: string
was_active_at?: string
was_inactive_at?: string
page?: number
per_page?: number
}
export interface CloudflareListTunnelsResponse extends ToolResponse {
output: {
tunnels: CloudflareTunnel[]
total_count: number
}
}
export interface CloudflareGetTunnelParams extends CloudflareBaseParams {
accountId: string
tunnelId: string
}
export interface CloudflareTunnelResponse extends ToolResponse {
output: CloudflareTunnel
}
export interface CloudflareGetTunnelConfigurationParams extends CloudflareBaseParams {
accountId: string
tunnelId: string
}
export interface CloudflareGetTunnelConfigurationResponse extends ToolResponse {
output: {
tunnel_id: string
account_id: string
version: number | null
source: string | null
created_at: string | null
config: Record<string, unknown> | null
}
}
export type CloudflareResponse =
| CloudflareListRulesetsResponse
| CloudflareRulesetResponse
| CloudflareListManagedRulesetOverridesResponse
| CloudflareListAccessApplicationsResponse
| CloudflareAccessApplicationResponse
| CloudflareListAccessPoliciesResponse
| CloudflareAccessPolicyResponse
| CloudflareListAccessGroupsResponse
| CloudflareListAccessIdentityProvidersResponse
| CloudflareListAccessServiceTokensResponse
| CloudflareCreateAccessServiceTokenResponse
| CloudflareAccessServiceTokenResponse
| CloudflareDeletedIdResponse
| CloudflareListR2BucketsResponse
| CloudflareR2BucketResponse
| CloudflareDeleteR2BucketResponse
| CloudflareListWorkerScriptsResponse
| CloudflareGetWorkerScriptSettingsResponse
| CloudflareListWorkerRoutesResponse
| CloudflareListTunnelsResponse
| CloudflareTunnelResponse
| CloudflareGetTunnelConfigurationResponse
| CloudflareListZonesResponse
| CloudflareGetZoneResponse
| CloudflareCreateZoneResponse
@@ -0,0 +1,241 @@
import type {
CloudflareAccessApplicationResponse,
CloudflareUpdateAccessApplicationParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyAccessApplication,
mapAccessApplication,
parseCsvParam,
parseJsonArrayParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const updateAccessApplicationTool: ToolConfig<
CloudflareUpdateAccessApplicationParams,
CloudflareAccessApplicationResponse
> = {
id: 'cloudflare_update_access_application',
name: 'Cloudflare Update Access Application',
description:
'Updates a Cloudflare Access (Zero Trust) application. This replaces the application definition rather than merging it, so send every field the application should keep — anything you omit reverts to its default, which can widen or break access. Read the current configuration with "Get Access Application" first. Requires an API token with Account Access: Apps and Policies Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID to update',
},
type: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Application type: self_hosted, saas, ssh, vnc, app_launcher, warp, biso, bookmark, dash_sso, infrastructure, rdp, mcp, mcp_portal, or proxy_endpoint',
},
domain: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The primary hostname and path secured by Access. Required for the self_hosted, ssh, vnc, rdp, and bookmark types; the saas, app_launcher, warp, biso, dash_sso, infrastructure, mcp, mcp_portal, and proxy_endpoint types do not accept it',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Friendly name shown in the dashboard and App Launcher',
},
sessionDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'How long an Access session stays valid, e.g. 24h or 30m',
},
allowedIdps: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated identity provider IDs users may authenticate with',
},
appLauncherVisible: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the application is shown in the App Launcher',
},
autoRedirectToIdentity: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether users skip the identity provider picker',
},
customDenyMessage: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Message shown to users who are denied access',
},
customDenyUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL denied users are redirected to',
},
logoUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Logo image URL shown in the dashboard and App Launcher',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated tag names categorizing the application',
},
policies: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of policies to attach. Entries may be reusable policy IDs or inline policy objects',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}`,
method: 'PUT',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = { type: params.type }
/**
* `domain` exists only on the self_hosted, ssh, vnc, rdp, and bookmark
* request variants; the saas, app_launcher, warp, biso, dash_sso,
* infrastructure, mcp, mcp_portal, and proxy_endpoint variants have no
* such field, so sending a blank one makes those app types unbuildable.
*/
if (params.domain) body.domain = params.domain
if (params.name) body.name = params.name
if (params.sessionDuration) body.session_duration = params.sessionDuration
const allowedIdps = parseCsvParam(params.allowedIdps)
if (allowedIdps) body.allowed_idps = allowedIdps
if (params.appLauncherVisible !== undefined) {
body.app_launcher_visible = params.appLauncherVisible
}
if (params.autoRedirectToIdentity !== undefined) {
body.auto_redirect_to_identity = params.autoRedirectToIdentity
}
if (params.customDenyMessage) body.custom_deny_message = params.customDenyMessage
if (params.customDenyUrl) body.custom_deny_url = params.customDenyUrl
if (params.logoUrl) body.logo_url = params.logoUrl
const tags = parseCsvParam(params.tags)
if (tags) body.tags = tags
const policies = parseJsonArrayParam(params.policies, 'Policies')
if (policies) body.policies = policies
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyAccessApplication(),
error: cloudflareErrorMessage(data, 'Failed to update Access application'),
}
}
return { success: true, output: mapAccessApplication(data.result) }
},
outputs: {
id: { type: 'string', description: 'Access application identifier' },
name: { type: 'string', description: 'Application name', optional: true },
domain: {
type: 'string',
description: 'Primary hostname and path secured by Access',
optional: true,
},
type: { type: 'string', description: 'Application type', optional: true },
aud: { type: 'string', description: 'Audience tag used to verify Access JWTs', optional: true },
session_duration: {
type: 'string',
description: 'How long an Access session stays valid',
optional: true,
},
allowed_idps: {
type: 'array',
description: 'Identity provider IDs users may authenticate with',
items: { type: 'string', description: 'Identity provider ID' },
optional: true,
},
app_launcher_visible: {
type: 'boolean',
description: 'Whether the app appears in the App Launcher',
optional: true,
},
auto_redirect_to_identity: {
type: 'boolean',
description: 'Whether users skip the identity provider picker',
optional: true,
},
custom_deny_message: {
type: 'string',
description: 'Message shown when access is denied',
optional: true,
},
custom_deny_url: {
type: 'string',
description: 'URL users are redirected to when access is denied',
optional: true,
},
logo_url: { type: 'string', description: 'Logo image URL', optional: true },
self_hosted_domains: {
type: 'array',
description:
'Additional hostnames and paths secured by the application. Cloudflare deprecated this field in favour of destinations, which is the one to read on a current application',
items: { type: 'string', description: 'Hostname and path' },
optional: true,
},
destinations: {
type: 'json',
description: 'Public and private destinations secured by the application',
optional: true,
},
tags: {
type: 'array',
description: 'Tags categorizing the application',
items: { type: 'string', description: 'Tag name' },
optional: true,
},
policies: {
type: 'json',
description: 'Access policies attached to the application',
optional: true,
},
},
}
@@ -0,0 +1,215 @@
import type {
CloudflareAccessPolicyResponse,
CloudflareUpdateAccessPolicyParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyAccessPolicy,
mapAccessPolicy,
parseJsonArrayParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const updateAccessPolicyTool: ToolConfig<
CloudflareUpdateAccessPolicyParams,
CloudflareAccessPolicyResponse
> = {
id: 'cloudflare_update_access_policy',
name: 'Cloudflare Update Access Policy',
description:
'Updates a Cloudflare Access (Zero Trust) policy on an application. This replaces the policy definition rather than merging it, so send every rule the policy should keep — omitted exclude or require rules are dropped, which can widen who gets in. The change applies to live traffic immediately. Read the current policy with "List Access Policies" first. Requires an API token with Account Access: Apps and Policies Edit.',
version: '1.0.0',
params: {
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Cloudflare account ID. Access applications are account-scoped',
},
appId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access application ID that owns the policy',
},
policyId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Access policy ID to update',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the policy',
},
decision: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'What the policy does when it matches: allow, deny, non_identity, or bypass (skip Access entirely)',
},
include: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of Access rules evaluated with OR logic. Example: [{"email_domain":{"domain":"example.com"}}]',
},
exclude: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of Access rules evaluated with NOT logic',
},
require: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of Access rules evaluated with AND logic',
},
precedence: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Evaluation order of the policy within the application',
},
sessionDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'How long a session granted by this policy stays valid, e.g. 24h',
},
approvalRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether an approver must grant each access request',
},
isolationRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the session must run in a remote isolated browser',
},
purposeJustificationRequired: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether users must state a reason for access',
},
purposeJustificationPrompt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Prompt shown when a justification is required',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/accounts/${params.accountId.trim()}/access/apps/${params.appId.trim()}/policies/${params.policyId.trim()}`,
method: 'PUT',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const include = parseJsonArrayParam(params.include, 'Include Rules')
if (!include || include.length === 0) {
throw new Error('Include Rules must contain at least one Access rule')
}
const body: Record<string, unknown> = {
name: params.name,
decision: params.decision,
include,
}
const exclude = parseJsonArrayParam(params.exclude, 'Exclude Rules')
if (exclude) body.exclude = exclude
const require = parseJsonArrayParam(params.require, 'Require Rules')
if (require) body.require = require
if (params.precedence !== undefined) body.precedence = params.precedence
if (params.sessionDuration) body.session_duration = params.sessionDuration
if (params.approvalRequired !== undefined) body.approval_required = params.approvalRequired
if (params.isolationRequired !== undefined) body.isolation_required = params.isolationRequired
if (params.purposeJustificationRequired !== undefined) {
body.purpose_justification_required = params.purposeJustificationRequired
}
if (params.purposeJustificationPrompt) {
body.purpose_justification_prompt = params.purposeJustificationPrompt
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyAccessPolicy(),
error: cloudflareErrorMessage(data, 'Failed to update Access policy'),
}
}
return { success: true, output: mapAccessPolicy(data.result) }
},
outputs: {
id: { type: 'string', description: 'Policy identifier' },
name: { type: 'string', description: 'Policy name', optional: true },
decision: {
type: 'string',
description: 'Decision the policy applies: allow, deny, non_identity, or bypass',
optional: true,
},
precedence: {
type: 'number',
description: 'Evaluation order of the policy within the application',
optional: true,
},
include: { type: 'json', description: 'Rules evaluated with OR logic', optional: true },
exclude: { type: 'json', description: 'Rules evaluated with NOT logic', optional: true },
require: { type: 'json', description: 'Rules evaluated with AND logic', optional: true },
session_duration: {
type: 'string',
description: 'How long a session granted by this policy stays valid',
optional: true,
},
approval_required: {
type: 'boolean',
description: 'Whether an approver must grant each access request',
optional: true,
},
isolation_required: {
type: 'boolean',
description: 'Whether the session must run in a remote browser',
optional: true,
},
purpose_justification_required: {
type: 'boolean',
description: 'Whether users must state a reason for access',
optional: true,
},
purpose_justification_prompt: {
type: 'string',
description: 'Prompt shown when a justification is required',
optional: true,
},
created_at: { type: 'string', description: 'Creation timestamp', optional: true },
updated_at: { type: 'string', description: 'Last update timestamp', optional: true },
},
}
+12 -5
View File
@@ -84,17 +84,24 @@ export const updateDnsRecordTool: ToolConfig<
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records/${params.recordId}`,
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/dns_records/${params.recordId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.type !== undefined) body.type = params.type
if (params.name !== undefined) body.name = params.name
if (params.content !== undefined) body.content = params.content
const body: Record<string, unknown> = {}
/**
* Cloudflare rejects an empty type, name, or content, so a blank field
* means "leave this alone" rather than "clear it" — the block already
* strips those, and this guard makes a direct tool call behave the same.
* `comment` is deliberately not guarded: an empty comment is how the API
* clears a stored comment, which is a real thing a caller asks for.
*/
if (params.type !== undefined && params.type !== '') body.type = params.type
if (params.name !== undefined && params.name !== '') body.name = params.name
if (params.content !== undefined && params.content !== '') body.content = params.content
if (params.ttl !== undefined) body.ttl = Number(params.ttl)
if (params.proxied !== undefined) body.proxied = params.proxied
if (params.priority !== undefined) body.priority = Number(params.priority)
@@ -0,0 +1,216 @@
import type {
CloudflareRulesetResponse,
CloudflareUpdateRateLimitRuleParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
parseCsvParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const updateRateLimitRuleTool: ToolConfig<
CloudflareUpdateRateLimitRuleParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_update_rate_limit_rule',
name: 'Cloudflare Update Rate Limiting Rule',
description:
'Updates a rate limiting rule in the http_ratelimit phase entry point ruleset of a zone, using the current Rulesets-based rate limiting API. Cloudflare replaces the rule definition rather than merging it, so send the complete rule — every field you omit is reset. Run "List Rate Limiting Rules" first to read the current definition and get the ruleset ID. Requires an API token with Zone WAF Edit.',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID that owns the rule',
},
rulesetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The http_ratelimit entry point ruleset ID, as returned by "List Rate Limiting Rules"',
},
ruleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The rate limiting rule ID to update',
},
expression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Cloudflare filter expression selecting the requests the rule applies to',
},
characteristics: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated counting characteristics. cf.colo.id is mandatory, plus exactly one of ip.src or cf.unique_visitor_id',
},
period: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description:
'Counting window in seconds. Cloudflare accepts only 10, 60, 120, 300, 600, or 3600',
},
requestsPerPeriod: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of requests allowed within the counting period before the action fires',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Action applied once the limit is exceeded: block, managed_challenge, js_challenge, challenge, or log. Required because this endpoint replaces the rule rather than merging into it — a defaulted action would silently convert an existing log or challenge rule into a hard block',
},
mitigationTimeout: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Seconds the action stays applied. Cloudflare accepts only 0, 10, 60, 120, 300, 600, 3600, or 86400',
},
counting_expression: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional expression defining which requests are counted',
},
requestsToOrigin: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, only requests that reach the origin are counted',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Human-readable description of the rule',
},
enabled: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the rule is enabled',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/${params.rulesetId.trim()}/rules/${params.ruleId.trim()}`,
method: 'PATCH',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const characteristics = parseCsvParam(params.characteristics)
if (!characteristics) {
throw new Error('Characteristics must list at least one counting characteristic')
}
const ratelimit: Record<string, unknown> = {
characteristics,
period: params.period,
requests_per_period: params.requestsPerPeriod,
}
if (params.mitigationTimeout !== undefined) {
ratelimit.mitigation_timeout = params.mitigationTimeout
}
if (params.counting_expression) ratelimit.counting_expression = params.counting_expression
if (params.requestsToOrigin !== undefined) {
ratelimit.requests_to_origin = params.requestsToOrigin
}
const body: Record<string, unknown> = {
action: params.action,
expression: params.expression,
ratelimit,
}
if (params.description !== undefined) body.description = params.description
if (params.enabled !== undefined) body.enabled = params.enabled
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to update rate limiting rule'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset ID of the http_ratelimit entry point' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind' },
phase: { type: 'string', description: 'Phase the ruleset runs in (http_ratelimit)' },
version: { type: 'string', description: 'Ruleset version after the change', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rate limiting rules after the change, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: { type: 'string', description: 'Action applied once the limit is exceeded' },
action_parameters: {
type: 'json',
description: 'Action-specific parameters',
optional: true,
},
expression: { type: 'string', description: 'Filter expression' },
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: { type: 'string', description: 'Rule reference tag', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: {
type: 'json',
description: 'Rate limiting configuration applied to the rule',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,191 @@
import type {
CloudflareRulesetResponse,
CloudflareUpdateRulesetRuleParams,
} from '@/tools/cloudflare/types'
import {
cloudflareErrorMessage,
cloudflareHeaders,
emptyRuleset,
mapRuleset,
parseJsonObjectParam,
} from '@/tools/cloudflare/utils'
import type { ToolConfig } from '@/tools/types'
export const updateRulesetRuleTool: ToolConfig<
CloudflareUpdateRulesetRuleParams,
CloudflareRulesetResponse
> = {
id: 'cloudflare_update_ruleset_rule',
name: 'Cloudflare Update Ruleset Rule',
description:
'Updates a rule in a zone ruleset. Cloudflare replaces the rule definition rather than merging it, so you must send every field you want the rule to keep — any field you omit is reset to its default. Read the current rule with "Get Ruleset" first. Requires an API token with Zone WAF Edit (or another matching ruleset Write permission).',
version: '1.0.0',
params: {
zoneId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The zone ID that owns the ruleset',
},
rulesetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ruleset ID containing the rule',
},
ruleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The rule ID to update',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The action the rule performs, e.g. block, challenge, js_challenge, managed_challenge, log, skip, or execute. Required because this endpoint replaces the rule definition — omitting it resets the stored action',
},
expression: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Cloudflare filter expression selecting matching requests. Required because this endpoint replaces the rule definition — omitting it resets the stored expression',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Human-readable description of the rule',
},
enabled: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the rule is enabled',
},
ref: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Reference tag that stays stable across rule updates',
},
actionParameters: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON object of action-specific parameters, e.g. {"id":"<MANAGED_RULESET_ID>","overrides":{"rules":[{"id":"<RULE_ID>","action":"log","enabled":true,"score_threshold":40}]}}',
},
ratelimit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON rate limiting configuration to preserve on a rule in the http_ratelimit phase, e.g. {"characteristics":["cf.colo.id","ip.src"],"period":60,"requests_per_period":100}. Because the update replaces the rule, omitting this on a rate limiting rule stops it rate limiting',
},
logging: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON logging configuration to preserve, e.g. {"enabled":true}. Omitting it on a rule that had logging configured resets it to the default',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Cloudflare API Token',
},
},
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/rulesets/${params.rulesetId.trim()}/rules/${params.ruleId.trim()}`,
method: 'PATCH',
headers: (params) => cloudflareHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
action: params.action,
expression: params.expression,
}
if (params.description !== undefined) body.description = params.description
if (params.enabled !== undefined) body.enabled = params.enabled
if (params.ref) body.ref = params.ref
const actionParameters = parseJsonObjectParam(params.actionParameters, 'Action Parameters')
if (actionParameters) body.action_parameters = actionParameters
const ratelimit = parseJsonObjectParam(params.ratelimit, 'Rate Limiting Configuration')
if (ratelimit) body.ratelimit = ratelimit
const logging = parseJsonObjectParam(params.logging, 'Logging Configuration')
if (logging) body.logging = logging
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: emptyRuleset(),
error: cloudflareErrorMessage(data, 'Failed to update ruleset rule'),
}
}
return { success: true, output: mapRuleset(data.result) }
},
outputs: {
id: { type: 'string', description: 'Ruleset identifier' },
name: { type: 'string', description: 'Ruleset name' },
description: { type: 'string', description: 'Ruleset description' },
kind: { type: 'string', description: 'Ruleset kind (managed, custom, root, or zone)' },
phase: { type: 'string', description: 'Phase the ruleset runs in' },
version: { type: 'string', description: 'Ruleset version after the change', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
rules: {
type: 'array',
description: 'Rules in the ruleset after the change, in evaluation order',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule identifier' },
version: { type: 'string', description: 'Rule version', optional: true },
action: { type: 'string', description: 'Action the rule performs' },
action_parameters: {
type: 'json',
description: 'Action-specific parameters',
optional: true,
},
expression: { type: 'string', description: 'Filter expression' },
description: { type: 'string', description: 'Rule description' },
enabled: { type: 'boolean', description: 'Whether the rule is enabled' },
ref: { type: 'string', description: 'Rule reference tag', optional: true },
last_updated: {
type: 'string',
description: 'RFC 3339 timestamp of the last change',
optional: true,
},
categories: {
type: 'array',
description: 'Managed-rule categories',
items: { type: 'string', description: 'Category tag' },
},
logging: { type: 'json', description: 'Logging configuration', optional: true },
ratelimit: { type: 'json', description: 'Rate limiting configuration', optional: true },
},
},
},
},
}
@@ -32,8 +32,7 @@ export const updateZoneSettingTool: ToolConfig<
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'New value for the setting as a string or JSON string for complex values (e.g., "full" for SSL, "medium" for security_level, "aggressive" for cache_level, \'["ECDHE-RSA-AES128-GCM-SHA256"]\' for ciphers)',
description: `New value for the setting as a string, or a JSON string for complex values (e.g., "full" for SSL, "medium" for security_level, "aggressive" for cache_level, ["ECDHE-RSA-AES128-GCM-SHA256"] for ciphers)`,
},
apiKey: {
type: 'string',
@@ -45,7 +44,7 @@ export const updateZoneSettingTool: ToolConfig<
request: {
url: (params) =>
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/settings/${params.settingId}`,
`https://api.cloudflare.com/client/v4/zones/${params.zoneId.trim()}/settings/${params.settingId.trim()}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
+223
View File
@@ -0,0 +1,223 @@
/**
* Shared request/response helpers for the Cloudflare API tools.
*
* Every Cloudflare v4 endpoint answers with the same envelope:
* `{ success, errors, messages, result }`. A `200 OK` carrying
* `success: false` is a failure, so callers must always branch on
* `data.success` rather than on the HTTP status.
*/
import type {
CloudflareEnvelope,
CloudflareRawAccessApplication,
CloudflareRawAccessPolicy,
CloudflareRawRuleset,
} from '@/tools/cloudflare/types'
/**
* Reads a Cloudflare v4 response body into the shared envelope shape, so the
* caller branches on a typed `success` flag and reads `result` through a checked
* payload type instead of an implicitly-`any` `response.json()`. Used by the
* tools whose `result` has a payload type in `types.ts`.
*/
export async function readCloudflareResponse<TResult>(
response: Response
): Promise<CloudflareEnvelope<TResult>> {
return (await response.json()) as CloudflareEnvelope<TResult>
}
/** Standard bearer-token headers for the Cloudflare v4 API. */
export function cloudflareHeaders(apiKey: string): Record<string, string> {
return {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
}
}
/** Extracts the first error message from a Cloudflare envelope. */
export function cloudflareErrorMessage(data: CloudflareEnvelope, fallback: string): string {
const message = data.errors?.[0]?.message
return typeof message === 'string' && message.length > 0 ? message : fallback
}
/**
* Parses a param that may arrive either as a JSON string (typed into a block
* field) or as an already-structured value (piped from an upstream block).
*/
export function parseJsonParam(value: unknown, fieldName: string): unknown {
if (value === undefined || value === null || value === '') return undefined
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch {
throw new Error(`${fieldName} must be valid JSON`)
}
}
/** Parses a param that must resolve to a JSON array. */
export function parseJsonArrayParam(value: unknown, fieldName: string): unknown[] | undefined {
const parsed = parseJsonParam(value, fieldName)
if (parsed === undefined) return undefined
if (!Array.isArray(parsed)) throw new Error(`${fieldName} must be a JSON array`)
return parsed
}
/** Parses a param that must resolve to a JSON object. */
export function parseJsonObjectParam(
value: unknown,
fieldName: string
): Record<string, unknown> | undefined {
const parsed = parseJsonParam(value, fieldName)
if (parsed === undefined) return undefined
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error(`${fieldName} must be a JSON object`)
}
return parsed as Record<string, unknown>
}
/** Splits a comma-separated string param into a trimmed, non-empty list. */
export function parseCsvParam(value: unknown): string[] | undefined {
if (typeof value !== 'string' || value.trim() === '') return undefined
const items = value
.split(',')
.map((item) => item.trim())
.filter(Boolean)
return items.length > 0 ? items : undefined
}
/**
* Maps a Cloudflare ruleset `result` payload onto the flat ruleset output shape
* shared by the get-ruleset, phase-entrypoint, and rule mutation tools — every
* one of those endpoints answers with the full ruleset object.
*/
export function mapRuleset(ruleset: CloudflareRawRuleset | undefined) {
return {
id: ruleset?.id ?? '',
name: ruleset?.name ?? '',
description: ruleset?.description ?? '',
kind: ruleset?.kind ?? '',
phase: ruleset?.phase ?? '',
version: ruleset?.version ?? null,
last_updated: ruleset?.last_updated ?? null,
rules: Array.isArray(ruleset?.rules)
? ruleset.rules.map((rule) => ({
id: rule.id ?? '',
version: rule.version ?? null,
action: rule.action ?? '',
action_parameters: rule.action_parameters ?? null,
expression: rule.expression ?? '',
description: rule.description ?? '',
enabled: rule.enabled ?? false,
ref: rule.ref ?? null,
last_updated: rule.last_updated ?? null,
categories: rule.categories ?? [],
logging: rule.logging ?? null,
ratelimit: rule.ratelimit ?? null,
}))
: [],
}
}
/** The empty ruleset payload returned alongside an error. */
export function emptyRuleset() {
return {
id: '',
name: '',
description: '',
kind: '',
phase: '',
version: null,
last_updated: null,
rules: [],
}
}
/** Maps a Cloudflare Access application `result` payload onto the flat output shape. */
export function mapAccessApplication(app: CloudflareRawAccessApplication | undefined) {
return {
id: app?.id ?? '',
name: app?.name ?? null,
domain: app?.domain ?? null,
type: app?.type ?? null,
aud: app?.aud ?? null,
session_duration: app?.session_duration ?? null,
allowed_idps: app?.allowed_idps ?? null,
app_launcher_visible: app?.app_launcher_visible ?? null,
auto_redirect_to_identity: app?.auto_redirect_to_identity ?? null,
custom_deny_message: app?.custom_deny_message ?? null,
custom_deny_url: app?.custom_deny_url ?? null,
logo_url: app?.logo_url ?? null,
self_hosted_domains: app?.self_hosted_domains ?? null,
destinations: app?.destinations ?? null,
tags: app?.tags ?? null,
policies: app?.policies ?? null,
}
}
/** The empty Access application payload returned alongside an error. */
export function emptyAccessApplication() {
return {
id: '',
name: null,
domain: null,
type: null,
aud: null,
session_duration: null,
allowed_idps: null,
app_launcher_visible: null,
auto_redirect_to_identity: null,
custom_deny_message: null,
custom_deny_url: null,
logo_url: null,
self_hosted_domains: null,
destinations: null,
tags: null,
policies: null,
}
}
/** Maps a Cloudflare Access policy `result` payload onto the flat output shape. */
export function mapAccessPolicy(policy: CloudflareRawAccessPolicy | undefined) {
return {
id: policy?.id ?? '',
name: policy?.name ?? null,
decision: policy?.decision ?? null,
precedence: policy?.precedence ?? null,
include: policy?.include ?? null,
exclude: policy?.exclude ?? null,
require: policy?.require ?? null,
session_duration: policy?.session_duration ?? null,
approval_required: policy?.approval_required ?? null,
isolation_required: policy?.isolation_required ?? null,
purpose_justification_required: policy?.purpose_justification_required ?? null,
purpose_justification_prompt: policy?.purpose_justification_prompt ?? null,
created_at: policy?.created_at ?? null,
updated_at: policy?.updated_at ?? null,
}
}
/** The empty Access policy payload returned alongside an error. */
export function emptyAccessPolicy() {
return {
id: '',
name: null,
decision: null,
precedence: null,
include: null,
exclude: null,
require: null,
session_duration: null,
approval_required: null,
isolation_required: null,
purpose_justification_required: null,
purpose_justification_prompt: null,
created_at: null,
updated_at: null,
}
}
/** Appends a query param when the value is present and non-empty. */
export function appendParam(url: URL, key: string, value: unknown): void {
if (value === undefined || value === null || value === '') return
url.searchParams.append(key, String(value))
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+70
View File
@@ -580,18 +580,53 @@ import {
clickupUploadAttachmentTool,
} from '@/tools/clickup'
import {
cloudflareCreateAccessApplicationTool,
cloudflareCreateAccessPolicyTool,
cloudflareCreateAccessServiceTokenTool,
cloudflareCreateDnsRecordTool,
cloudflareCreateR2BucketTool,
cloudflareCreateRateLimitRuleTool,
cloudflareCreateRulesetRuleTool,
cloudflareCreateRulesetTool,
cloudflareCreateZoneTool,
cloudflareDeleteAccessApplicationTool,
cloudflareDeleteAccessPolicyTool,
cloudflareDeleteDnsRecordTool,
cloudflareDeleteR2BucketTool,
cloudflareDeleteRulesetRuleTool,
cloudflareDeleteZoneTool,
cloudflareDnsAnalyticsTool,
cloudflareGetAccessApplicationTool,
cloudflareGetR2BucketTool,
cloudflareGetRulesetEntrypointTool,
cloudflareGetRulesetTool,
cloudflareGetTunnelConfigurationTool,
cloudflareGetTunnelTool,
cloudflareGetWorkerScriptSettingsTool,
cloudflareGetZoneSettingsTool,
cloudflareGetZoneTool,
cloudflareListAccessApplicationsTool,
cloudflareListAccessGroupsTool,
cloudflareListAccessIdentityProvidersTool,
cloudflareListAccessPoliciesTool,
cloudflareListAccessServiceTokensTool,
cloudflareListCertificatesTool,
cloudflareListDnsRecordsTool,
cloudflareListManagedRulesetOverridesTool,
cloudflareListR2BucketsTool,
cloudflareListRateLimitRulesTool,
cloudflareListRulesetsTool,
cloudflareListTunnelsTool,
cloudflareListWorkerRoutesTool,
cloudflareListWorkerScriptsTool,
cloudflareListZonesTool,
cloudflarePurgeCacheTool,
cloudflareRevokeAccessServiceTokenTool,
cloudflareUpdateAccessApplicationTool,
cloudflareUpdateAccessPolicyTool,
cloudflareUpdateDnsRecordTool,
cloudflareUpdateRateLimitRuleTool,
cloudflareUpdateRulesetRuleTool,
cloudflareUpdateZoneSettingTool,
} from '@/tools/cloudflare'
import {
@@ -8616,6 +8651,41 @@ export const tools: Record<string, ToolConfig> = {
cloudflare_update_zone_setting: cloudflareUpdateZoneSettingTool,
cloudflare_dns_analytics: cloudflareDnsAnalyticsTool,
cloudflare_purge_cache: cloudflarePurgeCacheTool,
cloudflare_list_rulesets: cloudflareListRulesetsTool,
cloudflare_get_ruleset: cloudflareGetRulesetTool,
cloudflare_get_ruleset_entrypoint: cloudflareGetRulesetEntrypointTool,
cloudflare_create_ruleset: cloudflareCreateRulesetTool,
cloudflare_create_ruleset_rule: cloudflareCreateRulesetRuleTool,
cloudflare_update_ruleset_rule: cloudflareUpdateRulesetRuleTool,
cloudflare_delete_ruleset_rule: cloudflareDeleteRulesetRuleTool,
cloudflare_list_managed_ruleset_overrides: cloudflareListManagedRulesetOverridesTool,
cloudflare_list_rate_limit_rules: cloudflareListRateLimitRulesTool,
cloudflare_create_rate_limit_rule: cloudflareCreateRateLimitRuleTool,
cloudflare_update_rate_limit_rule: cloudflareUpdateRateLimitRuleTool,
cloudflare_list_access_applications: cloudflareListAccessApplicationsTool,
cloudflare_get_access_application: cloudflareGetAccessApplicationTool,
cloudflare_create_access_application: cloudflareCreateAccessApplicationTool,
cloudflare_update_access_application: cloudflareUpdateAccessApplicationTool,
cloudflare_delete_access_application: cloudflareDeleteAccessApplicationTool,
cloudflare_list_access_policies: cloudflareListAccessPoliciesTool,
cloudflare_create_access_policy: cloudflareCreateAccessPolicyTool,
cloudflare_update_access_policy: cloudflareUpdateAccessPolicyTool,
cloudflare_delete_access_policy: cloudflareDeleteAccessPolicyTool,
cloudflare_list_access_groups: cloudflareListAccessGroupsTool,
cloudflare_list_access_identity_providers: cloudflareListAccessIdentityProvidersTool,
cloudflare_list_access_service_tokens: cloudflareListAccessServiceTokensTool,
cloudflare_create_access_service_token: cloudflareCreateAccessServiceTokenTool,
cloudflare_revoke_access_service_token: cloudflareRevokeAccessServiceTokenTool,
cloudflare_list_r2_buckets: cloudflareListR2BucketsTool,
cloudflare_get_r2_bucket: cloudflareGetR2BucketTool,
cloudflare_create_r2_bucket: cloudflareCreateR2BucketTool,
cloudflare_delete_r2_bucket: cloudflareDeleteR2BucketTool,
cloudflare_list_worker_scripts: cloudflareListWorkerScriptsTool,
cloudflare_get_worker_script_settings: cloudflareGetWorkerScriptSettingsTool,
cloudflare_list_worker_routes: cloudflareListWorkerRoutesTool,
cloudflare_list_tunnels: cloudflareListTunnelsTool,
cloudflare_get_tunnel: cloudflareGetTunnelTool,
cloudflare_get_tunnel_configuration: cloudflareGetTunnelConfigurationTool,
discord_send_message: discordSendMessageTool,
discord_get_messages: discordGetMessagesTool,
discord_get_server: discordGetServerTool,