This is page 7 of 27. Use http://codebase.md/cloudflare/mcp-server-cloudflare?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .dockerignore ├── .editorconfig ├── .eslintrc.cjs ├── .github │ ├── actions │ │ └── setup │ │ └── action.yml │ ├── ISSUE_TEMPLATE │ │ └── bug_report.md │ └── workflows │ ├── branches.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .syncpackrc.cjs ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── apps │ ├── ai-gateway │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── ai-gateway.app.ts │ │ │ ├── ai-gateway.context.ts │ │ │ ├── tools │ │ │ │ └── ai-gateway.tools.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── auditlogs │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── auditlogs.app.ts │ │ │ ├── auditlogs.context.ts │ │ │ └── tools │ │ │ └── auditlogs.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── autorag │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── autorag.app.ts │ │ │ ├── autorag.context.ts │ │ │ ├── tools │ │ │ │ └── autorag.tools.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── browser-rendering │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── browser.app.ts │ │ │ ├── browser.context.ts │ │ │ └── tools │ │ │ └── browser.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── cloudflare-one-casb │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── cf1-casb.app.ts │ │ │ ├── cf1-casb.context.ts │ │ │ └── tools │ │ │ └── integrations.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── demo-day │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── frontend │ │ │ ├── index.html │ │ │ ├── public │ │ │ │ ├── anthropic.svg │ │ │ │ ├── asana.svg │ │ │ │ ├── atlassian.svg │ │ │ │ ├── canva.svg │ │ │ │ ├── cloudflare_logo.svg │ │ │ │ ├── cloudflare.svg │ │ │ │ ├── dina.jpg │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── favicon.png │ │ │ │ ├── intercom.svg │ │ │ │ ├── linear.svg │ │ │ │ ├── matt.jpg │ │ │ │ ├── mcp_demo_day.svg │ │ │ │ ├── mcpog.png │ │ │ │ ├── more.svg │ │ │ │ ├── paypal.svg │ │ │ │ ├── pete.jpeg │ │ │ │ ├── sentry.svg │ │ │ │ ├── special_guest.png │ │ │ │ ├── square.svg │ │ │ │ ├── stripe.svg │ │ │ │ ├── sunil.jpg │ │ │ │ └── webflow.svg │ │ │ ├── script.js │ │ │ └── styles.css │ │ ├── package.json │ │ ├── src │ │ │ └── demo-day.app.ts │ │ ├── tsconfig.json │ │ ├── worker-configuration.d.ts │ │ └── wrangler.json │ ├── dex-analysis │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── dex-analysis.app.ts │ │ │ ├── dex-analysis.context.ts │ │ │ ├── tools │ │ │ │ └── dex-analysis.tools.ts │ │ │ └── warp_diag_reader.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── dns-analytics │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── dns-analytics.app.ts │ │ │ ├── dns-analytics.context.ts │ │ │ └── tools │ │ │ └── dex-analytics.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── docs-autorag │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── docs-autorag.app.ts │ │ │ ├── docs-autorag.context.ts │ │ │ └── tools │ │ │ └── docs-autorag.tools.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── docs-vectorize │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── docs-vectorize.app.ts │ │ │ └── docs-vectorize.context.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── graphql │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── graphql.app.ts │ │ │ ├── graphql.context.ts │ │ │ └── tools │ │ │ └── graphql.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── logpush │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── logpush.app.ts │ │ │ ├── logpush.context.ts │ │ │ └── tools │ │ │ └── logpush.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── radar │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── radar.app.ts │ │ │ ├── radar.context.ts │ │ │ ├── tools │ │ │ │ ├── radar.tools.ts │ │ │ │ └── url-scanner.tools.ts │ │ │ ├── types │ │ │ │ ├── radar.ts │ │ │ │ └── url-scanner.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── sandbox-container │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── container │ │ │ ├── fileUtils.spec.ts │ │ │ ├── fileUtils.ts │ │ │ ├── sandbox.container.app.ts │ │ │ └── tsconfig.json │ │ ├── CONTRIBUTING.md │ │ ├── Dockerfile │ │ ├── evals │ │ │ ├── exec.eval.ts │ │ │ ├── files.eval.ts │ │ │ ├── initialize.eval.ts │ │ │ └── utils.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── server │ │ │ ├── containerHelpers.ts │ │ │ ├── containerManager.ts │ │ │ ├── containerMcp.ts │ │ │ ├── metrics.ts │ │ │ ├── prompts.ts │ │ │ ├── sandbox.server.app.ts │ │ │ ├── sandbox.server.context.ts │ │ │ ├── userContainer.ts │ │ │ ├── utils.spec.ts │ │ │ └── utils.ts │ │ ├── shared │ │ │ ├── consts.ts │ │ │ └── schema.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.evals.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── workers-bindings │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── evals │ │ │ ├── accounts.eval.ts │ │ │ ├── hyperdrive.eval.ts │ │ │ ├── kv_namespaces.eval.ts │ │ │ ├── types.d.ts │ │ │ └── utils.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── bindings.app.ts │ │ │ └── bindings.context.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.evals.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── workers-builds │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── tools │ │ │ │ └── workers-builds.tools.ts │ │ │ ├── workers-builds.app.ts │ │ │ └── workers-builds.context.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vite.config.mts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ └── workers-observability │ ├── .dev.vars.example │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── package.json │ ├── README.md │ ├── src │ │ ├── tools │ │ │ └── workers-observability.tools.ts │ │ ├── workers-observability.app.ts │ │ └── workers-observability.context.ts │ ├── tsconfig.json │ ├── types.d.ts │ ├── vitest.config.ts │ ├── worker-configuration.d.ts │ └── wrangler.jsonc ├── CONTRIBUTING.md ├── implementation-guides │ ├── evals.md │ ├── tools.md │ └── type-validators.md ├── LICENSE ├── package.json ├── packages │ ├── eslint-config │ │ ├── CHANGELOG.md │ │ ├── default.cjs │ │ ├── package.json │ │ └── README.md │ ├── eval-tools │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── runTask.ts │ │ │ ├── scorers.ts │ │ │ └── test-models.ts │ │ ├── tsconfig.json │ │ ├── worker-configuration.d.ts │ │ └── wrangler.json │ ├── mcp-common │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── api │ │ │ │ ├── account.api.ts │ │ │ │ ├── cf1-integration.api.ts │ │ │ │ ├── workers-builds.api.ts │ │ │ │ ├── workers-observability.api.ts │ │ │ │ ├── workers.api.ts │ │ │ │ └── zone.api.ts │ │ │ ├── api-handler.ts │ │ │ ├── api-token-mode.ts │ │ │ ├── cloudflare-api.ts │ │ │ ├── cloudflare-auth.ts │ │ │ ├── cloudflare-oauth-handler.ts │ │ │ ├── config.ts │ │ │ ├── constants.ts │ │ │ ├── durable-kv-store.ts │ │ │ ├── durable-objects │ │ │ │ └── user_details.do.ts │ │ │ ├── env.ts │ │ │ ├── format.spec.ts │ │ │ ├── format.ts │ │ │ ├── get-props.ts │ │ │ ├── mcp-error.ts │ │ │ ├── poll.ts │ │ │ ├── prompts │ │ │ │ └── docs-vectorize.prompts.ts │ │ │ ├── scopes.ts │ │ │ ├── sentry.ts │ │ │ ├── server.ts │ │ │ ├── tools │ │ │ │ ├── account.tools.ts │ │ │ │ ├── d1.tools.ts │ │ │ │ ├── docs-vectorize.tools.ts │ │ │ │ ├── hyperdrive.tools.ts │ │ │ │ ├── kv_namespace.tools.ts │ │ │ │ ├── r2_bucket.tools.ts │ │ │ │ ├── worker.tools.ts │ │ │ │ └── zone.tools.ts │ │ │ ├── types │ │ │ │ ├── cf1-integrations.types.ts │ │ │ │ ├── cloudflare-mcp-agent.types.ts │ │ │ │ ├── d1.types.ts │ │ │ │ ├── hyperdrive.types.ts │ │ │ │ ├── kv_namespace.types.ts │ │ │ │ ├── r2_bucket.types.ts │ │ │ │ ├── shared.types.ts │ │ │ │ ├── tools.types.ts │ │ │ │ ├── workers-builds.types.ts │ │ │ │ ├── workers-logs.types.ts │ │ │ │ └── workers.types.ts │ │ │ ├── utils.spec.ts │ │ │ ├── utils.ts │ │ │ └── v4-api.ts │ │ ├── tests │ │ │ └── utils │ │ │ └── cloudflare-mock.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ └── worker-configuration.d.ts │ ├── mcp-observability │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── analytics-engine.ts │ │ │ ├── index.ts │ │ │ └── metrics.ts │ │ ├── tsconfig.json │ │ └── worker-configuration.d.ts │ ├── tools │ │ ├── .eslintrc.cjs │ │ ├── bin │ │ │ ├── run-changeset-new │ │ │ ├── run-eslint-workers │ │ │ ├── run-fix-deps │ │ │ ├── run-tsc │ │ │ ├── run-turbo │ │ │ ├── run-vitest │ │ │ ├── run-vitest-ci │ │ │ ├── run-wrangler-deploy │ │ │ ├── run-wrangler-types │ │ │ └── runx │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── bin │ │ │ │ └── runx.ts │ │ │ ├── changesets.spec.ts │ │ │ ├── changesets.ts │ │ │ ├── cmd │ │ │ │ └── deploy-published-packages.ts │ │ │ ├── proc.ts │ │ │ ├── test │ │ │ │ ├── fixtures │ │ │ │ │ └── changesets │ │ │ │ │ ├── empty │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── invalid-json │ │ │ │ │ │ └── published-packages.json │ │ │ │ │ ├── invalid-schema │ │ │ │ │ │ └── published-packages.json │ │ │ │ │ └── valid │ │ │ │ │ └── published-packages.json │ │ │ │ └── setup.ts │ │ │ └── tsconfig.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── typescript-config │ ├── CHANGELOG.md │ ├── package.json │ ├── tools.json │ ├── workers-lib.json │ └── workers.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── server.json ├── tsconfig.json ├── turbo.json └── vitest.workspace.ts ``` # Files -------------------------------------------------------------------------------- /apps/dex-analysis/src/tools/dex-analysis.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api' 4 | import { getEnv } from '@repo/mcp-common/src/env' 5 | import { getProps } from '@repo/mcp-common/src/get-props' 6 | 7 | import { getReader } from '../warp_diag_reader' 8 | 9 | import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js' 10 | import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js' 11 | import type { ZodRawShape, ZodTypeAny } from 'zod' 12 | import type { CloudflareDEXMCP } from '../dex-analysis.app' 13 | import type { Env } from '../dex-analysis.context' 14 | 15 | const env = getEnv<Env>() 16 | 17 | export function registerDEXTools(agent: CloudflareDEXMCP) { 18 | registerTool({ 19 | name: 'dex_test_statistics', 20 | description: 'Analyze Cloudflare DEX Test Results by quartile given a Test ID', 21 | schema: { 22 | testId: z.string().describe('The DEX Test ID to analyze details of.'), 23 | from: timeStartParam, 24 | to: timeEndParam, 25 | }, 26 | llmContext: 27 | "The quartiles are sorted by 'resource fetch time' from LEAST performant in quartile 1 to MOST performant in quartile 4. For each quartile-based entry, it provides extensive information about the up-to-20 specific test results that are within that quartile of performance.", 28 | agent, 29 | callback: async ({ accountId, accessToken, ...params }) => { 30 | return await fetchCloudflareApi({ 31 | endpoint: `/dex/test-results/by-quartile?${new URLSearchParams({ ...(params as Record<string, string>) })}`, 32 | accountId, 33 | apiToken: accessToken, 34 | options: { 35 | method: 'GET', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | }, 40 | }) 41 | }, 42 | }) 43 | 44 | registerTool({ 45 | name: 'dex_list_tests', 46 | description: 'Retrieve a list of all Cloudflare DEX Tests configured.', 47 | agent, 48 | schema: { page: pageParam }, 49 | callback: async ({ accountId, accessToken, page }) => { 50 | return await fetchCloudflareApi({ 51 | endpoint: `/dex/tests/overview?page=${page}&per_page=50`, 52 | accountId, 53 | apiToken: accessToken, 54 | options: { 55 | method: 'GET', 56 | headers: { 57 | 'Content-Type': 'application/json', 58 | }, 59 | }, 60 | }) 61 | }, 62 | }) 63 | 64 | registerTool({ 65 | name: 'dex_http_test_details', 66 | description: 'Retrieve detailed time series results for an HTTP DEX test by id.', 67 | schema: { 68 | testId: z.string().describe('The HTTP DEX Test ID to get details for.'), 69 | deviceId: z 70 | .string() 71 | .optional() 72 | .describe( 73 | "Optionally limit results to specific device(s). Can't be used in conjunction with the colo parameter." 74 | ), 75 | colo: z 76 | .string() 77 | .optional() 78 | .describe('Optionally limit results to a specific Cloudflare colo.'), 79 | from: timeStartParam, 80 | to: timeEndParam, 81 | interval: aggregationIntervalParam, 82 | }, 83 | agent, 84 | callback: async ({ testId, accountId, accessToken, ...params }) => { 85 | return await fetchCloudflareApi({ 86 | endpoint: `/dex/http-tests/${testId}?${new URLSearchParams({ ...(params as Record<string, string>) })}`, 87 | accountId, 88 | apiToken: accessToken, 89 | options: { 90 | method: 'GET', 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | }, 94 | }, 95 | }) 96 | }, 97 | }) 98 | 99 | registerTool({ 100 | name: 'dex_traceroute_test_details', 101 | description: 'Retrieve detailed time series results for a Traceroute DEX test by id.', 102 | schema: { 103 | testId: z.string().describe('The traceroute DEX Test ID to get details for.'), 104 | deviceId: z 105 | .string() 106 | .optional() 107 | .describe( 108 | "Optionally limit results to specific device(s). Can't be used in conjunction with the colo parameter." 109 | ), 110 | colo: z 111 | .string() 112 | .optional() 113 | .describe('Optionally limit results to a specific Cloudflare colo.'), 114 | timeStart: timeStartParam, 115 | timeEnd: timeEndParam, 116 | interval: aggregationIntervalParam, 117 | }, 118 | agent, 119 | callback: async ({ testId, accountId, accessToken, ...params }) => { 120 | return await fetchCloudflareApi({ 121 | endpoint: `/dex/traceroute-tests/${testId}?${new URLSearchParams({ ...(params as Record<string, string>) })}`, 122 | accountId, 123 | apiToken: accessToken, 124 | options: { 125 | method: 'GET', 126 | headers: { 127 | 'Content-Type': 'application/json', 128 | }, 129 | }, 130 | }) 131 | }, 132 | }) 133 | 134 | registerTool({ 135 | name: 'dex_traceroute_test_network_path', 136 | description: 137 | 'Retrieve aggregate network path data for a Traceroute DEX test by id. Use the dex_traceroute_test_result_network_path tool to further explore individual test runs hop-by-hop.', 138 | schema: { 139 | testId: z.string().describe('The traceroute DEX Test ID to get network path details for.'), 140 | deviceId: z.string().describe('The ID of the device to get network path details for.'), 141 | from: timeStartParam, 142 | to: timeEndParam, 143 | interval: aggregationIntervalParam, 144 | }, 145 | agent, 146 | callback: async ({ testId, accountId, accessToken, ...params }) => { 147 | return await fetchCloudflareApi({ 148 | endpoint: `/dex/traceroute-tests/${testId}/network-path?${new URLSearchParams({ ...(params as unknown as Record<string, string>) })}`, 149 | accountId, 150 | apiToken: accessToken, 151 | options: { 152 | method: 'GET', 153 | headers: { 154 | 'Content-Type': 'application/json', 155 | }, 156 | }, 157 | }) 158 | }, 159 | }) 160 | 161 | registerTool({ 162 | name: 'dex_traceroute_test_result_network_path', 163 | description: 164 | 'Retrieve the hop-by-hop network path for a specific Traceroute DEX test result by id.', 165 | schema: { 166 | testResultId: z 167 | .string() 168 | .describe('The traceroute DEX Test Result ID to get network path details for.'), 169 | }, 170 | agent, 171 | callback: async ({ testResultId, accountId, accessToken }) => { 172 | return await fetchCloudflareApi({ 173 | endpoint: `/dex/traceroute-test-results/${testResultId}/network-path`, 174 | accountId, 175 | apiToken: accessToken, 176 | options: { 177 | method: 'GET', 178 | headers: { 179 | 'Content-Type': 'application/json', 180 | }, 181 | }, 182 | }) 183 | }, 184 | }) 185 | 186 | registerTool({ 187 | name: 'dex_list_remote_capture_eligible_devices', 188 | description: 189 | "Retrieve a list of devices eligible for remote captures. You'll need the device_id and user_email from this " + 190 | 'response in order to create a remote capture for a specific device. It can also be used as a generic source to find ' + 191 | 'devices registered to the account, filtering by user email if necessary.', 192 | schema: { 193 | page: pageParam, 194 | search: z.string().optional().describe('Filter devices by name or email.'), 195 | }, 196 | agent, 197 | callback: async ({ accountId, accessToken, ...params }) => { 198 | return await fetchCloudflareApi({ 199 | endpoint: `/dex/commands/devices?${new URLSearchParams({ ...(params as unknown as Record<string, string>) })}&per_page=50`, 200 | accountId, 201 | apiToken: accessToken, 202 | options: { 203 | method: 'GET', 204 | headers: { 205 | 'Content-Type': 'application/json', 206 | }, 207 | }, 208 | }) 209 | }, 210 | }) 211 | 212 | registerTool({ 213 | name: 'dex_create_remote_pcap', 214 | description: 215 | 'Create a remote packet capture (PCAP) for a device. This is a resource intensive and privacy-sensitive operation on a real user device.' + 216 | 'Always ask for confirmation from the user that the targeted email and device are correct before executing a capture', 217 | schema: { 218 | device_id: z.string().describe('The device ID to target.'), 219 | user_email: z.string().describe('The email of the user associated with the device.'), 220 | 'max-file-size-mb': z 221 | .number() 222 | .min(1) 223 | .default(5) 224 | .optional() 225 | .describe( 226 | 'Maximum file size in MB for the capture file. Specifies the maximum file size of the warp-daig zip artifact that can be uploaded. ' + 227 | 'If the zip artifact exceeds the specified max file size it will NOT be uploaded.' 228 | ), 229 | 'packet-size-bytes': z 230 | .number() 231 | .min(1) 232 | .default(160) 233 | .optional() 234 | .describe('Maximum number of bytes to save for each packet.'), 235 | 'time-limit-min': z 236 | .number() 237 | .min(1) 238 | .default(5) 239 | .describe('Limit on capture duration in minutes'), 240 | }, 241 | agent, 242 | llmContext: 243 | 'If the request was successful, the capture has been initiated. You can poll the dex_list_remote_commands tool periodically to check on the completion status.', 244 | callback: async ({ accountId, accessToken, device_id, user_email, ...command_args }) => { 245 | return await fetchCloudflareApi({ 246 | endpoint: `/dex/commands`, 247 | accountId, 248 | apiToken: accessToken, 249 | options: { 250 | method: 'POST', 251 | headers: { 252 | 'Content-Type': 'application/json', 253 | }, 254 | body: JSON.stringify({ 255 | commands: [ 256 | { 257 | type: 'pcap', 258 | device_id, 259 | user_email, 260 | args: command_args, 261 | version: 1, 262 | }, 263 | ], 264 | }), 265 | }, 266 | }) 267 | }, 268 | }) 269 | 270 | registerTool({ 271 | name: 'dex_create_remote_warp_diag', 272 | description: 273 | 'Create a remote Warp Diagnostic (WARP-diag) for a device. This is a resource intensive and privacy-sensitive operation on a real user device.' + 274 | 'Always ask for confirmation from the user that the targeted email and device are correct before executing a capture', 275 | schema: { 276 | device_id: z.string().describe('The device ID to target.'), 277 | user_email: z.string().describe('The email of the user associated with the device.'), 278 | 'test-all-routes': z 279 | .boolean() 280 | .default(true) 281 | .describe( 282 | 'Test an IP address from all included or excluded ranges. Tests an IP address from all included or excluded ranges.' + 283 | "Essentially the same as running 'route get '' and collecting the results. This option may increase the time taken to collect the warp-diag" 284 | ), 285 | }, 286 | agent, 287 | llmContext: 288 | 'If the request was successful, the diagnostic has been initiated. You can poll the dex_list_remote_commands tool periodically to check on the completion status.' + 289 | 'See https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/troubleshooting/warp-logs/ for more info on warp-diags', 290 | callback: async ({ accountId, accessToken, device_id, user_email, ...command_args }) => { 291 | return await fetchCloudflareApi({ 292 | endpoint: `/dex/commands`, 293 | accountId, 294 | apiToken: accessToken, 295 | options: { 296 | method: 'POST', 297 | headers: { 298 | 'Content-Type': 'application/json', 299 | }, 300 | body: JSON.stringify({ 301 | commands: [ 302 | { 303 | type: 'warp-diag', 304 | device_id, 305 | user_email, 306 | args: command_args, 307 | version: 1, 308 | }, 309 | ], 310 | }), 311 | }, 312 | }) 313 | }, 314 | }) 315 | 316 | registerTool({ 317 | name: 'dex_list_remote_captures', 318 | description: 319 | 'Retrieve a list of remote captures for device debugging, like PCAPs or WARP Diags.', 320 | schema: { page: pageParam }, 321 | agent, 322 | callback: async ({ accountId, accessToken, page }) => { 323 | return await fetchCloudflareApi({ 324 | endpoint: `/dex/commands?page=${page}&per_page=50`, 325 | accountId, 326 | apiToken: accessToken, 327 | options: { 328 | method: 'GET', 329 | headers: { 330 | 'Content-Type': 'application/json', 331 | }, 332 | }, 333 | }) 334 | }, 335 | }) 336 | 337 | registerTool({ 338 | name: 'dex_fleet_status_live', 339 | description: 340 | 'Retrieve details about the real-time status of the fleet of devices broken down by dimension (mode, status, colo, platform, version)', 341 | schema: { 342 | since_minutes: z 343 | .number() 344 | .min(1) 345 | .max(60) 346 | .default(10) 347 | .describe( 348 | 'Number of minutes before current time to use as cutoff for device states to include.' 349 | ), 350 | colo: z 351 | .string() 352 | .optional() 353 | .describe('Optionally filter results to a specific Cloudflare colo.'), 354 | }, 355 | agent, 356 | callback: async ({ accountId, accessToken, ...params }) => { 357 | return await fetchCloudflareApi({ 358 | endpoint: `/dex/fleet-status/live?${new URLSearchParams({ ...(params as unknown as Record<string, string>) })}`, 359 | accountId, 360 | apiToken: accessToken, 361 | options: { 362 | method: 'GET', 363 | headers: { 364 | 'Content-Type': 'application/json', 365 | }, 366 | }, 367 | }) 368 | }, 369 | }) 370 | 371 | registerTool({ 372 | name: 'dex_fleet_status_over_time', 373 | description: 374 | 'Retrieve aggregate time series details about the status of the fleet of devices, or performance metrics for a specific device, over the specified time period.', 375 | schema: { 376 | from: timeStartParam, 377 | to: timeEndParam, 378 | interval: aggregationIntervalParam, 379 | colo: z 380 | .string() 381 | .optional() 382 | .describe('Filter results to WARP devices connected to a specific colo.'), 383 | device_id: z.string().optional().describe('Filter results to a specific device.'), 384 | }, 385 | agent, 386 | callback: async ({ accountId, accessToken, ...params }) => { 387 | return await fetchCloudflareApi({ 388 | endpoint: `/dex/fleet-status/over-time?${new URLSearchParams({ ...(params as Record<string, string>) })}`, 389 | accountId, 390 | apiToken: accessToken, 391 | options: { 392 | method: 'GET', 393 | headers: { 394 | 'Content-Type': 'application/json', 395 | }, 396 | }, 397 | }) 398 | }, 399 | }) 400 | 401 | registerTool({ 402 | name: 'dex_fleet_status_logs', 403 | description: 404 | 'Retrieve raw fleet status device logs with a variety of levels of granularity and filtering. Use `source=last_seen` to view logs showing the last known ' + 405 | 'state per device within the specified time period. Use `source=hourly` to view logs showing an hourly rollup per device where values are the average value of all' + 406 | 'events within the time period. Use `source=raw` to view all logs for the specified period.', 407 | schema: { 408 | page: pageParam, 409 | from: timeStartParam, 410 | to: timeEndParam, 411 | source: z 412 | .enum(['last_seen', 'hourly', 'raw']) 413 | .describe('Specifies the granularity of results.'), 414 | colo: z 415 | .string() 416 | .optional() 417 | .describe('Filter results to WARP devices connected to a specific colo.'), 418 | device_id: z.string().optional().describe('Filter results to a specific device.'), 419 | mode: z.string().optional().describe('Filter results to devices with a specific WARP mode.'), 420 | platform: z 421 | .string() 422 | .optional() 423 | .describe('Filter results to devices on a specific operating system.'), 424 | status: z 425 | .string() 426 | .optional() 427 | .describe('Filter results to devices with a specific WARP connection status.'), 428 | version: z 429 | .string() 430 | .optional() 431 | .describe('Filter results to devices with a specific WARP client version.'), 432 | }, 433 | agent, 434 | callback: async ({ accountId, accessToken, ...params }) => { 435 | return await fetchCloudflareApi({ 436 | endpoint: `/dex/fleet-status/devices?${new URLSearchParams({ ...(params as unknown as Record<string, string>) })}&per_page=50`, 437 | accountId, 438 | apiToken: accessToken, 439 | options: { 440 | method: 'GET', 441 | headers: { 442 | 'Content-Type': 'application/json', 443 | }, 444 | }, 445 | }) 446 | }, 447 | }) 448 | 449 | registerTool({ 450 | name: 'dex_list_warp_change_events', 451 | description: 'View logs of events when users toggle WARP on or off, or change configurations.', 452 | schema: { 453 | from: timeStartParam, 454 | to: timeEndParam, 455 | page: pageParam, 456 | account_name: z.string().optional().describe('Optionally filter events by account name.'), 457 | config_name: z 458 | .string() 459 | .optional() 460 | .describe( 461 | 'Optionally filter events by WARP configuration name changed from or to. Applicable to `type=config` events only.' 462 | ), 463 | sort_order: z 464 | .enum(['ASC', 'DESC']) 465 | .optional() 466 | .default('ASC') 467 | .describe('Set timestamp sort order.'), 468 | toggle: z 469 | .enum(['on', 'off']) 470 | .optional() 471 | .describe( 472 | 'Optionally filter events by toggle value. Applicable to `type=toggle` events only.' 473 | ), 474 | type: z.enum(['config', 'toggle']).optional().describe('Optionally filter events by type.'), 475 | }, 476 | agent, 477 | callback: async ({ accountId, accessToken, ...params }) => { 478 | return await fetchCloudflareApi({ 479 | endpoint: `/dex/warp-change-events?${new URLSearchParams({ ...(params as unknown as Record<string, string>) })}&per_page=50`, 480 | accountId, 481 | apiToken: accessToken, 482 | options: { 483 | method: 'GET', 484 | headers: { 485 | 'Content-Type': 'application/json', 486 | }, 487 | }, 488 | }) 489 | }, 490 | }) 491 | 492 | registerTool({ 493 | name: 'dex_list_colos', 494 | description: 495 | 'View a list of Cloudflare colos sorted alphabetically or by frequency encountered in fleet status or DEX test data.', 496 | schema: { 497 | from: timeStartParam, 498 | to: timeEndParam, 499 | sortBy: z 500 | .enum(['fleet-status-usage', 'application-tests-usage']) 501 | .optional() 502 | .describe( 503 | 'Use `fleet-status-usage` to sort by frequency seen in device state checkins.' + 504 | 'Use `application-tests-usage` to sort by frequency seen in DEX test results. Omit to sort alphabetically.' 505 | ), 506 | }, 507 | agent, 508 | callback: async ({ accountId, accessToken, ...params }) => { 509 | return await fetchCloudflareApi({ 510 | endpoint: `/dex/colos?${new URLSearchParams({ ...(params as unknown as Record<string, string>) })}`, 511 | accountId, 512 | apiToken: accessToken, 513 | options: { 514 | method: 'GET', 515 | headers: { 516 | 'Content-Type': 'application/json', 517 | }, 518 | }, 519 | }) 520 | }, 521 | }) 522 | 523 | registerTool({ 524 | name: 'dex_list_remote_warp_diag_contents', 525 | description: 526 | 'Given a WARP diag remote capture download url, returns a list of the files contained in the archive.', 527 | schema: { 528 | download: z 529 | .string() 530 | .describe( 531 | 'The `filename` url from the dex_list_remote_captures response for successful WARP diag captures.' 532 | ), 533 | }, 534 | llmContext: 535 | 'Use the dex_explore_remote_warp_diag_output tool for specific file paths to explore the file contents for analysis. ' + 536 | 'Hint: you can call dex_explore_remote_warp_diag_output multiple times in parallel if necessary to take advantage of in-memory caching for best performance.' + 537 | 'See https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/troubleshooting/warp-logs/ for more info on warp-diags', 538 | agent, 539 | callback: async ({ accessToken, download }) => { 540 | const reader = await getReader(env, accessToken, download) 541 | return await reader.list(accessToken, download) 542 | }, 543 | }) 544 | 545 | registerTool({ 546 | name: 'dex_explore_remote_warp_diag_output', 547 | description: 548 | 'Explore the contents of remote capture WARP diag archive filepaths returned by the dex_list_remote_warp_diag_contents tool for analysis.', 549 | schema: { 550 | download: z 551 | .string() 552 | .describe( 553 | 'The `filename` url from the dex_list_remote_captures response for successful WARP diag captures.' 554 | ), 555 | filepath: z.string().describe('The file path from the archive to retrieve contents for.'), 556 | }, 557 | llmContext: 558 | 'To avoid hitting conversation and memory limits, avoid outputting the whole contents of these files to the user unless specifically asked to. Instead prefer to show relevant snippets only.', 559 | agent, 560 | callback: async ({ accessToken, download, filepath }) => { 561 | const reader = await getReader(env, accessToken, download) 562 | return await reader.read(accessToken, download, filepath) 563 | }, 564 | }) 565 | 566 | registerTool({ 567 | name: 'dex_analyze_warp_diag', 568 | description: 569 | 'Analyze successful WARP-diag remote captures for common issues. This should be the first place you start when trying to narrow down device-level issues with WARP.', 570 | schema: { 571 | command_id: z 572 | .string() 573 | .describe('The command_id of the successful WARP-diag remote capture to analyze.'), 574 | }, 575 | llmContext: 576 | 'Detections with 0 occurences can be ruled out. Focus on detections with the highest severity.', 577 | agent, 578 | callback: async ({ accessToken, accountId, command_id }) => { 579 | return await fetchCloudflareApi({ 580 | endpoint: `/dex/commands/${command_id}/analysis`, 581 | accountId, 582 | apiToken: accessToken, 583 | options: { 584 | method: 'GET', 585 | headers: { 586 | 'Content-Type': 'application/json', 587 | }, 588 | }, 589 | }) 590 | }, 591 | }) 592 | } 593 | 594 | // Helper to simplify tool registration by reducing boilerplate for accountId and accessToken 595 | const registerTool = <T extends ZodRawShape, U = unknown>({ 596 | name, 597 | description, 598 | agent, 599 | callback, 600 | schema = {}, 601 | llmContext = '', 602 | }: { 603 | name: string 604 | description: string 605 | schema?: T | ToolAnnotations 606 | llmContext?: string 607 | agent: CloudflareDEXMCP 608 | callback: ( 609 | p: { extra: unknown; accountId: string; accessToken: string } & z.objectOutputType< 610 | T, 611 | ZodTypeAny 612 | > 613 | ) => Promise<U> 614 | }) => { 615 | agent.server.tool<T>(name, description, schema, (async (params, extra) => { 616 | const accountId = await agent.getActiveAccountId() 617 | if (!accountId) { 618 | return { 619 | content: [ 620 | { 621 | type: 'text', 622 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 623 | }, 624 | ], 625 | } 626 | } 627 | 628 | try { 629 | const props = getProps(agent) 630 | const accessToken = props.accessToken 631 | const res = await callback({ ...(params as T), extra, accountId, accessToken }) 632 | return { 633 | content: [ 634 | { 635 | type: 'text', 636 | text: JSON.stringify({ 637 | data: res, 638 | llmContext, 639 | }), 640 | }, 641 | ], 642 | } 643 | } catch (error) { 644 | return { 645 | content: [ 646 | { 647 | type: 'text', 648 | text: JSON.stringify({ 649 | error: `Error with tool ${name}: ${error instanceof Error && error.message}`, 650 | }), 651 | }, 652 | ], 653 | } 654 | } 655 | }) as ToolCallback<T>) 656 | } 657 | 658 | // Shared parameter schemas 659 | const timeStartParam = z 660 | .string() 661 | .describe( 662 | 'The datetime of the beginning point of time range for results. Must be in ISO 8601 datetime string in the extended format with UTC time (e.g, 2025-04-21T18:00:00Z).' 663 | ) 664 | const timeEndParam = z 665 | .string() 666 | .describe( 667 | 'The datetime of the ending point of time range for results. Must be in ISO 8601 datetime string in the extended format with UTC time (e.g, 2025-04-22T00:00:00Z).' 668 | ) 669 | const aggregationIntervalParam = z 670 | .enum(['minute', 'hour']) 671 | .describe('The time interval to group results by.') 672 | 673 | const pageParam = z.number().min(1).describe('The page of results to retrieve.') 674 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/r2_bucket.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getCloudflareClient } from '../cloudflare-api' 2 | import { MISSING_ACCOUNT_ID_RESPONSE } from '../constants' 3 | import { getProps } from '../get-props' 4 | import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types' 5 | import { 6 | BucketListCursorParam, 7 | BucketListDirectionParam, 8 | BucketListNameContainsParam, 9 | BucketListStartAfterParam, 10 | BucketNameSchema, 11 | } from '../types/r2_bucket.types' 12 | import { PaginationPerPageParam } from '../types/shared.types' 13 | 14 | export function registerR2BucketTools(agent: CloudflareMcpAgent) { 15 | agent.server.tool( 16 | 'r2_buckets_list', 17 | 'List r2 buckets in your Cloudflare account', 18 | { 19 | cursor: BucketListCursorParam, 20 | direction: BucketListDirectionParam, 21 | name_contains: BucketListNameContainsParam, 22 | per_page: PaginationPerPageParam, 23 | start_after: BucketListStartAfterParam, 24 | }, 25 | { 26 | title: 'List R2 buckets', 27 | annotations: { 28 | readOnlyHint: true, 29 | }, 30 | }, 31 | async ({ cursor, direction, name_contains, per_page, start_after }) => { 32 | const account_id = await agent.getActiveAccountId() 33 | if (!account_id) { 34 | return MISSING_ACCOUNT_ID_RESPONSE 35 | } 36 | try { 37 | const props = getProps(agent) 38 | const client = getCloudflareClient(props.accessToken) 39 | const listResponse = await client.r2.buckets.list({ 40 | account_id, 41 | cursor: cursor ?? undefined, 42 | direction: direction ?? undefined, 43 | name_contains: name_contains ?? undefined, 44 | per_page: per_page ?? undefined, 45 | start_after: start_after ?? undefined, 46 | }) 47 | 48 | return { 49 | content: [ 50 | { 51 | type: 'text', 52 | text: JSON.stringify({ 53 | buckets: listResponse.buckets, 54 | count: listResponse.buckets?.length ?? 0, 55 | }), 56 | }, 57 | ], 58 | } 59 | } catch (error) { 60 | return { 61 | content: [ 62 | { 63 | type: 'text', 64 | text: `Error listing R2 buckets: ${error instanceof Error && error.message}`, 65 | }, 66 | ], 67 | } 68 | } 69 | } 70 | ) 71 | 72 | agent.server.tool( 73 | 'r2_bucket_create', 74 | 'Create a new r2 bucket in your Cloudflare account', 75 | { name: BucketNameSchema }, 76 | { 77 | title: 'Create R2 bucket', 78 | annotations: { 79 | readOnlyHint: false, 80 | destructiveHint: false, 81 | }, 82 | }, 83 | async ({ name }) => { 84 | const account_id = await agent.getActiveAccountId() 85 | if (!account_id) { 86 | return MISSING_ACCOUNT_ID_RESPONSE 87 | } 88 | try { 89 | const props = getProps(agent) 90 | const client = getCloudflareClient(props.accessToken) 91 | const bucket = await client.r2.buckets.create({ 92 | account_id, 93 | name, 94 | }) 95 | return { 96 | content: [ 97 | { 98 | type: 'text', 99 | text: JSON.stringify(bucket), 100 | }, 101 | ], 102 | } 103 | } catch (error) { 104 | return { 105 | content: [ 106 | { 107 | type: 'text', 108 | text: `Error creating KV namespace: ${error instanceof Error && error.message}`, 109 | }, 110 | ], 111 | } 112 | } 113 | } 114 | ) 115 | 116 | agent.server.tool( 117 | 'r2_bucket_get', 118 | 'Get details about a specific R2 bucket', 119 | { name: BucketNameSchema }, 120 | { 121 | title: 'Get R2 bucket', 122 | annotations: { 123 | readOnlyHint: true, 124 | }, 125 | }, 126 | async ({ name }) => { 127 | const account_id = await agent.getActiveAccountId() 128 | if (!account_id) { 129 | return MISSING_ACCOUNT_ID_RESPONSE 130 | } 131 | try { 132 | const props = getProps(agent) 133 | const client = getCloudflareClient(props.accessToken) 134 | const bucket = await client.r2.buckets.get(name, { account_id }) 135 | return { 136 | content: [ 137 | { 138 | type: 'text', 139 | text: JSON.stringify(bucket), 140 | }, 141 | ], 142 | } 143 | } catch (error) { 144 | return { 145 | content: [ 146 | { 147 | type: 'text', 148 | text: `Error getting R2 bucket: ${error instanceof Error && error.message}`, 149 | }, 150 | ], 151 | } 152 | } 153 | } 154 | ) 155 | 156 | agent.server.tool( 157 | 'r2_bucket_delete', 158 | 'Delete an R2 bucket', 159 | { name: BucketNameSchema }, 160 | { 161 | title: 'Delete R2 bucket', 162 | annotations: { 163 | readOnlyHint: false, 164 | destructiveHint: true, 165 | }, 166 | }, 167 | async ({ name }) => { 168 | const account_id = await agent.getActiveAccountId() 169 | if (!account_id) { 170 | return MISSING_ACCOUNT_ID_RESPONSE 171 | } 172 | try { 173 | const props = getProps(agent) 174 | const client = getCloudflareClient(props.accessToken) 175 | const result = await client.r2.buckets.delete(name, { account_id }) 176 | return { 177 | content: [ 178 | { 179 | type: 'text', 180 | text: JSON.stringify(result), 181 | }, 182 | ], 183 | } 184 | } catch (error) { 185 | return { 186 | content: [ 187 | { 188 | type: 'text', 189 | text: `Error deleting R2 bucket: ${error instanceof Error && error.message}`, 190 | }, 191 | ], 192 | } 193 | } 194 | } 195 | ) 196 | 197 | // Commenting out non-CRUD tools for now to keep the bindings MCP surface small 198 | // agent.server.tool( 199 | // 'r2_bucket_cors_get', 200 | // 'Get CORS configuration for an R2 bucket', 201 | // { 202 | // name: BucketNameSchema, 203 | // params: CorsGetParamsSchema.optional(), 204 | // }, 205 | // async ({ name, params }) => { 206 | // const account_id = await agent.getActiveAccountId() 207 | // if (!account_id) { 208 | // return MISSING_ACCOUNT_ID_RESPONSE 209 | // } 210 | // try { 211 | // const client = getCloudflareClient(props.accessToken) 212 | // const cors = await client.r2.buckets.cors.get(name, { 213 | // account_id, 214 | // ...params, 215 | // }) 216 | // return { 217 | // content: [ 218 | // { 219 | // type: 'text', 220 | // text: JSON.stringify(cors), 221 | // }, 222 | // ], 223 | // } 224 | // } catch (error) { 225 | // return { 226 | // content: [ 227 | // { 228 | // type: 'text', 229 | // text: `Error getting R2 bucket CORS configuration: ${error instanceof Error && error.message}`, 230 | // }, 231 | // ], 232 | // } 233 | // } 234 | // } 235 | // ) 236 | 237 | // agent.server.tool( 238 | // 'r2_bucket_cors_update', 239 | // 'Update CORS configuration for an R2 bucket', 240 | // { 241 | // name: BucketNameSchema, 242 | // cors_config: CorsRulesSchema, 243 | // }, 244 | // async ({ name, cors_config }) => { 245 | // const account_id = await agent.getActiveAccountId() 246 | // if (!account_id) { 247 | // return MISSING_ACCOUNT_ID_RESPONSE 248 | // } 249 | // try { 250 | // const client = getCloudflareClient(props.accessToken) 251 | // const result = await client.r2.buckets.cors.update(name, { 252 | // account_id, 253 | // ...cors_config, 254 | // }) 255 | // return { 256 | // content: [ 257 | // { 258 | // type: 'text', 259 | // text: JSON.stringify(result), 260 | // }, 261 | // ], 262 | // } 263 | // } catch (error) { 264 | // return { 265 | // content: [ 266 | // { 267 | // type: 'text', 268 | // text: `Error updating R2 bucket CORS configuration: ${error instanceof Error && error.message}`, 269 | // }, 270 | // ], 271 | // } 272 | // } 273 | // } 274 | // ) 275 | 276 | // agent.server.tool( 277 | // 'r2_bucket_cors_delete', 278 | // 'Delete CORS configuration for an R2 bucket', 279 | // { 280 | // name: BucketNameSchema, 281 | // params: CorsDeleteParamsSchema.optional(), 282 | // }, 283 | // async ({ name, params }) => { 284 | // const account_id = await agent.getActiveAccountId() 285 | // if (!account_id) { 286 | // return MISSING_ACCOUNT_ID_RESPONSE 287 | // } 288 | // try { 289 | // const client = getCloudflareClient(props.accessToken) 290 | // const result = await client.r2.buckets.cors.delete(name, { 291 | // account_id, 292 | // ...params, 293 | // }) 294 | // return { 295 | // content: [ 296 | // { 297 | // type: 'text', 298 | // text: JSON.stringify(result), 299 | // }, 300 | // ], 301 | // } 302 | // } catch (error) { 303 | // return { 304 | // content: [ 305 | // { 306 | // type: 'text', 307 | // text: `Error deleting R2 bucket CORS configuration: ${error instanceof Error && error.message}`, 308 | // }, 309 | // ], 310 | // } 311 | // } 312 | // } 313 | // ) 314 | 315 | // agent.server.tool( 316 | // 'r2_bucket_domains_list', 317 | // 'List all of the domains for an R2 bucket', 318 | // { name: BucketNameSchema, params: CustomDomainListParamsSchema.optional() }, 319 | // async ({ name, params }) => { 320 | // const account_id = await agent.getActiveAccountId() 321 | // if (!account_id) { 322 | // return MISSING_ACCOUNT_ID_RESPONSE 323 | // } 324 | // try { 325 | // const client = getCloudflareClient(props.accessToken) 326 | // const domains = await client.r2.buckets.domains.custom.list(name, { account_id, ...params }) 327 | // return { 328 | // content: [ 329 | // { 330 | // type: 'text', 331 | // text: JSON.stringify(domains), 332 | // }, 333 | // ], 334 | // } 335 | // } catch (error) { 336 | // return { 337 | // content: [ 338 | // { 339 | // type: 'text', 340 | // text: `Error listing R2 bucket domains: ${error instanceof Error && error.message}`, 341 | // }, 342 | // ], 343 | // } 344 | // } 345 | // } 346 | // ) 347 | 348 | // agent.server.tool( 349 | // 'r2_bucket_domains_get', 350 | // 'Get details about a specific domain for an R2 bucket', 351 | // { 352 | // name: BucketNameSchema, 353 | // domain: CustomDomainNameSchema, 354 | // params: CustomDomainGetParamsSchema.optional(), 355 | // }, 356 | // async ({ name, domain, params }) => { 357 | // const account_id = await agent.getActiveAccountId() 358 | // if (!account_id) { 359 | // return MISSING_ACCOUNT_ID_RESPONSE 360 | // } 361 | // try { 362 | // const client = getCloudflareClient(props.accessToken) 363 | // const result = await client.r2.buckets.domains.custom.get(name, domain, { 364 | // account_id, 365 | // ...params, 366 | // }) 367 | // return { 368 | // content: [ 369 | // { 370 | // type: 'text', 371 | // text: JSON.stringify(result), 372 | // }, 373 | // ], 374 | // } 375 | // } catch (error) { 376 | // return { 377 | // content: [ 378 | // { 379 | // type: 'text', 380 | // text: `Error getting R2 bucket domain: ${error instanceof Error && error.message}`, 381 | // }, 382 | // ], 383 | // } 384 | // } 385 | // } 386 | // ) 387 | 388 | // agent.server.tool( 389 | // 'r2_bucket_domains_create', 390 | // 'Create a new domain for an R2 bucket', 391 | // { name: BucketNameSchema, params: CustomDomainCreateParamsSchema }, 392 | // async ({ name, params }) => { 393 | // const account_id = await agent.getActiveAccountId() 394 | // if (!account_id) { 395 | // return MISSING_ACCOUNT_ID_RESPONSE 396 | // } 397 | // try { 398 | // const client = getCloudflareClient(props.accessToken) 399 | // const result = await client.r2.buckets.domains.custom.create(name, { 400 | // account_id, 401 | // ...params, 402 | // }) 403 | // return { 404 | // content: [ 405 | // { 406 | // type: 'text', 407 | // text: JSON.stringify(result), 408 | // }, 409 | // ], 410 | // } 411 | // } catch (error) { 412 | // return { 413 | // content: [ 414 | // { 415 | // type: 'text', 416 | // text: `Error creating R2 bucket domain: ${error instanceof Error && error.message}`, 417 | // }, 418 | // ], 419 | // } 420 | // } 421 | // } 422 | // ) 423 | 424 | // agent.server.tool( 425 | // 'r2_bucket_domains_delete', 426 | // 'Delete a domain for an R2 bucket', 427 | // { 428 | // name: BucketNameSchema, 429 | // domain: CustomDomainNameSchema, 430 | // params: CustomDomainDeleteParamsSchema.optional(), 431 | // }, 432 | // async ({ name, domain, params }) => { 433 | // const account_id = await agent.getActiveAccountId() 434 | // if (!account_id) { 435 | // return MISSING_ACCOUNT_ID_RESPONSE 436 | // } 437 | // try { 438 | // const client = getCloudflareClient(props.accessToken) 439 | // const result = await client.r2.buckets.domains.custom.delete(name, domain, { 440 | // account_id, 441 | // ...params, 442 | // }) 443 | // return { 444 | // content: [ 445 | // { 446 | // type: 'text', 447 | // text: JSON.stringify(result), 448 | // }, 449 | // ], 450 | // } 451 | // } catch (error) { 452 | // return { 453 | // content: [ 454 | // { 455 | // type: 'text', 456 | // text: `Error deleting R2 bucket domain: ${error instanceof Error && error.message}`, 457 | // }, 458 | // ], 459 | // } 460 | // } 461 | // } 462 | // ) 463 | 464 | // agent.server.tool( 465 | // 'r2_bucket_domains_update', 466 | // 'Update a domain for an R2 bucket', 467 | // { 468 | // name: BucketNameSchema, 469 | // domain: CustomDomainNameSchema, 470 | // params: CustomDomainUpdateParamsSchema, 471 | // }, 472 | // async ({ name, domain, params }) => { 473 | // const account_id = await agent.getActiveAccountId() 474 | // if (!account_id) { 475 | // return MISSING_ACCOUNT_ID_RESPONSE 476 | // } 477 | // try { 478 | // const client = getCloudflareClient(props.accessToken) 479 | // const result = await client.r2.buckets.domains.custom.update(name, domain, { 480 | // account_id, 481 | // ...params, 482 | // }) 483 | // return { 484 | // content: [ 485 | // { 486 | // type: 'text', 487 | // text: JSON.stringify(result), 488 | // }, 489 | // ], 490 | // } 491 | // } catch (error) { 492 | // return { 493 | // content: [ 494 | // { 495 | // type: 'text', 496 | // text: `Error updating R2 bucket domain: ${error instanceof Error && error.message}`, 497 | // }, 498 | // ], 499 | // } 500 | // } 501 | // } 502 | // ) 503 | 504 | // agent.server.tool( 505 | // 'r2_bucket_event_notifications_get', 506 | // 'Get event notifications for an R2 bucket', 507 | // { name: BucketNameSchema, params: EventNotificationGetParamsSchema.optional() }, 508 | // async ({ name, params }) => { 509 | // const account_id = await agent.getActiveAccountId() 510 | // if (!account_id) { 511 | // return MISSING_ACCOUNT_ID_RESPONSE 512 | // } 513 | // try { 514 | // const client = getCloudflareClient(props.accessToken) 515 | // const result = await client.r2.buckets.eventNotifications.get(name, { 516 | // account_id, 517 | // ...params, 518 | // }) 519 | // return { 520 | // content: [ 521 | // { 522 | // type: 'text', 523 | // text: JSON.stringify(result), 524 | // }, 525 | // ], 526 | // } 527 | // } catch (error) { 528 | // return { 529 | // content: [ 530 | // { 531 | // type: 'text', 532 | // text: `Error getting R2 bucket event notifications: ${error instanceof Error && error.message}`, 533 | // }, 534 | // ], 535 | // } 536 | // } 537 | // } 538 | // ) 539 | 540 | // agent.server.tool( 541 | // 'r2_bucket_event_notifications_update', 542 | // 'Update event notifications for an R2 bucket', 543 | // { 544 | // name: BucketNameSchema, 545 | // queueId: QueueIdSchema, 546 | // params: EventNotificationUpdateParamsSchema.optional(), 547 | // }, 548 | // async ({ name, queueId, params }) => { 549 | // const account_id = await agent.getActiveAccountId() 550 | // if (!account_id) { 551 | // return MISSING_ACCOUNT_ID_RESPONSE 552 | // } 553 | // try { 554 | // const client = getCloudflareClient(props.accessToken) 555 | // const result = await client.r2.buckets.eventNotifications.update(name, queueId, { 556 | // account_id, 557 | // ...params, 558 | // }) 559 | // return { 560 | // content: [ 561 | // { 562 | // type: 'text', 563 | // text: JSON.stringify(result), 564 | // }, 565 | // ], 566 | // } 567 | // } catch (error) { 568 | // return { 569 | // content: [ 570 | // { 571 | // type: 'text', 572 | // text: `Error updating R2 bucket event notifications: ${error instanceof Error && error.message}`, 573 | // }, 574 | // ], 575 | // } 576 | // } 577 | // } 578 | // ) 579 | 580 | // agent.server.tool( 581 | // 'r2_bucket_event_notifications_delete', 582 | // 'Delete event notifications for an R2 bucket', 583 | // { 584 | // name: BucketNameSchema, 585 | // queueId: QueueIdSchema, 586 | // params: EventNotificationDeleteParamsSchema.optional(), 587 | // }, 588 | // async ({ name, queueId, params }) => { 589 | // const account_id = await agent.getActiveAccountId() 590 | // if (!account_id) { 591 | // return MISSING_ACCOUNT_ID_RESPONSE 592 | // } 593 | // try { 594 | // const client = getCloudflareClient(props.accessToken) 595 | // const result = await client.r2.buckets.eventNotifications.delete(name, queueId, { 596 | // account_id, 597 | // ...params, 598 | // }) 599 | // return { 600 | // content: [ 601 | // { 602 | // type: 'text', 603 | // text: JSON.stringify(result), 604 | // }, 605 | // ], 606 | // } 607 | // } catch (error) { 608 | // return { 609 | // content: [ 610 | // { 611 | // type: 'text', 612 | // text: `Error deleting R2 bucket event notifications: ${error instanceof Error && error.message}`, 613 | // }, 614 | // ], 615 | // } 616 | // } 617 | // } 618 | // ) 619 | 620 | // agent.server.tool( 621 | // 'r2_bucket_locks_get', 622 | // 'Get locks for an R2 bucket', 623 | // { name: BucketNameSchema, params: LockGetParamsSchema.optional() }, 624 | // async ({ name, params }) => { 625 | // const account_id = await agent.getActiveAccountId() 626 | // if (!account_id) { 627 | // return MISSING_ACCOUNT_ID_RESPONSE 628 | // } 629 | // try { 630 | // const client = getCloudflareClient(props.accessToken) 631 | // const result = await client.r2.buckets.locks.get(name, { account_id, ...params }) 632 | // return { 633 | // content: [ 634 | // { 635 | // type: 'text', 636 | // text: JSON.stringify(result), 637 | // }, 638 | // ], 639 | // } 640 | // } catch (error) { 641 | // return { 642 | // content: [ 643 | // { 644 | // type: 'text', 645 | // text: `Error getting R2 bucket locks: ${error instanceof Error && error.message}`, 646 | // }, 647 | // ], 648 | // } 649 | // } 650 | // } 651 | // ) 652 | 653 | // agent.server.tool( 654 | // 'r2_bucket_locks_update', 655 | // 'Update locks for an R2 bucket', 656 | // { name: BucketNameSchema, params: LockUpdateParamsSchema }, 657 | // async ({ name, params }) => { 658 | // const account_id = await agent.getActiveAccountId() 659 | // if (!account_id) { 660 | // return MISSING_ACCOUNT_ID_RESPONSE 661 | // } 662 | // try { 663 | // const client = getCloudflareClient(props.accessToken) 664 | // const result = await client.r2.buckets.locks.update(name, { account_id, ...params }) 665 | // return { 666 | // content: [ 667 | // { 668 | // type: 'text', 669 | // text: JSON.stringify(result), 670 | // }, 671 | // ], 672 | // } 673 | // } catch (error) { 674 | // return { 675 | // content: [ 676 | // { 677 | // type: 'text', 678 | // text: `Error updating R2 bucket locks: ${error instanceof Error && error.message}`, 679 | // }, 680 | // ], 681 | // } 682 | // } 683 | // } 684 | // ) 685 | 686 | // agent.server.tool( 687 | // 'r2_bucket_temporary_credentials_create', 688 | // 'Create temporary credentials for an R2 bucket', 689 | // { params: TemporaryCredentialsCreateParamsSchema }, 690 | // async ({ params }) => { 691 | // const account_id = await agent.getActiveAccountId() 692 | // if (!account_id) { 693 | // return MISSING_ACCOUNT_ID_RESPONSE 694 | // } 695 | // try { 696 | // const client = getCloudflareClient(props.accessToken) 697 | // const result = await client.r2.temporaryCredentials.create({ 698 | // account_id, 699 | // ...params, 700 | // }) 701 | // return { 702 | // content: [ 703 | // { 704 | // type: 'text', 705 | // text: JSON.stringify(result), 706 | // }, 707 | // ], 708 | // } 709 | // } catch (error) { 710 | // return { 711 | // content: [ 712 | // { 713 | // type: 'text', 714 | // text: `Error creating temporary credentials for R2 bucket: ${error instanceof Error && error.message}`, 715 | // }, 716 | // ], 717 | // } 718 | // } 719 | // } 720 | // ) 721 | 722 | // agent.server.tool('r2_metrics_list', 'List metrics for an R2 bucket', async () => { 723 | // const account_id = await agent.getActiveAccountId() 724 | // if (!account_id) { 725 | // return MISSING_ACCOUNT_ID_RESPONSE 726 | // } 727 | // try { 728 | // const client = getCloudflareClient(props.accessToken) 729 | // const result = await client.r2.buckets.metrics.list({ account_id }) 730 | // return { 731 | // content: [ 732 | // { 733 | // type: 'text', 734 | // text: JSON.stringify(result), 735 | // }, 736 | // ], 737 | // } 738 | // } catch (error) { 739 | // return { 740 | // content: [ 741 | // { 742 | // type: 'text', 743 | // text: `Error listing R2 bucket metrics: ${error instanceof Error && error.message}`, 744 | // }, 745 | // ], 746 | // } 747 | // } 748 | // }) 749 | 750 | // agent.server.tool( 751 | // 'r2_sippy_get', 752 | // 'Get configuration for sippy for an R2 bucket', 753 | // { bucketName: BucketNameSchema, params: SippyGetParamsSchema.optional() }, 754 | // async ({ bucketName, params }) => { 755 | // const account_id = await agent.getActiveAccountId() 756 | // if (!account_id) { 757 | // return MISSING_ACCOUNT_ID_RESPONSE 758 | // } 759 | // try { 760 | // const client = getCloudflareClient(props.accessToken) 761 | // const result = await client.r2.buckets.sippy.get(bucketName, { account_id, ...params }) 762 | // console.log('sippy get result', result) 763 | // return { 764 | // content: [ 765 | // { 766 | // type: 'text', 767 | // text: JSON.stringify(result ?? null), 768 | // }, 769 | // ], 770 | // } 771 | // } catch (error) { 772 | // return { 773 | // content: [ 774 | // { 775 | // type: 'text', 776 | // text: `Error getting R2 bucket sippy: ${error instanceof Error && error.message}`, 777 | // }, 778 | // ], 779 | // } 780 | // } 781 | // } 782 | // ) 783 | 784 | // agent.server.tool( 785 | // 'r2_sippy_update', 786 | // 'Update configuration for sippy for an R2 bucket', 787 | // { bucketName: BucketNameSchema, params: SippyUpdateParamsSchema }, 788 | // async ({ bucketName, params }) => { 789 | // const account_id = await agent.getActiveAccountId() 790 | // if (!account_id) { 791 | // return MISSING_ACCOUNT_ID_RESPONSE 792 | // } 793 | // try { 794 | // const client = getCloudflareClient(props.accessToken) 795 | // const result = await client.r2.buckets.sippy.update(bucketName, { account_id, ...params }) 796 | // return { 797 | // content: [ 798 | // { 799 | // type: 'text', 800 | // text: JSON.stringify(result), 801 | // }, 802 | // ], 803 | // } 804 | // } catch (error) { 805 | // return { 806 | // content: [ 807 | // { 808 | // type: 'text', 809 | // text: `Error updating R2 bucket sippy: ${error instanceof Error && error.message}`, 810 | // }, 811 | // ], 812 | // } 813 | // } 814 | // } 815 | // ) 816 | 817 | // agent.server.tool( 818 | // 'r2_sippy_delete', 819 | // 'Delete sippy for an R2 bucket', 820 | // { bucketName: BucketNameSchema }, 821 | // async ({ bucketName }) => { 822 | // const account_id = await agent.getActiveAccountId() 823 | // if (!account_id) { 824 | // return MISSING_ACCOUNT_ID_RESPONSE 825 | // } 826 | // try { 827 | // const client = getCloudflareClient(props.accessToken) 828 | // const result = await client.r2.buckets.sippy.delete(bucketName, { account_id }) 829 | // return { 830 | // content: [ 831 | // { 832 | // type: 'text', 833 | // text: JSON.stringify(result), 834 | // }, 835 | // ], 836 | // } 837 | // } catch (error) { 838 | // return { 839 | // content: [ 840 | // { 841 | // type: 'text', 842 | // text: `Error deleting R2 bucket sippy: ${error instanceof Error && error.message}`, 843 | // }, 844 | // ], 845 | // } 846 | // } 847 | // } 848 | // ) 849 | } 850 | ``` -------------------------------------------------------------------------------- /apps/graphql/src/tools/graphql.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as LZString from 'lz-string' 2 | import { z } from 'zod' 3 | 4 | import { getProps } from '@repo/mcp-common/src/get-props' 5 | 6 | import type { GraphQLMCP } from '../graphql.app' 7 | 8 | // GraphQL API endpoint 9 | const CLOUDFLARE_GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql' 10 | 11 | // Type definitions for GraphQL schema responses 12 | interface GraphQLTypeRef { 13 | kind: string 14 | name: string | null 15 | ofType?: GraphQLTypeRef | null 16 | } 17 | 18 | interface GraphQLField { 19 | name: string 20 | description: string | null 21 | args: Array<{ 22 | name: string 23 | description: string | null 24 | type: GraphQLTypeRef 25 | }> 26 | type: GraphQLTypeRef 27 | } 28 | 29 | interface GraphQLType { 30 | name: string 31 | kind: string 32 | description: string | null 33 | fields?: GraphQLField[] | null 34 | inputFields?: Array<{ 35 | name: string 36 | description: string | null 37 | type: GraphQLTypeRef 38 | }> | null 39 | interfaces?: Array<{ name: string }> | null 40 | enumValues?: Array<{ 41 | name: string 42 | description: string | null 43 | }> | null 44 | possibleTypes?: Array<{ name: string }> | null 45 | } 46 | 47 | interface SchemaOverviewResponse { 48 | data: { 49 | __schema: { 50 | queryType: { name: string } | null 51 | mutationType: { name: string } | null 52 | subscriptionType: { name: string } | null 53 | types: Array<{ 54 | name: string 55 | kind: string 56 | description: string | null 57 | }> 58 | } 59 | } 60 | } 61 | 62 | interface TypeDetailsResponse { 63 | data: { 64 | __type: GraphQLType 65 | } 66 | } 67 | 68 | // Define the structure of a single error 69 | const graphQLErrorSchema = z.object({ 70 | message: z.string(), 71 | path: z.array(z.union([z.string(), z.number()])), 72 | extensions: z.object({ 73 | code: z.string(), 74 | timestamp: z.string(), 75 | ray_id: z.string(), 76 | }), 77 | }) 78 | 79 | // Define the overall GraphQL response schema 80 | const graphQLResponseSchema = z.object({ 81 | data: z.union([z.record(z.unknown()), z.null()]), 82 | errors: z.union([z.array(graphQLErrorSchema), z.null()]), 83 | }) 84 | 85 | /** 86 | * Fetches the high-level overview of the GraphQL schema 87 | * @param apiToken Cloudflare API token 88 | * @returns Basic schema structure 89 | */ 90 | async function fetchSchemaOverview(apiToken: string): Promise<SchemaOverviewResponse> { 91 | const overviewQuery = ` 92 | query SchemaOverview { 93 | __schema { 94 | queryType { name } 95 | mutationType { name } 96 | subscriptionType { name } 97 | types { 98 | name 99 | kind 100 | description 101 | } 102 | } 103 | } 104 | ` 105 | 106 | const response = await executeGraphQLRequest<SchemaOverviewResponse>(overviewQuery, apiToken) 107 | return response 108 | } 109 | 110 | /** 111 | * Fetches detailed information about a specific GraphQL type 112 | * @param typeName The name of the type to fetch details for 113 | * @param apiToken Cloudflare API token 114 | * @returns Detailed type information 115 | */ 116 | async function fetchTypeDetails(typeName: string, apiToken: string): Promise<TypeDetailsResponse> { 117 | const typeDetailsQuery = ` 118 | query TypeDetails { 119 | __type(name: "${typeName}") { 120 | name 121 | kind 122 | description 123 | fields(includeDeprecated: false) { 124 | name 125 | description 126 | args { 127 | name 128 | description 129 | type { 130 | kind 131 | name 132 | ofType { 133 | kind 134 | name 135 | } 136 | } 137 | } 138 | type { 139 | kind 140 | name 141 | ofType { 142 | kind 143 | name 144 | ofType { 145 | kind 146 | name 147 | } 148 | } 149 | } 150 | } 151 | inputFields { 152 | name 153 | description 154 | type { 155 | kind 156 | name 157 | ofType { 158 | kind 159 | name 160 | } 161 | } 162 | } 163 | interfaces { 164 | name 165 | } 166 | enumValues(includeDeprecated: false) { 167 | name 168 | description 169 | } 170 | possibleTypes { 171 | name 172 | } 173 | } 174 | } 175 | ` 176 | 177 | const response = await executeGraphQLRequest<TypeDetailsResponse>(typeDetailsQuery, apiToken) 178 | return response 179 | } 180 | 181 | /** 182 | * Helper function to execute GraphQL requests 183 | * @param query GraphQL query to execute 184 | * @param apiToken Cloudflare API token 185 | * @returns Response data 186 | */ 187 | async function executeGraphQLRequest<T>(query: string, apiToken: string): Promise<T> { 188 | const response = await fetch(CLOUDFLARE_GRAPHQL_ENDPOINT, { 189 | method: 'POST', 190 | headers: { 191 | 'Content-Type': 'application/json', 192 | Authorization: `Bearer ${apiToken}`, 193 | }, 194 | body: JSON.stringify({ query }), 195 | }) 196 | 197 | if (!response.ok) { 198 | throw new Error(`Failed to execute GraphQL request: ${response.statusText}`) 199 | } 200 | 201 | const data = graphQLResponseSchema.parse(await response.json()) 202 | 203 | // Check for GraphQL errors in the response 204 | if (data && data.errors && Array.isArray(data.errors) && data.errors.length > 0) { 205 | const errorMessages = data.errors.map((e: { message: string }) => e.message).join(', ') 206 | console.warn(`GraphQL errors: ${errorMessages}`) 207 | 208 | // If the error is about mutations not being supported, we can handle it gracefully 209 | if (errorMessages.includes('Mutations are not supported')) { 210 | console.info('Mutations are not supported by the Cloudflare GraphQL API') 211 | } 212 | } 213 | 214 | return data as T 215 | } 216 | 217 | /** 218 | * Executes a GraphQL query against Cloudflare's API 219 | * @param query The GraphQL query to execute 220 | * @param variables Variables for the query 221 | * @param apiToken Cloudflare API token 222 | * @returns The query results 223 | */ 224 | async function executeGraphQLQuery(query: string, variables: any, apiToken: string) { 225 | // Clone the variables to avoid modifying the original 226 | const queryVariables = { ...variables } 227 | 228 | const response = await fetch(CLOUDFLARE_GRAPHQL_ENDPOINT, { 229 | method: 'POST', 230 | headers: { 231 | 'Content-Type': 'application/json', 232 | Authorization: `Bearer ${apiToken}`, 233 | }, 234 | body: JSON.stringify({ 235 | query, 236 | variables: queryVariables, 237 | }), 238 | }) 239 | 240 | if (!response.ok) { 241 | throw new Error(`Failed to execute GraphQL query: ${response.statusText}`) 242 | } 243 | 244 | const result = graphQLResponseSchema.parse(await response.json()) 245 | 246 | // Check for GraphQL errors in the response 247 | if (result && result.errors && Array.isArray(result.errors) && result.errors.length > 0) { 248 | const errorMessages = result.errors.map((e: { message: string }) => e.message).join(', ') 249 | console.warn(`GraphQL query errors: ${errorMessages}`) 250 | } 251 | 252 | return result 253 | } 254 | 255 | /** 256 | * Searches for matching types and fields in a GraphQL schema 257 | * @param schema The GraphQL schema to search 258 | * @param keyword The keyword to search for 259 | * @param typeDetails Optional map of type details for deeper searching 260 | * @returns Matching types and fields 261 | */ 262 | async function searchGraphQLSchema( 263 | schema: SchemaOverviewResponse, 264 | keyword: string, 265 | accountId: string, 266 | apiToken: string, 267 | maxDetailsToFetch: number = 10, 268 | onlyObjectTypes: boolean = true 269 | ) { 270 | const normalizedKeyword = keyword.toLowerCase() 271 | const results = { 272 | types: [] as Array<{ 273 | name: string 274 | kind: string 275 | description: string | null 276 | matchReason: string 277 | }>, 278 | fields: [] as Array<{ 279 | typeName: string 280 | fieldName: string 281 | description: string | null 282 | matchReason: string 283 | }>, 284 | enumValues: [] as Array<{ 285 | typeName: string 286 | enumValue: string 287 | description: string | null 288 | matchReason: string 289 | }>, 290 | args: [] as Array<{ 291 | typeName: string 292 | fieldName: string 293 | argName: string 294 | description: string | null 295 | matchReason: string 296 | }>, 297 | } 298 | 299 | // First pass: Search through type names and descriptions 300 | const matchingTypeNames: string[] = [] 301 | 302 | for (const type of schema.data.__schema.types || []) { 303 | // Skip internal types (those starting with __) 304 | if (type.name?.startsWith('__')) continue 305 | 306 | // Check if type name or description matches 307 | if (type.name?.toLowerCase().includes(normalizedKeyword)) { 308 | results.types.push({ 309 | ...type, 310 | matchReason: `Type name contains "${keyword}"`, 311 | }) 312 | matchingTypeNames.push(type.name) 313 | } else if (type.description?.toLowerCase().includes(normalizedKeyword)) { 314 | results.types.push({ 315 | ...type, 316 | matchReason: `Type description contains "${keyword}"`, 317 | }) 318 | matchingTypeNames.push(type.name) 319 | } 320 | } 321 | 322 | // Second pass: For potentially relevant types, fetch details and search deeper 323 | // Start with matching types, then add important schema types if we have capacity 324 | let typesToExamine = [...matchingTypeNames] 325 | 326 | // Add root operation types if they're not already included 327 | const rootTypes = [ 328 | schema.data.__schema.queryType?.name, 329 | schema.data.__schema.mutationType?.name, 330 | schema.data.__schema.subscriptionType?.name, 331 | ].filter(Boolean) as string[] 332 | 333 | for (const rootType of rootTypes) { 334 | if (!typesToExamine.includes(rootType)) { 335 | typesToExamine.push(rootType) 336 | } 337 | } 338 | 339 | // Add object types that might contain relevant fields 340 | const objectTypes = schema.data.__schema.types 341 | .filter((t) => { 342 | // If onlyObjectTypes is true, only include OBJECT types 343 | if (onlyObjectTypes) { 344 | return t.kind === 'OBJECT' && !t.name.startsWith('__') 345 | } 346 | // Otherwise include both OBJECT and INTERFACE types 347 | return (t.kind === 'OBJECT' || t.kind === 'INTERFACE') && !t.name.startsWith('__') 348 | }) 349 | .map((t) => t.name) 350 | 351 | // Combine all potential types to examine, but limit to a reasonable number 352 | typesToExamine = [...new Set([...typesToExamine, ...objectTypes])].slice(0, maxDetailsToFetch) 353 | 354 | // Fetch details for these types and search through their fields 355 | for (const typeName of typesToExamine) { 356 | try { 357 | const typeDetails = await fetchTypeDetails(typeName, apiToken) 358 | const type = typeDetails.data.__type 359 | 360 | if (!type) continue 361 | 362 | // Search through fields 363 | if (type.fields) { 364 | for (const field of type.fields) { 365 | // Check if field name or description matches 366 | if (field.name.toLowerCase().includes(normalizedKeyword)) { 367 | results.fields.push({ 368 | typeName: type.name, 369 | fieldName: field.name, 370 | description: field.description, 371 | matchReason: `Field name contains "${keyword}"`, 372 | }) 373 | } else if (field.description?.toLowerCase().includes(normalizedKeyword)) { 374 | results.fields.push({ 375 | typeName: type.name, 376 | fieldName: field.name, 377 | description: field.description, 378 | matchReason: `Field description contains "${keyword}"`, 379 | }) 380 | } 381 | 382 | // Search through field arguments 383 | if (field.args) { 384 | for (const arg of field.args) { 385 | if (arg.name.toLowerCase().includes(normalizedKeyword)) { 386 | results.args.push({ 387 | typeName: type.name, 388 | fieldName: field.name, 389 | argName: arg.name, 390 | description: arg.description, 391 | matchReason: `Argument name contains "${keyword}"`, 392 | }) 393 | } else if (arg.description?.toLowerCase().includes(normalizedKeyword)) { 394 | results.args.push({ 395 | typeName: type.name, 396 | fieldName: field.name, 397 | argName: arg.name, 398 | description: arg.description, 399 | matchReason: `Argument description contains "${keyword}"`, 400 | }) 401 | } 402 | } 403 | } 404 | } 405 | } 406 | 407 | // Search through enum values 408 | if (type.enumValues) { 409 | for (const enumValue of type.enumValues) { 410 | if (enumValue.name.toLowerCase().includes(normalizedKeyword)) { 411 | results.enumValues.push({ 412 | typeName: type.name, 413 | enumValue: enumValue.name, 414 | description: enumValue.description, 415 | matchReason: `Enum value contains "${keyword}"`, 416 | }) 417 | } else if (enumValue.description?.toLowerCase().includes(normalizedKeyword)) { 418 | results.enumValues.push({ 419 | typeName: type.name, 420 | enumValue: enumValue.name, 421 | description: enumValue.description, 422 | matchReason: `Enum value description contains "${keyword}"`, 423 | }) 424 | } 425 | } 426 | } 427 | } catch (error) { 428 | console.error(`Error fetching details for type ${typeName}:`, error) 429 | } 430 | } 431 | 432 | return results 433 | } 434 | 435 | /** 436 | * Registers GraphQL tools with the MCP server 437 | * @param agent The MCP agent instance 438 | */ 439 | export function registerGraphQLTools(agent: GraphQLMCP) { 440 | // Tool to search the GraphQL schema for types, fields, and enum values matching a keyword 441 | agent.server.tool( 442 | 'graphql_schema_search', 443 | `Search the Cloudflare GraphQL API schema for types, fields, and enum values matching a keyword 444 | 445 | Use this tool when: 446 | 447 | - You are unsure which dataset to use for your query. 448 | - A user is looking for specific types, fields, or enum values in the Cloudflare GraphQL API schema. 449 | 450 | IMPORTANT GUIDELINES: 451 | - DO NOT query for dimensions unless the user explicitly asked to group by or show dimensions. 452 | - Only include fields that the user specifically requested in their query. 453 | - Keep queries as simple as possible while fulfilling the user's request. 454 | 455 | Workflow: 456 | 1. Use this tool to search for dataset types by keyword. 457 | 2. When a relevant dataset type is found, immediately use graphql_schema_details to get the complete structure of that dataset. 458 | 3. After understanding the schema structure, proceed directly to constructing and executing queries using the graphql_query tool. 459 | 4. Do not use graphql_schema_overview or graphql_complete_schema after finding the relevant dataset - these are redundant steps. 460 | 461 | This tool searches the Cloudflare GraphQL API schema for any schema elements (such as object types, field names, or enum options) that match a given keyword. It returns schema fragments and definitions to assist in constructing valid and precise GraphQL queries. 462 | `, 463 | { 464 | keyword: z.string().describe('The keyword to search for in the schema'), 465 | maxDetailsToFetch: z 466 | .number() 467 | .min(1) 468 | .max(50) 469 | .default(10) 470 | .describe('Maximum number of types to fetch details for'), 471 | includeInternalTypes: z 472 | .boolean() 473 | .default(false) 474 | .describe( 475 | 'Whether to include internal types (those starting with __) in the search results' 476 | ), 477 | onlyObjectTypes: z 478 | .boolean() 479 | .default(true) 480 | .describe( 481 | 'Whether to only include OBJECT kind types in the search results with descriptions' 482 | ), 483 | }, 484 | async (params) => { 485 | const { 486 | keyword, 487 | maxDetailsToFetch = 10, 488 | includeInternalTypes = false, 489 | onlyObjectTypes = true, 490 | } = params 491 | const accountId = await agent.getActiveAccountId() 492 | if (!accountId) { 493 | return { 494 | content: [ 495 | { 496 | type: 'text', 497 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 498 | }, 499 | ], 500 | } 501 | } 502 | 503 | try { 504 | const props = getProps(agent) 505 | // First fetch the schema overview 506 | const schemaOverview = await fetchSchemaOverview(props.accessToken) 507 | 508 | // Search the schema for the keyword 509 | const searchResults = await searchGraphQLSchema( 510 | schemaOverview, 511 | keyword, 512 | accountId, 513 | props.accessToken, 514 | maxDetailsToFetch, 515 | onlyObjectTypes 516 | ) 517 | 518 | // Filter out internal types if requested 519 | if (!includeInternalTypes) { 520 | searchResults.types = searchResults.types.filter((t) => !t.name.startsWith('__')) 521 | searchResults.fields = searchResults.fields.filter((f) => !f.typeName.startsWith('__')) 522 | searchResults.enumValues = searchResults.enumValues.filter( 523 | (e) => !e.typeName.startsWith('__') 524 | ) 525 | searchResults.args = searchResults.args.filter((a) => !a.typeName.startsWith('__')) 526 | } 527 | 528 | // Filter out items without descriptions when onlyObjectTypes is true 529 | if (onlyObjectTypes) { 530 | searchResults.types = searchResults.types.filter((t) => { 531 | return t.description && t.description.trim() !== '' 532 | }) 533 | searchResults.fields = searchResults.fields.filter((f) => { 534 | return f.description && f.description.trim() !== '' 535 | }) 536 | searchResults.enumValues = searchResults.enumValues.filter((e) => { 537 | return e.description && e.description.trim() !== '' 538 | }) 539 | searchResults.args = searchResults.args.filter((a) => { 540 | return a.description && a.description.trim() !== '' 541 | }) 542 | } 543 | 544 | // Add summary information 545 | const results = { 546 | keyword, 547 | summary: { 548 | totalMatches: 549 | searchResults.types.length + 550 | searchResults.fields.length + 551 | searchResults.enumValues.length + 552 | searchResults.args.length, 553 | typeMatches: searchResults.types.length, 554 | fieldMatches: searchResults.fields.length, 555 | enumValueMatches: searchResults.enumValues.length, 556 | argumentMatches: searchResults.args.length, 557 | }, 558 | results: searchResults, 559 | } 560 | 561 | return { 562 | content: [ 563 | { 564 | type: 'text', 565 | text: JSON.stringify(results), 566 | }, 567 | ], 568 | } 569 | } catch (error) { 570 | return { 571 | content: [ 572 | { 573 | type: 'text', 574 | text: JSON.stringify({ 575 | error: `Error searching GraphQL schema: ${error instanceof Error ? error.message : String(error)}`, 576 | }), 577 | }, 578 | ], 579 | } 580 | } 581 | } 582 | ) 583 | 584 | // Tool to fetch the GraphQL schema overview (high-level structure) 585 | agent.server.tool( 586 | 'graphql_schema_overview', 587 | `Fetch the high-level overview of the Cloudflare GraphQL API schema 588 | 589 | Use this tool when: 590 | 591 | - A user requests insights into the structure or capabilities of Cloudflare’s GraphQL API. 592 | - You need to explore available types, queries, mutations, or schema relationships exposed by Cloudflare’s GraphQL interface. 593 | - You're generating or validating GraphQL queries against Cloudflare’s schema. 594 | - You are troubleshooting or developing integrations with Cloudflare’s API and require up-to-date schema information. 595 | 596 | This tool returns a high-level summary of the Cloudflare GraphQL API schema. It provides a structured outline of API entry points, data models, and relationships to help guide query construction or system integration. 597 | `, 598 | { 599 | pageSize: z 600 | .number() 601 | .min(10) 602 | .max(1000) 603 | .default(100) 604 | .describe('Number of types to return per page'), 605 | page: z.number().min(1).default(1).describe('Page number to fetch'), 606 | }, 607 | async (params) => { 608 | const { pageSize = 100, page = 1 } = params 609 | const accountId = await agent.getActiveAccountId() 610 | if (!accountId) { 611 | return { 612 | content: [ 613 | { 614 | type: 'text', 615 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 616 | }, 617 | ], 618 | } 619 | } 620 | 621 | try { 622 | const props = getProps(agent) 623 | const schemaOverview = await fetchSchemaOverview(props.accessToken) 624 | 625 | // Apply pagination to the types array 626 | const allTypes = schemaOverview.data.__schema.types || [] 627 | const totalTypes = allTypes.length 628 | const totalPages = Math.ceil(totalTypes / pageSize) 629 | 630 | // Calculate start and end indices for the current page 631 | const startIndex = (page - 1) * pageSize 632 | const endIndex = Math.min(startIndex + pageSize, totalTypes) 633 | 634 | // Create a paginated version of the schema 635 | const paginatedSchema = { 636 | data: { 637 | __schema: { 638 | queryType: schemaOverview.data.__schema.queryType, 639 | mutationType: schemaOverview.data.__schema.mutationType, 640 | subscriptionType: schemaOverview.data.__schema.subscriptionType, 641 | types: allTypes.slice(startIndex, endIndex), 642 | }, 643 | }, 644 | pagination: { 645 | page, 646 | pageSize, 647 | totalTypes, 648 | totalPages, 649 | hasNextPage: page < totalPages, 650 | hasPreviousPage: page > 1, 651 | }, 652 | } 653 | 654 | return { 655 | content: [ 656 | { 657 | type: 'text', 658 | text: JSON.stringify(paginatedSchema), 659 | }, 660 | ], 661 | } 662 | } catch (error) { 663 | return { 664 | content: [ 665 | { 666 | type: 'text', 667 | text: JSON.stringify({ 668 | error: `Error fetching GraphQL schema overview: ${error instanceof Error ? error.message : String(error)}`, 669 | }), 670 | }, 671 | ], 672 | } 673 | } 674 | } 675 | ) 676 | 677 | // Tool to fetch detailed information about a specific GraphQL type 678 | agent.server.tool( 679 | 'graphql_type_details', 680 | `Fetch detailed information about a specific GraphQL type (dataset) 681 | 682 | IMPORTANT: After exploring the schema, DO NOT generate overly complicated GraphQL queries that the user didn't explicitly ask for. Only include fields that were specifically requested. 683 | 684 | Use this tool when: 685 | 686 | - You need to explore the fields by the type name (dataset) for detailed information 687 | - You're building or debugging GraphQL queries and want to ensure the correct usage of schema components 688 | - You need contextual information about how a certain concept or object is represented in Cloudflare's GraphQL API. 689 | 690 | Guidelines for query construction: 691 | - Keep queries as simple as possible while fulfilling the user's request 692 | - Only include fields that the user specifically asked for 693 | - Do not add dimensions or additional fields unless explicitly requested 694 | - When in doubt, ask the user for clarification rather than creating a complex query 695 | `, 696 | { 697 | typeName: z 698 | .string() 699 | .describe('The type name (dataset) of the GraphQL type to fetch details for'), 700 | fieldsPageSize: z 701 | .number() 702 | .min(5) 703 | .max(500) 704 | .default(50) 705 | .describe('Number of fields to return per page'), 706 | fieldsPage: z.number().min(1).default(1).describe('Page number for fields to fetch'), 707 | enumValuesPageSize: z 708 | .number() 709 | .min(5) 710 | .max(500) 711 | .default(50) 712 | .describe('Number of enum values to return per page'), 713 | enumValuesPage: z.number().min(1).default(1).describe('Page number for enum values to fetch'), 714 | }, 715 | async (params) => { 716 | const { 717 | typeName, 718 | fieldsPageSize = 50, 719 | fieldsPage = 1, 720 | enumValuesPageSize = 50, 721 | enumValuesPage = 1, 722 | } = params 723 | 724 | const accountId = await agent.getActiveAccountId() 725 | if (!accountId) { 726 | return { 727 | content: [ 728 | { 729 | type: 'text', 730 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 731 | }, 732 | ], 733 | } 734 | } 735 | 736 | try { 737 | const props = getProps(agent) 738 | const typeDetails = await fetchTypeDetails(typeName, props.accessToken) 739 | 740 | // Apply pagination to fields if they exist 741 | const allFields = typeDetails.data.__type.fields || [] 742 | const totalFields = allFields.length 743 | const totalFieldsPages = Math.ceil(totalFields / fieldsPageSize) 744 | 745 | // Calculate start and end indices for the fields page 746 | const fieldsStartIndex = (fieldsPage - 1) * fieldsPageSize 747 | const fieldsEndIndex = Math.min(fieldsStartIndex + fieldsPageSize, totalFields) 748 | 749 | // Apply pagination to enum values if they exist 750 | const allEnumValues = typeDetails.data.__type.enumValues || [] 751 | const totalEnumValues = allEnumValues.length 752 | const totalEnumValuesPages = Math.ceil(totalEnumValues / enumValuesPageSize) 753 | 754 | // Calculate start and end indices for the enum values page 755 | const enumValuesStartIndex = (enumValuesPage - 1) * enumValuesPageSize 756 | const enumValuesEndIndex = Math.min( 757 | enumValuesStartIndex + enumValuesPageSize, 758 | totalEnumValues 759 | ) 760 | 761 | // Create a paginated version of the type details 762 | const paginatedTypeDetails = { 763 | data: { 764 | __type: { 765 | ...typeDetails.data.__type, 766 | fields: allFields.slice(fieldsStartIndex, fieldsEndIndex), 767 | enumValues: allEnumValues.slice(enumValuesStartIndex, enumValuesEndIndex), 768 | }, 769 | }, 770 | pagination: { 771 | fields: { 772 | page: fieldsPage, 773 | pageSize: fieldsPageSize, 774 | totalFields, 775 | totalPages: totalFieldsPages, 776 | hasNextPage: fieldsPage < totalFieldsPages, 777 | hasPreviousPage: fieldsPage > 1, 778 | }, 779 | enumValues: { 780 | page: enumValuesPage, 781 | pageSize: enumValuesPageSize, 782 | totalEnumValues, 783 | totalPages: totalEnumValuesPages, 784 | hasNextPage: enumValuesPage < totalEnumValuesPages, 785 | hasPreviousPage: enumValuesPage > 1, 786 | }, 787 | }, 788 | } 789 | 790 | return { 791 | content: [ 792 | { 793 | type: 'text', 794 | text: JSON.stringify(paginatedTypeDetails), 795 | }, 796 | ], 797 | } 798 | } catch (error) { 799 | return { 800 | content: [ 801 | { 802 | type: 'text', 803 | text: JSON.stringify({ 804 | error: `Error fetching type details: ${error instanceof Error ? error.message : String(error)}`, 805 | }), 806 | }, 807 | ], 808 | } 809 | } 810 | } 811 | ) 812 | 813 | // Tool to fetch the complete GraphQL schema (combines overview and important type details) 814 | agent.server.tool( 815 | 'graphql_complete_schema', 816 | 'Fetch the complete Cloudflare GraphQL API schema (combines overview and important type details)', 817 | { 818 | typesPageSize: z 819 | .number() 820 | .min(10) 821 | .max(500) 822 | .default(100) 823 | .describe('Number of types to return per page'), 824 | typesPage: z.number().min(1).default(1).describe('Page number for types to fetch'), 825 | includeRootTypeDetails: z 826 | .boolean() 827 | .default(true) 828 | .describe('Whether to include detailed information about root types'), 829 | maxTypeDetailsToFetch: z 830 | .number() 831 | .min(0) 832 | .max(10) 833 | .default(3) 834 | .describe('Maximum number of important types to fetch details for'), 835 | }, 836 | async (params) => { 837 | const { 838 | typesPageSize = 100, 839 | typesPage = 1, 840 | includeRootTypeDetails = true, 841 | maxTypeDetailsToFetch = 3, 842 | } = params 843 | 844 | const accountId = await agent.getActiveAccountId() 845 | if (!accountId) { 846 | return { 847 | content: [ 848 | { 849 | type: 'text', 850 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 851 | }, 852 | ], 853 | } 854 | } 855 | 856 | try { 857 | const props = getProps(agent) 858 | // First fetch the schema overview 859 | const schemaOverview = await fetchSchemaOverview(props.accessToken) 860 | 861 | // Apply pagination to the types array 862 | const allTypes = schemaOverview.data.__schema.types || [] 863 | const totalTypes = allTypes.length 864 | const totalPages = Math.ceil(totalTypes / typesPageSize) 865 | 866 | // Calculate start and end indices for the current page 867 | const startIndex = (typesPage - 1) * typesPageSize 868 | const endIndex = Math.min(startIndex + typesPageSize, totalTypes) 869 | 870 | // Get the paginated types 871 | const paginatedTypes = allTypes.slice(startIndex, endIndex) 872 | 873 | // Create the base schema with paginated types 874 | const schema: { 875 | data: { 876 | __schema: { 877 | queryType: { name: string } | null 878 | mutationType: { name: string } | null 879 | subscriptionType: { name: string } | null 880 | types: Array<{ 881 | name: string 882 | kind: string 883 | description: string | null 884 | }> 885 | } 886 | } 887 | typeDetails: Record<string, GraphQLType> 888 | pagination: { 889 | types: { 890 | page: number 891 | pageSize: number 892 | totalTypes: number 893 | totalPages: number 894 | hasNextPage: boolean 895 | hasPreviousPage: boolean 896 | } 897 | } 898 | } = { 899 | data: { 900 | __schema: { 901 | queryType: schemaOverview.data.__schema.queryType, 902 | mutationType: schemaOverview.data.__schema.mutationType, 903 | subscriptionType: schemaOverview.data.__schema.subscriptionType, 904 | types: paginatedTypes, 905 | }, 906 | }, 907 | typeDetails: {} as Record<string, GraphQLType>, 908 | pagination: { 909 | types: { 910 | page: typesPage, 911 | pageSize: typesPageSize, 912 | totalTypes, 913 | totalPages, 914 | hasNextPage: typesPage < totalPages, 915 | hasPreviousPage: typesPage > 1, 916 | }, 917 | }, 918 | } 919 | 920 | // If requested, fetch details for root types 921 | if (includeRootTypeDetails) { 922 | // Identify important root types 923 | const rootTypes = [ 924 | schemaOverview.data.__schema.queryType?.name, 925 | ...(schemaOverview.data.__schema.mutationType?.name 926 | ? [schemaOverview.data.__schema.mutationType.name] 927 | : []), 928 | ].filter(Boolean) as string[] 929 | 930 | // Limit the number of types to fetch details for 931 | const typesToFetch = rootTypes.slice(0, maxTypeDetailsToFetch) 932 | 933 | // Fetch details for each type 934 | for (const typeName of typesToFetch) { 935 | try { 936 | const typeDetails = await fetchTypeDetails(typeName, props.accessToken) 937 | if (typeDetails.data.__type) { 938 | schema.typeDetails[typeName] = typeDetails.data.__type 939 | } 940 | } catch (error) { 941 | console.error(`Error fetching details for type ${typeName}:`, error) 942 | } 943 | } 944 | } 945 | 946 | return { 947 | content: [ 948 | { 949 | type: 'text', 950 | text: JSON.stringify(schema), 951 | }, 952 | ], 953 | } 954 | } catch (error) { 955 | return { 956 | content: [ 957 | { 958 | type: 'text', 959 | text: JSON.stringify({ 960 | error: `Error fetching GraphQL schema: ${error instanceof Error ? error.message : String(error)}`, 961 | }), 962 | }, 963 | ], 964 | } 965 | } 966 | } 967 | ) 968 | 969 | // Tool to execute a GraphQL query 970 | agent.server.tool( 971 | 'graphql_query', 972 | `Execute a GraphQL query against the Cloudflare API 973 | 974 | IMPORTANT: ONLY execute the EXACT GraphQL query provided by the user. DO NOT generate complicated queries that the user didn't explicitly ask for. 975 | 976 | CRITICAL: When querying, make sure to set a LIMIT (e.g., first: 10, limit: 20) otherwise the response may be too large for the MCP server to process. 977 | 978 | Use this tool when: 979 | 980 | - A user provides a GraphQL query and expects real-time data from Cloudflare's API. 981 | - You need to retrieve live information from Cloudflare, such as analytics, logs, account data, or configuration details. 982 | - You want to validate the behavior of a GraphQL query or inspect its runtime results. 983 | 984 | This tool sends a user-defined GraphQL query to the Cloudflare API and returns the raw response exactly as received. When filtering or querying by time, use ISO 8601 datetime format (e.g., "2020-08-03T02:07:05Z"). 985 | 986 | For each query execution, a clickable GraphQL API Explorer link will be provided in the response. Users can click this link to open the query in Cloudflare's GraphQL Explorer interface where they can further modify and experiment with the query. 987 | 988 | Guidelines: 989 | - Only use the exact query provided by the user. Do not modify or expand it unless explicitly requested. 990 | - Always suggest including limits in queries (e.g., first: 10, limit: 20) to prevent response size issues. 991 | - If a query fails due to size limits, advise the user to add or reduce limits in their query. 992 | `, 993 | { 994 | query: z.string().describe('The GraphQL query to execute'), 995 | variables: z.record(z.any()).optional().describe('Variables for the query'), 996 | }, 997 | async (params) => { 998 | const accountId = await agent.getActiveAccountId() 999 | if (!accountId) { 1000 | return { 1001 | content: [ 1002 | { 1003 | type: 'text', 1004 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 1005 | }, 1006 | ], 1007 | } 1008 | } 1009 | 1010 | try { 1011 | const props = getProps(agent) 1012 | const { query, variables = {} } = params 1013 | 1014 | // Execute the GraphQL query and get the raw result 1015 | const result = await executeGraphQLQuery(query, variables, props.accessToken) 1016 | 1017 | // Generate GraphQL API Explorer link for this query 1018 | const compressedQuery = LZString.compressToEncodedURIComponent(query) 1019 | const compressedVariables = LZString.compressToEncodedURIComponent( 1020 | JSON.stringify(variables) 1021 | ) 1022 | const explorerUrl = `https://graphql.cloudflare.com/explorer?query=${compressedQuery}&variables=${compressedVariables}` 1023 | 1024 | // Check if the response is too large (MCP server will fail if > 1MB) 1025 | const resultString = JSON.stringify(result) 1026 | const SIZE_LIMIT = 800000 // Set a safer limit (800KB) to ensure we stay under 1MB 1027 | if (resultString.length > SIZE_LIMIT) { 1028 | return { 1029 | content: [ 1030 | { 1031 | type: 'text', 1032 | text: `ERROR: Query result exceeds size limit (${Math.round(resultString.length / 1024)}KB). MCP server will fail with results larger than 1MB. Please use a lower LIMIT in your GraphQL query to reduce the number of returned items. For example: 1033 | 1034 | - Add 'first: 10' or 'limit: 10' parameters to your query 1035 | - Reduce the number of requested fields 1036 | - Add more specific filters to narrow down results`, 1037 | }, 1038 | ], 1039 | } 1040 | } 1041 | 1042 | return { 1043 | content: [ 1044 | { 1045 | type: 'text', 1046 | text: `${resultString}\n\n**[Open in GraphQL Explorer](${explorerUrl})**\nClick the link above to view and modify this query in the Cloudflare GraphQL API Explorer.`, 1047 | }, 1048 | ], 1049 | } 1050 | } catch (error) { 1051 | return { 1052 | content: [ 1053 | { 1054 | type: 'text', 1055 | text: JSON.stringify({ 1056 | error: `Error executing GraphQL query: ${error instanceof Error ? error.message : String(error)}`, 1057 | }), 1058 | }, 1059 | ], 1060 | } 1061 | } 1062 | } 1063 | ) 1064 | 1065 | // Tool to generate a GraphQL API Explorer link 1066 | agent.server.tool( 1067 | 'graphql_api_explorer', 1068 | `Generate a Cloudflare GraphQL API Explorer link 1069 | 1070 | Use this tool when: 1071 | 1072 | - A user asks for any GraphQL queries and wants to explore them in the Cloudflare GraphQL API Explorer. 1073 | - You want to provide a shareable link to a specific GraphQL query for the user to explore and modify. 1074 | - You need to help the user visualize or interact with GraphQL queries in a user-friendly interface. 1075 | 1076 | This tool generates a direct link to the Cloudflare GraphQL API Explorer with a pre-populated query and variables. 1077 | The response includes a clickable Markdown link that users can click to open the query in Cloudflare's interactive GraphQL playground. 1078 | The original query and variables are also displayed for reference. 1079 | `, 1080 | { 1081 | query: z.string().describe('The GraphQL query to include in the explorer link'), 1082 | variables: z.record(z.any()).optional().describe('Variables for the query in JSON format'), 1083 | }, 1084 | async (params) => { 1085 | try { 1086 | const { query, variables = {} } = params 1087 | 1088 | // Compress the query and variables using lz-string 1089 | const compressedQuery = LZString.compressToEncodedURIComponent(query) 1090 | const compressedVariables = LZString.compressToEncodedURIComponent( 1091 | JSON.stringify(variables) 1092 | ) 1093 | 1094 | // Generate the GraphQL API Explorer URL 1095 | const explorerUrl = `https://graphql.cloudflare.com/explorer?query=${compressedQuery}&variables=${compressedVariables}` 1096 | 1097 | return { 1098 | content: [ 1099 | { 1100 | type: 'text', 1101 | text: `**[Open in GraphQL Explorer](${explorerUrl})**\n\nYou can click the link above to open the Cloudflare GraphQL API Explorer with your query pre-populated.\n\n**Query:**\n\`\`\`graphql\n${query}\n\`\`\`\n\n${Object.keys(variables).length > 0 ? `**Variables:**\n\`\`\`json\n${JSON.stringify(variables, null, 2)}\n\`\`\`\n` : ''}`, 1102 | }, 1103 | ], 1104 | } 1105 | } catch (error) { 1106 | return { 1107 | content: [ 1108 | { 1109 | type: 'text', 1110 | text: JSON.stringify({ 1111 | error: `Error generating GraphQL API Explorer link: ${error instanceof Error ? error.message : String(error)}`, 1112 | }), 1113 | }, 1114 | ], 1115 | } 1116 | } 1117 | } 1118 | ) 1119 | } 1120 | ```