Skip to the content

Capsid menu

src/tools/cloudflare.ts

37 lines and 3 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. It is in an import cycle. This file on GitHub.

Functions

The functions of cloudflare.ts
FunctionLinesComplexityCoveredCRAPMutation
defineCloudflareOperations10 to 401100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:30>30 to 385100%OkCRAP 5No data: mutation testing has not run on this repo yet
<anonymous:34>34 to 341100%OkCRAP 1No data: mutation testing has not run on this repo yet

Imports

It imports src/limits.ts, src/ops-cloudflare-config.ts, src/tools/docs.ts. Imported by src/server.ts.

Source

import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
import { CF_CONFIG_ACTIONS, CF_PERMISSION, readCloudflareConfig } from "../ops-cloudflare-config";
import { fail, ok, type ToolCtx } from "./docs";
import { bounded } from "../limits";

// A zone name as Cloudflare lists it: labels of letters, digits and hyphens.
const ZONE_NAME = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;

export function defineCloudflareOperations(server: McpServer, ctx: ToolCtx): void {
  const { env } = ctx;

  // Cloudflare Access and Email Routing, read. Ruled 2026-09-29, "Capsid MCP roadmap,
  // build or don't build". Admin only in whole: it reads the
  // account's login gates and mail routes, which span every namespace. TOOL_GRANTS in
  // src/scope.ts states it and the registrar refuses anyone else before this handler
  // runs. It reads only, through src/ops-cloudflare-config.ts, which copies named
  // fields and never passes a Cloudflare object through, so no client secret, SCIM
  // credential or aud tag reaches the caller.
  ctx.ops.define(
    "cloudflare_config",
    {
      description:
        `Cloudflare Access and Email Routing configuration for the account, read with CF_OPS_TOKEN. Admin only. It reads and never writes. action "access_apps": each Access application's id, name, domain, type, self_hosted_domains, destinations (type, uri, hostname) and its policies (name, decision). action "access_policies": each reusable policy's id, name, decision and its include, exclude and require rules as types, with the value only for email (the address), email_domain (the domain), group (the group id) and service_token (the token id). action "email_rules": each zone's Email Routing rules (name, enabled, priority, matchers, actions); zone narrows it to one zone name, else every zone in the account. action "email_addresses": each destination address and whether it is verified. Client secrets, SCIM credentials and aud tags are never returned. A 403 names the permission the token lacks: ${CF_PERMISSION.access} for Access, ${CF_PERMISSION.zones} and ${CF_PERMISSION.rules} for rules, ${CF_PERMISSION.addresses} for addresses.`,
      inputSchema: {
        action: z.enum(CF_CONFIG_ACTIONS).describe("access_apps | access_policies | email_rules | email_addresses."),
        zone: bounded(253).regex(ZONE_NAME).optional().describe("For email_rules: one zone name, such as example.com. Omitted reads every zone in the account."),
      },
    },
    async (args) => {
      try {
        if (args.zone !== undefined && args.action !== "email_rules") return fail(`zone applies to email_rules only, and this call is ${args.action}.`);
        // Looked up per call, not captured, so the Worker's fetch is the one in scope.
        return ok(await readCloudflareConfig(env, args.action, args.zone?.toLowerCase(), (url, init) => fetch(url, init)));
      } catch (err) {
        return fail(err instanceof Error ? err.message : String(err));
      }
    }
  );
}