mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user