This is page 3 of 25. Use http://codebase.md/cloudflare/mcp-server-cloudflare?lines=false&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 -------------------------------------------------------------------------------- /packages/eval-tools/src/test-models.ts: -------------------------------------------------------------------------------- ```typescript import { createAnthropic } from '@ai-sdk/anthropic' import { AnthropicMessagesModelId } from '@ai-sdk/anthropic/internal' import { createGoogleGenerativeAI } from '@ai-sdk/google' import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal' import { createOpenAI } from '@ai-sdk/openai' import { OpenAIChatModelId } from '@ai-sdk/openai/internal' import { createAiGateway } from 'ai-gateway-provider' import { env } from 'cloudflare:test' import { describe } from 'vitest' import { createWorkersAI } from 'workers-ai-provider' export const factualityModel = getOpenAiModel('gpt-4o') type value2key<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T] type AiTextGenerationModels = Exclude< value2key<AiModels, BaseAiTextGeneration>, value2key<AiModels, BaseAiTextToImage> > function getOpenAiModel(modelName: OpenAIChatModelId) { if (!env.CLOUDFLARE_ACCOUNT_ID || !env.AI_GATEWAY_ID || !env.AI_GATEWAY_TOKEN) { throw new Error('No AI gateway credentials set!') } const aigateway = createAiGateway({ accountId: env.CLOUDFLARE_ACCOUNT_ID, gateway: env.AI_GATEWAY_ID, apiKey: env.AI_GATEWAY_TOKEN, }) const ai = createOpenAI({ apiKey: '', }) const model = aigateway([ai(modelName)]) return { modelName, model, ai } } function getAnthropicModel(modelName: AnthropicMessagesModelId) { const aigateway = createAiGateway({ accountId: env.CLOUDFLARE_ACCOUNT_ID, gateway: env.AI_GATEWAY_ID, apiKey: env.AI_GATEWAY_TOKEN, }) const ai = createAnthropic({ apiKey: '', }) const model = aigateway([ai(modelName)]) return { modelName, model, ai } } function getGeminiModel(modelName: GoogleGenerativeAILanguageModel['modelId']) { if (!env.CLOUDFLARE_ACCOUNT_ID || !env.AI_GATEWAY_ID || !env.AI_GATEWAY_TOKEN) { throw new Error('No AI gateway credentials set!') } const aigateway = createAiGateway({ accountId: env.CLOUDFLARE_ACCOUNT_ID, gateway: env.AI_GATEWAY_ID, apiKey: env.AI_GATEWAY_TOKEN, }) const ai = createGoogleGenerativeAI({ apiKey: '' }) const model = aigateway([ai(modelName)]) return { modelName, model, ai } } function getWorkersAiModel(modelName: AiTextGenerationModels) { if (!env.AI) { throw new Error('No AI binding provided!') } const ai = createWorkersAI({ binding: env.AI }) const model = ai(modelName) return { modelName, model, ai } } export const eachModel = describe.each([ getOpenAiModel('gpt-4o'), getOpenAiModel('gpt-4o-mini'), // getAnthropicModel('claude-3-5-sonnet-20241022'), TODO: The evals pass with anthropic, but our rate limit is so low with AI wholesaling that we can't use it in CI because it's impossible to get a complete run with the current limits getGeminiModel('gemini-2.0-flash'), // llama 3 is somewhat inconsistent //getWorkersAiModel("@cf/meta/llama-3.3-70b-instruct-fp8-fast") // Currently llama 4 is having issues with tool calling //getWorkersAiModel("@cf/meta/llama-4-scout-17b-16e-instruct") ]) ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/types/cf1-integrations.types.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' const Vendor = z.object({ id: z.string(), name: z.string(), display_name: z.string(), description: z.string().nullable(), logo: z.string().nullable(), static_logo: z.string().nullable(), }) const Policy = z.object({ id: z.string(), name: z.string(), permissions: z.array(z.string()), link: z.string().nullable(), dlp_enabled: z.boolean(), }) // Base Integration schema export const Integration = z.object({ id: z.string(), name: z.string(), status: z.enum(['Healthy', 'Unhealthy', 'Initializing', 'Paused']), upgradable: z.boolean(), permissions: z.array(z.string()), vendor: Vendor, policy: Policy, created: z.string(), updated: z.string(), credentials_expiry: z.string().nullable(), last_hydrated: z.string().nullable(), }) // Schema for output: a single integration export const IntegrationResponse = Integration export type zReturnedIntegrationResult = z.infer<typeof IntegrationResponse> // Schema for output: multiple integrations export const IntegrationsResponse = z.array(Integration) export type zReturnedIntegrationsResult = z.infer<typeof IntegrationsResponse> export const AssetCategory = z.object({ id: z.string().uuid(), type: z.string(), vendor: z.string(), service: z.string().nullable(), }) export const AssetDetail = z.object({ id: z.string().uuid(), external_id: z.string(), name: z.string(), link: z.string().nullable(), fields: z.array( z.object({ link: z.string().nullable(), name: z.string(), value: z.any(), }) ), category: AssetCategory, integration: Integration, }) export type zReturnedAssetResult = z.infer<typeof AssetDetail> export const AssetsResponse = z.array(AssetDetail) export type zReturnedAssetsResult = z.infer<typeof AssetsResponse> export const AssetCategoriesResponse = z.array(AssetCategory) export type zReturnedAssetCategoriesResult = z.infer<typeof AssetCategoriesResponse> export const assetCategoryTypeParam = z .enum([ 'Account', 'Alert', 'App', 'Authentication Method', 'Bucket', 'Bucket Iam Permission', 'Bucket Permission', 'Calendar', 'Certificate', 'Channel', 'Commit', 'Content', 'Credential', 'Domain', 'Drive', 'Environment', 'Factor', 'File', 'File Permission', 'Folder', 'Group', 'Incident', 'Instance', 'Issue', 'Label', 'Meeting', 'Message', 'Message Rule', 'Namespace', 'Organization', 'Package', 'Pipeline', 'Project', 'Report', 'Repository', 'Risky User', 'Role', 'Server', 'Site', 'Space', 'Submodule', 'Third Party User', 'User', 'User No Mfa', 'Variable', 'Webhook', 'Workspace', ]) .optional() .describe('Type of cloud resource or service category') export const assetCategoryVendorParam = z .enum([ 'AWS', 'Bitbucket', 'Box', 'Confluence', 'Dropbox', 'GitHub', 'Google Cloud Platform', 'Google Workspace', 'Jira', 'Microsoft', 'Microsoft Azure', 'Okta', 'Salesforce', 'ServiceNow', 'Slack', 'Workday', 'Zoom', ]) .describe('Vendor of the cloud service or resource') ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/server.ts: -------------------------------------------------------------------------------- ```typescript import { isPromise } from 'node:util/types' import { type ServerOptions } from '@modelcontextprotocol/sdk/server/index.js' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { type ZodRawShape } from 'zod' import { MetricsTracker, SessionStart, ToolCall } from '../../mcp-observability/src' import { McpError } from './mcp-error' import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js' import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js' import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js' import type { SentryClient } from './sentry' export class CloudflareMCPServer extends McpServer { private metrics private sentry?: SentryClient constructor({ userId, wae, serverInfo, options, sentry, }: { userId?: string wae: AnalyticsEngineDataset serverInfo: { [x: string]: unknown name: string version: string } options?: ServerOptions sentry?: SentryClient }) { super(serverInfo, options) this.metrics = new MetricsTracker(wae, serverInfo) this.sentry = sentry this.server.oninitialized = () => { const clientInfo = this.server.getClientVersion() const clientCapabilities = this.server.getClientCapabilities() this.metrics.logEvent( new SessionStart({ userId, clientInfo, clientCapabilities, }) ) } this.server.onerror = (e) => { this.recordError(e) } const _tool = this.tool.bind(this) this.tool = (name: string, ...rest: unknown[]): ReturnType<typeof this.tool> => { const toolCb = rest[rest.length - 1] as ToolCallback<ZodRawShape | undefined> const replacementToolCb: ToolCallback<ZodRawShape | undefined> = (arg1, arg2) => { const toolCall = toolCb( arg1 as { [x: string]: any } & RequestHandlerExtra<ServerRequest, ServerNotification>, arg2 ) // There are 4 cases to track: try { if (isPromise(toolCall)) { return toolCall .then((r: any) => { // promise succeeds this.metrics.logEvent( new ToolCall({ toolName: name, userId, }) ) return r }) .catch((e: unknown) => { // promise throws this.trackToolCallError(e, name, userId) throw e }) } else { // non-promise succeeds this.metrics.logEvent( new ToolCall({ toolName: name, userId, }) ) return toolCall } } catch (e: unknown) { // non-promise throws this.trackToolCallError(e, name, userId) throw e } } rest[rest.length - 1] = replacementToolCb // @ts-ignore return _tool(name, ...rest) } } private trackToolCallError(e: unknown, toolName: string, userId?: string) { // placeholder error code let errorCode = -1 if (e instanceof McpError) { errorCode = e.code } this.metrics.logEvent( new ToolCall({ toolName, userId: userId, errorCode: errorCode, }) ) } public recordError(e: unknown) { this.sentry?.recordError(e) } } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/durable-kv-store.ts: -------------------------------------------------------------------------------- ```typescript import type { ZodSchema } from 'zod' export type DurableKVStorageKeys = { [key: string]: ZodSchema } /** * DurableKVStore is a type-safe key/value store backed by Durable Object storage. * * @example * * ```ts * export class MyDurableObject extends DurableObject<Bindings> { * readonly kv * constructor( * readonly state: DurableObjectState, * env: Bindings * ) { * super(state, env) * this.kv = new DurableKVStore({ * state, * prefix: 'meta', * keys: { * // Each key has a matching Zod schema enforcing what's stored * date_key: z.coerce.date(), * // While empty keys will always return null, adding * // `nullable()` allows us to explicitly set it to null * string_key: z.string().nullable(), * number_key: z.number(), * } as const satisfies StorageKeys, * }) * } * * async example(): Promise<void> { * await this.kv.get('number_key') // -> null * this.kv.put('number_key', 5) * await this.kv.get('number_key') // -> 5 * } * } * ``` */ export class DurableKVStore<T extends DurableKVStorageKeys> { private readonly prefix: string private readonly keys: T private readonly state: DurableObjectState constructor({ state, prefix, keys }: { state: DurableObjectState; prefix: string; keys: T }) { this.state = state this.prefix = prefix this.keys = keys } /** Add the prefix to a key (used for get/put operations) */ private addPrefix<K extends keyof T>(key: K): string { if (this.prefix.length > 0) { return `${this.prefix}/${key.toString()}` } return key.toString() } /** * Get a value from KV storage. Returns `null` if the value * is not set (or if it's explicitly set to `null`) */ async get<K extends keyof T>(key: K): Promise<T[K]['_output'] | null> /** * Get a value from KV storage or return the provided * default if they value in storage is unset (undefined). * The default value must match the schema for the given key. * * If defaultValue is explicitly set to undefined, it will still return null (avoid this). * * If the value in storage is null then this will return null instead of the default. */ async get<K extends keyof T>(key: K, defaultValue: T[K]['_output']): Promise<T[K]['_output']> async get<K extends keyof T>( key: K, defaultValue?: T[K]['_output'] ): Promise<T[K]['_output'] | null> { const schema = this.keys[key] if (schema === undefined) { throw new TypeError(`key ${key.toString()} has no matching schema`) } const res = await this.state.storage.get(this.addPrefix(key)) if (res === undefined) { if (defaultValue !== undefined) { return schema.parse(defaultValue) } return null } return schema.parse(res) } /** Write value to KV storage */ put<K extends keyof T>(key: K, value: T[K]['_input']): void { const schema = this.keys[key] if (schema === undefined) { throw new TypeError(`key ${key.toString()} has no matching schema`) } const parsedValue = schema.parse(value) void this.state.storage.put(this.addPrefix(key), parsedValue) } /** * Delete value in KV storage. **Does not need to be awaited** * * @returns `true` if a value was deleted, or `false` if it did not. */ async delete<K extends keyof T>(key: K): Promise<boolean> { return this.state.storage.delete(this.addPrefix(key)) } } ``` -------------------------------------------------------------------------------- /apps/dns-analytics/src/tools/dex-analytics.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' import { getProps } from '@repo/mcp-common/src/get-props' import type { AccountGetParams } from 'cloudflare/resources/accounts/accounts.mjs' import type { ReportGetParams } from 'cloudflare/resources/dns/analytics.mjs' import type { ZoneGetParams } from 'cloudflare/resources/dns/settings.mjs' import type { DNSAnalyticsMCP } from '../dns-analytics.app' function getStartDate(days: number) { const today = new Date() const start_date = new Date(today.setDate(today.getDate() - days)) return start_date.toISOString() } export function registerAnalyticTools(agent: DNSAnalyticsMCP) { // Register DNS Report tool agent.server.tool( 'dns_report', 'Fetch the DNS Report for a given zone since a date', { zone: z.string(), days: z.number(), }, async ({ zone, days }) => { try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const start_date = getStartDate(days) const params: ReportGetParams = { zone_id: zone, metrics: 'responseTimeAvg,queryCount,uncachedCount,staleCount', dimensions: 'responseCode,responseCached', since: start_date, } const result = await client.dns.analytics.reports.get(params) return { content: [ { type: 'text', text: JSON.stringify({ result, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error fetching DNS report: ${error instanceof Error && error.message}`, }, ], } } } ) // Register Account DNS Settings display tool agent.server.tool( 'show_account_dns_settings', 'Show DNS settings for current account', async () => { try { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const params: AccountGetParams = { account_id: accountId, } const result = await client.dns.settings.account.get(params) return { content: [ { type: 'text', text: JSON.stringify({ result, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error fetching DNS report: ${error instanceof Error && error.message}`, }, ], } } } ) // Register Zone DNS Settings display tool agent.server.tool( 'show_zone_dns_settings', 'Show DNS settings for a zone', { zone: z.string(), }, async ({ zone }) => { try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const params: ZoneGetParams = { zone_id: zone, } const result = await client.dns.settings.zone.get(params) return { content: [ { type: 'text', text: JSON.stringify({ result, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error fetching DNS report: ${error instanceof Error && error.message}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/zone.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { handleZonesList } from '../api/zone.api' import { getCloudflareClient } from '../cloudflare-api' import { getProps } from '../get-props' import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types' export function registerZoneTools(agent: CloudflareMcpAgent) { // Tool to list all zones under an account agent.server.tool( 'zones_list', 'List all zones under a Cloudflare account', { name: z.string().optional().describe('Filter zones by name'), status: z .string() .optional() .describe( 'Filter zones by status (active, pending, initializing, moved, deleted, deactivated, read only)' ), page: z.number().min(1).default(1).describe('Page number for pagination'), perPage: z.number().min(5).max(1000).default(50).describe('Number of zones per page'), order: z .string() .default('name') .describe('Field to order results by (name, status, account_name)'), direction: z .enum(['asc', 'desc']) .default('desc') .describe('Direction to order results (asc, desc)'), }, { title: 'List zones', annotations: { readOnlyHint: true, destructiveHint: false, }, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const { page = 1, perPage = 50 } = params const zones = await handleZonesList({ client: getCloudflareClient(props.accessToken), accountId, ...params, }) return { content: [ { type: 'text', text: JSON.stringify({ zones, count: zones.length, page, perPage, accountId, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error listing zones: ${error instanceof Error ? error.message : String(error)}`, }, ], } } } ) // Tool to get zone details by ID agent.server.tool( 'zone_details', 'Get details for a specific Cloudflare zone', { zoneId: z.string().describe('The ID of the zone to get details for'), }, { title: 'Get zone details', annotations: { readOnlyHint: true, destructiveHint: false, }, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const { zoneId } = params const client = getCloudflareClient(props.accessToken) // Use the zones.get method to fetch a specific zone const response = await client.zones.get({ zone_id: zoneId }) return { content: [ { type: 'text', text: JSON.stringify({ zone: response, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error fetching zone details: ${error instanceof Error ? error.message : String(error)}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/types/hyperdrive.types.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import type { ConfigCreateParams } from 'cloudflare/resources/hyperdrive/configs.mjs' // --- Base Field Schemas --- /** Zod schema for a Hyperdrive config ID. */ export const HyperdriveConfigIdSchema = z .string() .describe('The ID of the Hyperdrive configuration') /** Zod schema for a Hyperdrive config name. */ export const HyperdriveConfigNameSchema: z.ZodType<ConfigCreateParams['name']> = z .string() .min(1) .max(64) .regex(/^[a-zA-Z0-9_-]+$/) .describe('The name of the Hyperdrive configuration (alphanumeric, underscore, hyphen)') // --- Origin Field Schemas --- /** Zod schema for the origin database name. */ export const HyperdriveOriginDatabaseSchema: z.ZodType< ConfigCreateParams.PublicDatabase['database'] > = z.string().describe('The database name') /** Zod schema for the origin database host. */ export const HyperdriveOriginHostSchema: z.ZodType<ConfigCreateParams.PublicDatabase['host']> = z .string() .describe('The database host address') /** Zod schema for the origin database port. */ export const HyperdriveOriginPortSchema: z.ZodType<ConfigCreateParams.PublicDatabase['port']> = z .number() .int() .min(1) .max(65535) .describe('The database port') /** Zod schema for the origin database scheme. */ export const HyperdriveOriginSchemeSchema: z.ZodType<ConfigCreateParams.PublicDatabase['scheme']> = z.enum(['postgresql']).describe('The database protocol') /** Zod schema for the origin database user. */ export const HyperdriveOriginUserSchema: z.ZodType<ConfigCreateParams.PublicDatabase['user']> = z .string() .describe('The database user') /** Zod schema for the origin database password. */ export const HyperdriveOriginPasswordSchema: z.ZodType< ConfigCreateParams.PublicDatabase['password'] > = z.string().describe('The database password') // --- Caching Field Schemas (Referencing ConfigCreateParams.HyperdriveHyperdriveCachingEnabled) --- /** Zod schema for disabling caching. */ export const HyperdriveCachingDisabledSchema: z.ZodType< ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['disabled'] > = z.boolean().optional().describe('Whether caching is disabled') /** Zod schema for the maximum cache age. */ export const HyperdriveCachingMaxAgeSchema: z.ZodType< ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['max_age'] > = z.number().int().min(1).optional().describe('Maximum cache age in seconds') /** Zod schema for the stale while revalidate duration. */ export const HyperdriveCachingStaleWhileRevalidateSchema: z.ZodType< ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['stale_while_revalidate'] > = z.number().int().min(1).optional().describe('Stale while revalidate duration in seconds') // --- List Parameter Schemas (Cannot directly type against SDK ConfigListParams which only has account_id) --- /** Zod schema for the list page number. */ export const HyperdriveListParamPageSchema = z .number() .int() .positive() .optional() .describe('Page number of results') /** Zod schema for the list results per page. */ export const HyperdriveListParamPerPageSchema = z .number() .int() .min(1) .max(100) .optional() .describe('Number of results per page') /** Zod schema for the list order field. */ export const HyperdriveListParamOrderSchema = z .enum(['id', 'name']) .optional() .describe('Field to order by') /** Zod schema for the list order direction. */ export const HyperdriveListParamDirectionSchema = z .enum(['asc', 'desc']) .optional() .describe('Direction to order') // --- Tool Parameter Schemas --- ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/format.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it } from 'vitest' import { fmt } from './format' describe('fmt', () => { describe('trim()', () => { it('should return an empty string for an empty input', () => { expect(fmt.trim('')).toBe('') }) it('should trim leading and trailing spaces', () => { expect(fmt.trim(' hello ')).toBe('hello') }) it('should trim leading and trailing newlines', () => { expect(fmt.trim('\n\nhello\n\n')).toBe('hello') }) it('should trim leading/trailing spaces and newlines from each line but not remove empty lines', () => { const input = ` line1 line2 line3 ` const expected = `line1 line2 line3` expect(fmt.trim(input)).toBe(expected) }) it('should handle a string that is already trimmed', () => { expect(fmt.trim('hello\nworld')).toBe('hello\nworld') }) it('should handle a string with only spaces', () => { expect(fmt.trim(' ')).toBe('') }) it('should handle a string with only newlines', () => { expect(fmt.trim('\n\n\n')).toBe('') }) it('should preserve empty lines from the middle', () => { expect(fmt.trim('hello\n\nworld')).toBe('hello\n\nworld') }) }) describe('oneLine()', () => { it('should return an empty string for an empty input', () => { expect(fmt.oneLine('')).toBe('') }) it('should convert a multi-line string to a single line', () => { expect(fmt.oneLine('hello\nworld')).toBe('hello world') }) it('should trim leading/trailing spaces and newlines before joining', () => { expect(fmt.oneLine(' hello \n world \n')).toBe('hello world') }) it('should remove empty lines before joining', () => { expect(fmt.oneLine('hello\n\nworld')).toBe('hello world') }) it('should handle a string that is already a single line', () => { expect(fmt.oneLine('hello world')).toBe('hello world') }) it('should handle a string with only spaces and newlines', () => { expect(fmt.oneLine(' \n \n ')).toBe('') }) }) describe('asTSV()', () => { it('should convert an empty array to an empty string', async () => { expect(await fmt.asTSV([])).toBe('') }) it('should convert an array of one object to a TSV string', async () => { const data = [{ a: 1, b: 'hello' }] expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello') }) it('should convert an array of multiple objects to a TSV string', async () => { const data = [ { a: 1, b: 'hello' }, { a: 2, b: 'world' }, ] expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\tworld') }) it('should handle objects with different keys (using keys from the first object as headers)', async () => { const data = [ { a: 1, b: 'hello' }, { a: 2, c: 'world' }, ] expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\t') expect(await fmt.asTSV(data)).toMatchInlineSnapshot(` "a b 1 hello 2 " `) }) it('should handle values with tabs and newlines (fast-csv should quote them)', async () => { const data = [{ name: 'John\tDoe', description: 'Line1\nLine2' }] expect(await fmt.asTSV(data)).toBe('name\tdescription\n"John\tDoe"\t"Line1\nLine2"') expect(await fmt.asTSV(data)).toMatchInlineSnapshot(` "name description "John Doe" "Line1 Line2"" `) }) it('should handle values with quotes (fast-csv should escape them)', async () => { const data = [{ name: 'James "Jim" Raynor' }] expect(await fmt.asTSV(data)).toBe('name\n"James ""Jim"" Raynor"') expect(await fmt.asTSV(data)).toMatchInlineSnapshot(` "name "James ""Jim"" Raynor"" `) }) }) }) ``` -------------------------------------------------------------------------------- /apps/logpush/src/tools/logpush.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api' import { getProps } from '@repo/mcp-common/src/get-props' import type { LogsMCP } from '../logpush.app' const zJobIdentifier = z.number().int().min(1).optional().describe('Unique id of the job.') const zEnabled = z.boolean().optional().describe('Flag that indicates if the job is enabled.') const zName = z .string() .regex(/^[a-zA-Z0-9\-.]*$/) .max(512) .nullable() .optional() .describe('Optional human readable job name. Not unique.') const zDataset = z .string() .regex(/^[a-zA-Z0-9_-]*$/) .max(256) .nullable() .optional() .describe('Name of the dataset.') const zLastComplete = z .string() .datetime() .nullable() .optional() .describe('Records the last time for which logs have been successfully pushed.') const zLastError = z .string() .datetime() .nullable() .optional() .describe('Records the last time the job failed.') const zErrorMessage = z .string() .nullable() .optional() .describe('If not null, the job is currently failing.') export const zLogpushJob = z .object({ id: zJobIdentifier, enabled: zEnabled, name: zName, dataset: zDataset, last_complete: zLastComplete, last_error: zLastError, error_message: zErrorMessage, }) .nullable() .optional() const zApiResponseCommon = z.object({ success: z.literal(true), errors: z.array(z.object({ message: z.string() })).optional(), }) const zLogPushJobResults = z.array(zLogpushJob).optional() // The complete schema for zone_logpush_job_response_collection export const zLogpushJobResponseCollection = zApiResponseCommon.extend({ result: zLogPushJobResults, }) /** * Fetches available telemetry keys for a specified Cloudflare Worker * @param accountId Cloudflare account ID * @param apiToken Cloudflare API token * @returns List of telemetry keys available for the worker */ export async function handleGetAccountLogPushJobs( accountId: string, apiToken: string ): Promise<z.infer<typeof zLogPushJobResults>> { // Call the Public API const data = await fetchCloudflareApi({ endpoint: `/logpush/jobs`, accountId, apiToken, responseSchema: zLogpushJobResponseCollection, options: { method: 'GET', headers: { 'Content-Type': 'application/json', 'portal-version': '2', }, }, }) const res = data as z.infer<typeof zLogpushJobResponseCollection> return (res.result ?? []).slice(0, 100) } /** * Registers the logs analysis tool with the MCP server * @param server The MCP server instance * @param accountId Cloudflare account ID * @param apiToken Cloudflare API token */ export function registerLogsTools(agent: LogsMCP) { // Register the worker logs analysis tool by worker name agent.server.tool( 'logpush_jobs_by_account_id', `All Logpush jobs by Account ID. You should use this tool when: - You have questions or wish to request information about their Cloudflare Logpush jobs by account - You want a condensed version for the output results of your account's Cloudflare Logpush job This tool returns at most the first 100 jobs. `, {}, async () => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const result = await handleGetAccountLogPushJobs(accountId, props.accessToken) return { content: [ { type: 'text', text: JSON.stringify({ result, }), }, ], } } catch (e) { agent.server.recordError(e) return { content: [ { type: 'text', text: JSON.stringify({ error: `Error analyzing logpush jobs: ${e instanceof Error && e.message}`, }), }, ], } } } ) } ``` -------------------------------------------------------------------------------- /apps/browser-rendering/src/tools/browser.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' import { getProps } from '@repo/mcp-common/src/get-props' import type { BrowserMCP } from '../browser.app' export function registerBrowserTools(agent: BrowserMCP) { agent.server.tool( 'get_url_html_content', 'Get page HTML content', { url: z.string().url(), }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = await client.browserRendering.content.create({ account_id: accountId, url: params.url, }) return { content: [ { type: 'text', text: JSON.stringify({ result: r, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting page html: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'get_url_markdown', 'Get page converted into Markdown', { url: z.string().url(), }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = (await client.post(`/accounts/${accountId}/browser-rendering/markdown`, { body: { url: params.url, }, })) as { result: string } return { content: [ { type: 'text', text: JSON.stringify({ result: r.result, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting page in markdown: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'get_url_screenshot', 'Get page screenshot', { url: z.string().url(), viewport: z .object({ height: z.number().default(600), width: z.number().default(800), }) .optional(), }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) // Cf client appears to be broken, so we use the raw API instead. // const client = getCloudflareClient(props.accessToken) // const r = await client.browserRendering.screenshot.create({ // account_id: accountId, // url: params.url, // viewport: params.viewport, // }) const r = await fetch( `https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/screenshot`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${props.accessToken}`, }, body: JSON.stringify({ url: params.url, viewport: params.viewport, }), } ) const arrayBuffer = await r.arrayBuffer() const base64Image = Buffer.from(arrayBuffer).toString('base64') return { content: [ { type: 'image', mimeType: 'image/png', data: base64Image, }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting page in markdown: ${error instanceof Error && error.message}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /apps/cloudflare-one-casb/src/cf1-casb.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerIntegrationsTools } from './tools/integrations.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './cf1-casb.context' export { UserDetails } const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class CASBMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) registerIntegrationsTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const CloudflareOneCasbScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'teams:read': 'See Cloudflare One Resources', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(CASBMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': CASBMCP.serve('/mcp'), '/sse': CASBMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/autorag/src/autorag.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerAutoRAGTools } from './tools/autorag.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './autorag.context' const env = getEnv<Env>() export { UserDetails } const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class AutoRAGMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) // Register Cloudflare Log Push tools registerAutoRAGTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const LogPushScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'rag:write': 'Grants write level access to AutoRag.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(AutoRAGMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': AutoRAGMCP.serve('/mcp'), '/sse': AutoRAGMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/browser-rendering/src/browser.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerBrowserTools } from './tools/browser.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './browser.context' const env = getEnv<Env>() export { UserDetails } const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class BrowserMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) // Register Cloudflare Log Push tools registerBrowserTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const BrowserScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'browser:write': 'Grants write level access to Browser Rendering.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(BrowserMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': BrowserMCP.serve('/mcp'), '/sse': BrowserMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: BrowserScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/ai-gateway/src/ai-gateway.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerAIGatewayTools } from './tools/ai-gateway.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './ai-gateway.context' const env = getEnv<Env>() export { UserDetails } const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class AIGatewayMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) // Register Cloudflare Log Push tools registerAIGatewayTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const AIGatewayScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'aig:read': 'Grants read level access to AI Gateway.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(AIGatewayMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': AIGatewayMCP.serve('/mcp'), '/sse': AIGatewayMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: AIGatewayScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/auditlogs/src/auditlogs.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerAuditLogTools } from './tools/auditlogs.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './auditlogs.context' const env = getEnv<Env>() export { UserDetails } const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps export type State = { activeAccountId: string | null } export class AuditlogMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) // Register Cloudflare Audit Log tools registerAuditLogTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const AuditlogScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'auditlogs:read': 'See your resource configuration changes.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(AuditlogMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': AuditlogMCP.serve('/mcp'), '/sse': AuditlogMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: AuditlogScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/dex-analysis/src/dex-analysis.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '@repo/mcp-observability' import { registerDEXTools } from './tools/dex-analysis.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './dex-analysis.context' export { UserDetails } export { WarpDiagReader } from './warp_diag_reader' const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class CloudflareDEXMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) registerDEXTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const DexScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'dex:write': 'Grants write level access to DEX resources like tests, fleet status, and remote captures.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(CloudflareDEXMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': CloudflareDEXMCP.serve('/mcp'), '/sse': CloudflareDEXMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: DexScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/sentry.ts: -------------------------------------------------------------------------------- ```typescript import { APIError } from 'cloudflare' import { Toucan, zodErrorsIntegration } from 'toucan-js' import { McpError } from './mcp-error' import type { BaseTransportOptions, Client, ClientOptions, Event, EventHint } from '@sentry/types' import type { Context, Next } from 'hono' import type { Context as SentryContext } from 'toucan-js/dist/types' import type { MCPEnvironment } from './config' function is5xxError(status: number): boolean { return status >= 500 && status <= 599 } export class SentryClient { private sentry: Toucan constructor(sentry: Toucan) { this.sentry = sentry } public recordError(e: unknown) { if (this.sentry) { // ignore errors from McpError and APIError (cloudflare) that have reportToSentry = false, or aren't 5xx errors if (e instanceof McpError) { if (e.reportToSentry === false) { return } } else if (e instanceof APIError) { if (!is5xxError(e.status)) { return } } this.sentry.captureException(e) } } public setUser(userId: string) { this.sentry.setUser({ ...this.sentry.getUser(), user_id: userId }) } } interface BaseBindings { ENVIRONMENT: MCPEnvironment GIT_HASH: string SENTRY_DSN: string SENTRY_ACCESS_CLIENT_ID: string SENTRY_ACCESS_CLIENT_SECRET: string } export interface BaseHonoContext { Bindings: BaseBindings Variables: { sentry?: SentryClient } } export function initSentry<T extends BaseBindings>( env: T, ctx: SentryContext, req?: Request<unknown, CfProperties> ): SentryClient { const sentry = new Toucan({ dsn: env.SENTRY_DSN, request: req, environment: env.ENVIRONMENT, context: ctx, release: env.GIT_HASH, requestDataOptions: { allowedHeaders: [ 'user-agent', 'cf-challenge', 'accept-encoding', 'accept-language', 'cf-ray', 'content-length', 'content-type', 'host', ], // Allow ONLY the “scope” param in order to avoid recording jwt, code, state and any other callback params allowedSearchParams: /^scope$/, }, integrations: [ zodErrorsIntegration({ saveAttachments: true }), { name: 'mcp-api-errors', processEvent( event: Event, _hint: EventHint, _client: Client<ClientOptions<BaseTransportOptions>> ): Event { const processedEvent = applyMcpErrorsToEvent(event) return processedEvent }, }, ], transportOptions: { headers: { 'CF-Access-Client-ID': env.SENTRY_ACCESS_CLIENT_ID, 'CF-Access-Client-Secret': env.SENTRY_ACCESS_CLIENT_SECRET, }, }, }) return new SentryClient(sentry) } export function initSentryWithUser<T extends BaseBindings>( env: T, ctx: SentryContext, userId: string, req?: Request<unknown, CfProperties> ): SentryClient { const sentryClient = initSentry(env, ctx, req) sentryClient.setUser(userId) return sentryClient } export async function useSentry<T extends BaseHonoContext>( c: Context<T>, next: Next ): Promise<void> { c.set('sentry', initSentry(c.env, c.executionCtx, c.req.raw)) await next() } export function setSentryRequestHeaders(sentry: Toucan, req: Request<unknown, CfProperties>) { const colo: string = req.cf && typeof req.cf.colo === 'string' ? req.cf.colo : 'UNKNOWN' sentry.setTag('colo', colo) const ip_address = req.headers.get('cf-connecting-ip') ?? '' const userAgent = req.headers.get('user-agent') ?? '' sentry.setUser({ ...sentry.getUser(), ip_address, userAgent, colo, }) } function applyMcpErrorsToEvent(event: Event): Event { if (event.exception === undefined || event.exception.values === undefined) { return event } if (event.exception instanceof McpError) { try { return { ...event, extra: { ...event.extra, statusCode: event.exception.code, internalMessage: event.exception.internalMessage, }, } } catch (e) { // Hopefully we never throw errors here, but record it // with the event just in case. return { ...event, extra: { ...event.extra, 'McpError sentry integration parse error': { message: `an exception was thrown while processing McpError within applyMcpErrorsToEvent()`, error: e instanceof Error ? `${e.name}: ${e.cause}\n${e.stack}` : 'unknown', }, }, } } } return event } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/docs-vectorize.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import type { CloudflareMcpAgentNoAccount } from '../types/cloudflare-mcp-agent.types' interface RequiredEnv { AI: Ai VECTORIZE: VectorizeIndex } // Always return 10 results for simplicity, don't make it configurable const TOP_K = 10 /** * Registers the docs search tool with the MCP server * @param agent The MCP server instance */ export function registerDocsTools(agent: CloudflareMcpAgentNoAccount, env: RequiredEnv) { agent.server.tool( 'search_cloudflare_documentation', `Search the Cloudflare documentation. This tool should be used to answer any question about Cloudflare products or features, including: - Workers, Pages, R2, Images, Stream, D1, Durable Objects, KV, Workflows, Hyperdrive, Queues - AutoRAG, Workers AI, Vectorize, AI Gateway, Browser Rendering - Zero Trust, Access, Tunnel, Gateway, Browser Isolation, WARP, DDOS, Magic Transit, Magic WAN - CDN, Cache, DNS, Zaraz, Argo, Rulesets, Terraform, Account and Billing Results are returned as semantically similar chunks to the query. `, { query: z.string(), }, { title: 'Search Cloudflare docs', annotations: { readOnlyHint: true, }, }, async ({ query }) => { const results = await queryVectorize(env.AI, env.VECTORIZE, query, TOP_K) const resultsAsXml = results .map((result) => { return `<result> <url>${result.url}</url> <title>${result.title}</title> <text> ${result.text} </text> </result>` }) .join('\n') return { content: [{ type: 'text', text: resultsAsXml }], } } ) // Note: this is a tool instead of a prompt because // prompt support is much less common than tools. agent.server.tool( 'migrate_pages_to_workers_guide', `ALWAYS read this guide before migrating Pages projects to Workers.`, {}, { title: 'Get Pages migration guide', annotations: { readOnlyHint: true, }, }, async () => { const res = await fetch( 'https://developers.cloudflare.com/workers/prompts/pages-to-workers.txt', { cf: { cacheEverything: true, cacheTtl: 3600 }, } ) if (!res.ok) { return { content: [{ type: 'text', text: 'Error: Failed to fetch guide. Please try again.' }], } } return { content: [ { type: 'text', text: await res.text(), }, ], } } ) } async function queryVectorize(ai: Ai, vectorizeIndex: VectorizeIndex, query: string, topK: number) { // Recommendation from: https://ai.google.dev/gemma/docs/embeddinggemma/model_card#prompt_instructions const [queryEmbedding] = await getEmbeddings(ai, ['task: search result | query: ' + query]) const { matches } = await vectorizeIndex.query(queryEmbedding, { topK, returnMetadata: 'all', returnValues: false, }) return matches.map((match, _i) => ({ similarity: Math.min(match.score, 1), id: match.id, url: sourceToUrl(String(match.metadata?.filePath ?? '')), title: String(match.metadata?.title ?? ''), text: String(match.metadata?.text ?? ''), })) } const TOP_DIR = 'src/content/docs' function sourceToUrl(path: string) { const prefix = `${TOP_DIR}/` return ( 'https://developers.cloudflare.com/' + (path.startsWith(prefix) ? path.slice(prefix.length) : path) .replace(/index\.mdx$/, '') .replace(/\.mdx$/, '') ) } async function getEmbeddings(ai: Ai, strings: string[]): Promise<number[][]> { const response = await doWithRetries(() => // @ts-expect-error embeddinggemma not in types yet ai.run('@cf/google/embeddinggemma-300m', { text: strings, }) ) // @ts-expect-error embeddinggemma not in types yet return response.data } /** * @template T * @param {() => Promise<T>} action */ async function doWithRetries<T>(action: () => Promise<T>) { const NUM_RETRIES = 10 const INIT_RETRY_MS = 50 for (let i = 0; i <= NUM_RETRIES; i++) { try { return await action() } catch (e) { // TODO: distinguish between user errors (4xx) and system errors (5xx) console.error(e) if (i === NUM_RETRIES) { throw e } // Exponential backoff with full jitter await scheduler.wait(Math.random() * INIT_RETRY_MS * Math.pow(2, i)) } } // Should never reach here – last loop iteration should return throw new Error('An unknown error occurred') } ``` -------------------------------------------------------------------------------- /apps/logpush/src/logpush.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerLogsTools } from './tools/logpush.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './logpush.context' const env = getEnv<Env>() export { UserDetails } const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class LogsMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) // Register Cloudflare Log Push tools registerLogsTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const LogPushScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'logpush:write': 'Grants read and write access to Logpull and Logpush, and read access to Instant Logs. Note that all Logpush API operations require Logs: Write permission because Logpush jobs contain sensitive information.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(LogsMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': LogsMCP.serve('/mcp'), '/sse': LogsMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/workers-bindings/evals/kv_namespaces.eval.ts: -------------------------------------------------------------------------------- ```typescript import { expect } from 'vitest' import { describeEval } from 'vitest-evals' import { runTask } from '@repo/eval-tools/src/runTask' import { checkFactuality } from '@repo/eval-tools/src/scorers' import { eachModel } from '@repo/eval-tools/src/test-models' import { KV_NAMESPACE_TOOLS } from '@repo/mcp-common/src/tools/kv_namespace.tools' import { initializeClient } from './utils' // Assuming utils.ts will exist here eachModel('$modelName', ({ model }) => { describeEval('Create Cloudflare KV Namespace', { data: async () => [ { input: 'Create a new Cloudflare KV Namespace called "my-test-namespace".', expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_create} tool should be called to create a new kv namespace.`, }, ], task: async (input: string) => { const client = await initializeClient(/* Pass necessary mocks/config */) const { promptOutput, toolCalls } = await runTask(client, model, input) const toolCall = toolCalls.find( (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_create ) expect(toolCall, 'Tool kv_namespace_create was not called').toBeDefined() return promptOutput }, scorers: [checkFactuality], threshold: 1, timeout: 60000, // 60 seconds }) describeEval('List Cloudflare KV Namespaces', { data: async () => [ { input: 'List all my Cloudflare KV Namespaces.', expected: `The ${KV_NAMESPACE_TOOLS.kv_namespaces_list} tool should be called to retrieve the list of kv namespaces. There should be at least one kv namespace in the list.`, }, ], task: async (input: string) => { const client = await initializeClient(/* Pass necessary mocks/config */) const { promptOutput, toolCalls } = await runTask(client, model, input) const toolCall = toolCalls.find( (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespaces_list ) expect(toolCall, 'Tool kv_namespaces_list was not called').toBeDefined() return promptOutput }, scorers: [checkFactuality], threshold: 1, timeout: 60000, // 60 seconds }) describeEval('Rename Cloudflare KV Namespace', { data: async () => [ { input: 'Rename my Cloudflare KV Namespace with ID 1234 to "my-new-test-namespace".', expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_update} tool should be called to rename the kv namespace.`, }, ], task: async (input: string) => { const client = await initializeClient(/* Pass necessary mocks/config */) const { promptOutput, toolCalls } = await runTask(client, model, input) const toolCall = toolCalls.find( (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_update ) expect(toolCall, 'Tool kv_namespace_update was not called').toBeDefined() return promptOutput }, scorers: [checkFactuality], threshold: 1, timeout: 60000, // 60 seconds }) describeEval('Get Cloudflare KV Namespace Details', { data: async () => [ { input: 'Get details of my Cloudflare KV Namespace with ID 1234.', expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_get} tool should be called to retrieve the details of the kv namespace.`, }, ], task: async (input: string) => { const client = await initializeClient(/* Pass necessary mocks/config */) const { promptOutput, toolCalls } = await runTask(client, model, input) const toolCall = toolCalls.find( (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_get ) expect(toolCall, 'Tool kv_namespace_get was not called').toBeDefined() return promptOutput }, scorers: [checkFactuality], threshold: 1, timeout: 60000, // 60 seconds }) describeEval('Delete Cloudflare KV Namespace', { data: async () => [ { input: 'Delete the kv namespace with ID 1234.', expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_delete} tool should be called to delete the kv namespace.`, }, ], task: async (input: string) => { const client = await initializeClient(/* Pass necessary mocks/config */) const { promptOutput, toolCalls } = await runTask(client, model, input) const toolCall = toolCalls.find( (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_delete ) expect(toolCall, 'Tool kv_namespace_delete was not called').toBeDefined() return promptOutput }, scorers: [checkFactuality], threshold: 1, timeout: 60000, // 60 seconds }) }) ``` -------------------------------------------------------------------------------- /apps/radar/src/radar.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { MetricsTracker } from '@repo/mcp-observability' import { BASE_INSTRUCTIONS } from './radar.context' import { registerRadarTools } from './tools/radar.tools' import { registerUrlScannerTools } from './tools/url-scanner.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './radar.context' const env = getEnv<Env>() export { UserDetails } const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class RadarMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, options: { instructions: BASE_INSTRUCTIONS }, }) registerAccountTools(this) registerRadarTools(this) registerUrlScannerTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const RadarScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'radar:read': 'Grants access to read Cloudflare Radar data.', 'url_scanner:write': 'Grants write level access to URL Scanner', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(RadarMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': RadarMCP.serve('/mcp'), '/sse': RadarMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: RadarScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /packages/mcp-observability/src/analytics-engine.ts: -------------------------------------------------------------------------------- ```typescript export type MetricsBindings = { MCP_METRICS: AnalyticsEngineDataset } /** * Generic metrics event utilities * @description Wrapper for RA binding */ export class MetricsTracker { constructor( private wae: AnalyticsEngineDataset, private mcpServerInfo: { name: string version: string } ) {} logEvent(event: MetricsEvent): void { try { event.serverInfo = this.mcpServerInfo let dataPoint = event.toDataPoint() this.wae.writeDataPoint(dataPoint) } catch (e) { console.error(`Failed to log metrics event, ${e}`) } } } /** * MetricsEvent * * Each event type is stored with a different indexId and has an associated class which * maps a more ergonomic event object to a ReadyAnalyticsEvent */ export abstract class MetricsEvent { public _serverInfo: { name: string; version: string } | undefined set serverInfo(serverInfo: { name: string; version: string }) { this._serverInfo = serverInfo } get serverInfo(): { name: string; version: string } { if (!this._serverInfo) { throw new Error('Server info not set') } return this._serverInfo } /** * Output a valid AnalyticsEngineDataPoint. Use `mapBlobs` and `mapDoubles` to write well defined * analytics engine datapoints. The first and second blob entries are reserved for the MCP server name and * MCP server version. */ abstract toDataPoint(): AnalyticsEngineDataPoint mapBlobs(blobs: Blobs): Array<string | null> { if (blobs.blob1 || blobs.blob2) { throw new MetricsError( 'Failed to map blobs, blob1 and blob2 are reserved for MCP server info' ) } // add placeholder blobs, filled in by the MetricsTracker later blobs.blob1 = this.serverInfo.name blobs.blob2 = this.serverInfo.version const blobsArray = new Array(Object.keys(blobs).length) for (const [key, value] of Object.entries(blobs)) { const match = key.match(/^blob(\d+)$/) if (match === null || match.length < 2) { // we should never hit this because of the typedefinitions above, // but this error is for safety throw new MetricsError('Failed to map blobs, invalid key') } const index = parseInt(match[1], 10) if (isNaN(index)) { // we should never hit this because of the typedefinitions above, // but this esrror is for safety throw new MetricsError('Failed to map blobs, invalid index') } if (index - 1 >= blobsArray.length) { throw new MetricsError('Failed to map blobs, missing blob') } blobsArray[index - 1] = value } return blobsArray } mapDoubles(doubles: Doubles): number[] { const doublesArray = new Array(Object.keys(doubles).length) for (const [key, value] of Object.entries(doubles)) { const match = key.match(/^double(\d+)$/) if (match === null || match.length < 2) { // we should never hit this because of the typedefinitions above, // but this error is for safety throw new MetricsError(': Failed to map doubles, invalid key') } const index = parseInt(match[1], 10) if (isNaN(index)) { // we should never hit this because of the typedefinitions above, // but this error is for safety throw new MetricsError('Failed to map doubles, invalid index') } if (index - 1 >= doublesArray.length) { throw new MetricsError('Failed to map doubles, missing blob') } doublesArray[index - 1] = value } return doublesArray } } export enum MetricsEventIndexIds { AUTH_USER = 'auth_user', SESSION_START = 'session_start', TOOL_CALL = 'tool_call', CONTAINER_MANAGER = 'container_manager', } /** * Utility functions to map named blob/double objects to an array * We do this so we don't have to annotate `blob1`, `blob2`, etc in comments. * * I prefer this to just writing it in an array because it'll be easier to reference * later when we are writing ready analytics queries. * * IMO named tuples and raw arrays aren't as ergonomic to work with, but they require less of this code below */ type Range1To20 = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 // blob1 and blob2 are reserved for server name and version type Blobs = { [key in `blob${Range1To20}`]?: string | null } type Doubles = { [key in `double${Range1To20}`]?: number } export class MetricsError extends Error { constructor(message: string) { super(message) this.name = 'MetricsError' } } ``` -------------------------------------------------------------------------------- /apps/dns-analytics/src/dns-analytics.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerAnalyticTools } from './tools/dex-analytics.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './dns-analytics.context' export { UserDetails } const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props export type Props = AuthProps export type State = { activeAccountId: string | null } export class DNSAnalyticsMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) // Register Cloudflare DNS Analytics tools registerAnalyticTools(this) registerZoneTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const AnalyticsScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'zone:read': 'See your zones', 'dns_settings:read': 'See your DNS settings', 'dns_analytics:read': 'See your DNS analytics', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(DNSAnalyticsMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': DNSAnalyticsMCP.serve('/mcp'), '/sse': DNSAnalyticsMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: AnalyticsScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/sandbox-container/container/sandbox.container.app.ts: -------------------------------------------------------------------------------- ```typescript import { exec } from 'node:child_process' import * as fs from 'node:fs/promises' import path from 'node:path' import { serve } from '@hono/node-server' import { zValidator } from '@hono/zod-validator' import { Hono } from 'hono' import { streamText } from 'hono/streaming' import mime from 'mime' import { ExecParams, FileWrite } from '../shared/schema.ts' import { DIRECTORY_CONTENT_TYPE, get_file_name_from_path, get_mime_type, list_files_in_directory, } from './fileUtils.ts' import type { FileList } from '../shared/schema.ts' process.chdir('workdir') const app = new Hono() app.get('/ping', (c) => c.text('pong!')) /** * GET /files/ls * * Gets all files in a directory */ app.get('/files/ls', async (c) => { const directoriesToRead = ['.'] const files: FileList = { resources: [] } while (directoriesToRead.length > 0) { const curr = directoriesToRead.pop() if (!curr) { throw new Error('Popped empty stack, error while listing directories') } const fullPath = path.join(process.cwd(), curr) const dir = await fs.readdir(fullPath, { withFileTypes: true }) for (const dirent of dir) { const relPath = path.relative(process.cwd(), `${fullPath}/${dirent.name}`) if (dirent.isDirectory()) { directoriesToRead.push(dirent.name) files.resources.push({ uri: `file:///${relPath}`, name: dirent.name, mimeType: 'inode/directory', }) } else { const mimeType = mime.getType(dirent.name) files.resources.push({ uri: `file:///${relPath}`, name: dirent.name, mimeType: mimeType ?? undefined, }) } } } return c.json(files) }) /** * GET /files/contents/{filepath} * * Get the contents of a file or directory */ app.get('/files/contents/*', async (c) => { const reqPath = await get_file_name_from_path(c.req.path) try { const mimeType = await get_mime_type(reqPath) const headers = mimeType ? { 'Content-Type': mimeType } : undefined const contents = await fs.readFile(path.join(process.cwd(), reqPath)) return c.newResponse(contents, 200, headers) } catch (e: any) { if (e.code) { if (e.code === 'EISDIR') { const files = await list_files_in_directory(reqPath) return c.newResponse(files.join('\n'), 200, { 'Content-Type': DIRECTORY_CONTENT_TYPE, }) } if (e.code === 'ENOENT') { return c.notFound() } } throw e } }) /** * POST /files/contents * * Create or update file contents */ app.post('/files/contents', zValidator('json', FileWrite), async (c) => { const file = c.req.valid('json') const reqPath = await get_file_name_from_path(file.path) try { await fs.writeFile(reqPath, file.text) return c.newResponse(null, 200) } catch (e) { return c.newResponse(`Error: ${e}`, 400) } }) /** * DELETE /files/contents/{filepath} * * Delete a file or directory */ app.delete('/files/contents/*', async (c) => { const reqPath = await get_file_name_from_path(c.req.path) try { await fs.rm(path.join(process.cwd(), reqPath), { recursive: true }) return c.newResponse('ok', 200) } catch (e: any) { if (e.code) { if (e.code === 'ENOENT') { return c.notFound() } } throw e } }) /** * POST /exec * * Execute a command in a shell */ app.post('/exec', zValidator('json', ExecParams), (c) => { const execParams = c.req.valid('json') const proc = exec(execParams.args) return streamText(c, async (stream) => { return new Promise((resolve, reject) => { if (proc.stdout) { // Stream data from stdout proc.stdout.on('data', async (data) => { await stream.write(data.toString()) }) } else { void stream.write('WARNING: no stdout stream for process') } if (execParams.streamStderr) { if (proc.stderr) { proc.stderr.on('data', async (data) => { await stream.write(data.toString()) }) } else { void stream.write('WARNING: no stderr stream for process') } } // Handle process exit proc.on('exit', async (code) => { await stream.write(`Process exited with code: ${code}`) if (code === 0) { await stream.close() resolve() } else { console.error(`Process exited with code ${code}`) reject(new Error(`Process failed with code ${code}`)) } }) proc.on('error', (err) => { console.error('Error with process: ', err) reject(err) }) }) }) }) serve({ fetch: app.fetch, port: 8080, }) ``` -------------------------------------------------------------------------------- /apps/graphql/src/graphql.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { initSentryWithUser } from '@repo/mcp-common/src/sentry' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools' import { MetricsTracker } from '@repo/mcp-observability' import { registerGraphQLTools } from './tools/graphql.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './graphql.context' export { UserDetails } const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class GraphQLMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined const sentry = props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, sentry, }) // Register account tools registerAccountTools(this) // Register zone tools registerZoneTools(this) // Register GraphQL tools registerGraphQLTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const GraphQLScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'zone:read': 'See zone data such as settings, analytics, and DNS records.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(GraphQLMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': GraphQLMCP.serve('/mcp'), '/sse': GraphQLMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: GraphQLScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/autorag/src/tools/autorag.tools.ts: -------------------------------------------------------------------------------- ```typescript import { V4PagePaginationArray } from 'cloudflare/src/pagination.js' import { z } from 'zod' import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' import { getProps } from '@repo/mcp-common/src/get-props' import { pageParam, perPageParam } from '../types' import type { AutoRAGMCP } from '../autorag.app' export function registerAutoRAGTools(agent: AutoRAGMCP) { agent.server.tool( 'list_rags', 'List AutoRAGs (vector stores)', { page: pageParam, per_page: perPageParam, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = (await client.getAPIList( `/accounts/${accountId}/autorag/rags`, // @ts-ignore V4PagePaginationArray, { query: { page: params.page, per_page: params.per_page } } )) as unknown as { result: Array<{ id: string; source: string; paused: boolean }> result_info: { total_count: number } } return { content: [ { type: 'text', text: JSON.stringify({ autorags: r.result.map((obj) => { return { id: obj.id, source: obj.source, paused: obj.paused, } }), total_count: r.result_info.total_count, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error listing rags: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'search', 'Search Documents using AutoRAG (vector store)', { rag_id: z.string().describe('ID of the AutoRAG to search'), query: z.string().describe('Query to search for. Can be a URL, a title, or a snippet.'), }, async (params) => { try { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = (await client.post( `/accounts/${accountId}/autorag/rags/${params.rag_id}/search`, { body: { query: params.query, max_num_results: 5, }, } )) as { result: { data: Array<{ filename: string; content: Array<{ text: string }> }> } } const chunks = r.result.data .map((item) => { const data = item.content .map((content) => { return content.text }) .join('\n\n') return `<file name="${item.filename}">${data}</file>` }) .join('\n\n') return { content: [ { type: 'text', text: chunks, }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error searching rag: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'ai_search', 'AI Search Documents using AutoRAG (vector store)', { rag_id: z.string().describe('ID of the AutoRAG to search'), query: z.string().describe('Query to search for. Can be a URL, a title, or a snippet.'), }, async (params) => { try { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = (await client.post( `/accounts/${accountId}/autorag/rags/${params.rag_id}/ai-search`, { body: { query: params.query, max_num_results: 10, // Limit can be bigger here, since llm is only getting the end response and not individual chunks }, } )) as { result: { response: string } } return { content: [ { type: 'text', text: r.result.response, }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error searching rag: ${error instanceof Error && error.message}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /apps/sandbox-container/server/userContainer.ts: -------------------------------------------------------------------------------- ```typescript import { DurableObject } from 'cloudflare:workers' import { OPEN_CONTAINER_PORT } from '../shared/consts' import { MAX_CONTAINERS, proxyFetch, startAndWaitForPort } from './containerHelpers' import { getContainerManager } from './containerManager' import { fileToBase64 } from './utils' import type { ExecParams, FileList, FileWrite } from '../shared/schema' import type { Env } from './sandbox.server.context' export class UserContainer extends DurableObject<Env> { constructor( public ctx: DurableObjectState, public env: Env ) { console.log('creating user container DO') super(ctx, env) } async destroyContainer(): Promise<void> { await this.ctx.container?.destroy() } async killContainer(): Promise<void> { console.log('Reaping container') const containerManager = getContainerManager(this.env) const active = await containerManager.listActive() if (this.ctx.id.toString() in active) { console.log('killing container') await this.destroyContainer() await containerManager.killContainer(this.ctx.id.toString()) } } async container_initialize(): Promise<string> { // kill container await this.killContainer() // try to cleanup cleanup old containers const containerManager = getContainerManager(this.env) // if more than half of our containers are being used, let's try reaping if ((await containerManager.listActive()).length >= MAX_CONTAINERS / 2) { await containerManager.tryKillOldContainers() if ((await containerManager.listActive()).length >= MAX_CONTAINERS) { throw new Error( `Unable to reap enough containers. There are ${MAX_CONTAINERS} active container sandboxes, please wait` ) } } // start container let startedContainer = false await this.ctx.blockConcurrencyWhile(async () => { startedContainer = await startAndWaitForPort( this.env.ENVIRONMENT, this.ctx.container, OPEN_CONTAINER_PORT ) }) if (!startedContainer) { throw new Error('Failed to start container') } // track and manage lifecycle await containerManager.trackContainer(this.ctx.id.toString()) return `Created new container` } async container_ping(): Promise<string> { const res = await proxyFetch( this.env.ENVIRONMENT, this.ctx.container, new Request(`http://host:${OPEN_CONTAINER_PORT}/ping`), OPEN_CONTAINER_PORT ) if (!res || !res.ok) { throw new Error(`Request to container failed: ${await res.text()}`) } return await res.text() } async container_exec(params: ExecParams): Promise<string> { const res = await proxyFetch( this.env.ENVIRONMENT, this.ctx.container, new Request(`http://host:${OPEN_CONTAINER_PORT}/exec`, { method: 'POST', body: JSON.stringify(params), headers: { 'content-type': 'application/json', }, }), OPEN_CONTAINER_PORT ) if (!res || !res.ok) { throw new Error(`Request to container failed: ${await res.text()}`) } const txt = await res.text() return txt } async container_ls(): Promise<FileList> { const res = await proxyFetch( this.env.ENVIRONMENT, this.ctx.container, new Request(`http://host:${OPEN_CONTAINER_PORT}/files/ls`), OPEN_CONTAINER_PORT ) if (!res || !res.ok) { throw new Error(`Request to container failed: ${await res.text()}`) } const json = (await res.json()) as FileList return json } async container_file_delete(filePath: string): Promise<boolean> { const res = await proxyFetch( this.env.ENVIRONMENT, this.ctx.container, new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`, { method: 'DELETE', }), OPEN_CONTAINER_PORT ) return res.ok } async container_file_read( filePath: string ): Promise< | { type: 'text'; textOutput: string; mimeType: string | undefined } | { type: 'base64'; base64Output: string; mimeType: string | undefined } > { const res = await proxyFetch( this.env.ENVIRONMENT, this.ctx.container, new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`), OPEN_CONTAINER_PORT ) if (!res || !res.ok) { throw new Error(`Request to container failed: ${await res.text()}`) } const mimeType = res.headers.get('Content-Type') ?? undefined const blob = await res.blob() if (mimeType && mimeType.startsWith('text')) { return { type: 'text', textOutput: await blob.text(), mimeType, } } else { return { type: 'base64', base64Output: await fileToBase64(blob), mimeType, } } } async container_file_write(file: FileWrite): Promise<string> { const res = await proxyFetch( this.env.ENVIRONMENT, this.ctx.container, new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents`, { method: 'POST', body: JSON.stringify(file), headers: { 'content-type': 'application/json', }, }), OPEN_CONTAINER_PORT ) if (!res || !res.ok) { throw new Error(`Request to container failed: ${await res.text()}`) } return `Wrote file: ${file.path}` } } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/cloudflare-auth.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { McpError } from './mcp-error' import type { AuthRequest } from '@cloudflare/workers-oauth-provider' // Constants const PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' const RECOMMENDED_CODE_VERIFIER_LENGTH = 96 function base64urlEncode(value: string): string { let base64 = btoa(value) base64 = base64.replace(/\+/g, '-') base64 = base64.replace(/\//g, '_') base64 = base64.replace(/=/g, '') return base64 } interface PKCECodes { codeChallenge: string codeVerifier: string } async function generatePKCECodes(): Promise<PKCECodes> { const output = new Uint32Array(RECOMMENDED_CODE_VERIFIER_LENGTH) crypto.getRandomValues(output) const codeVerifier = base64urlEncode( Array.from(output) .map((num: number) => PKCE_CHARSET[num % PKCE_CHARSET.length]) .join('') ) const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)) const hash = new Uint8Array(buffer) let binary = '' const hashLength = hash.byteLength for (let i = 0; i < hashLength; i++) { binary += String.fromCharCode(hash[i]) } const codeChallenge = base64urlEncode(binary) //btoa(binary); return { codeChallenge, codeVerifier } } function generateAuthUrl({ client_id, redirect_uri, state, code_challenge, scopes, }: { client_id: string redirect_uri: string code_challenge: string state: string scopes: Record<string, string> }) { const params = new URLSearchParams({ response_type: 'code', client_id, redirect_uri, state, code_challenge, code_challenge_method: 'S256', scope: Object.keys(scopes).join(' '), }) const upstream = new URL(`https://dash.cloudflare.com/oauth2/auth?${params.toString()}`) return upstream.href } /** * Constructs an authorization URL for Cloudflare. * * @param {Object} options * @param {string} options.client_id - The client ID of the application. * @param {string} options.redirect_uri - The redirect URI of the application. * @param {string} [options.state] - The state parameter. * * @returns {string} The authorization URL. */ export async function getAuthorizationURL({ client_id, redirect_uri, state, scopes, }: { client_id: string redirect_uri: string state: AuthRequest scopes: Record<string, string> }): Promise<{ authUrl: string; codeVerifier: string }> { const { codeChallenge, codeVerifier } = await generatePKCECodes() return { authUrl: generateAuthUrl({ client_id, redirect_uri, state: btoa(JSON.stringify({ ...state, codeVerifier })), code_challenge: codeChallenge, scopes, }), codeVerifier: codeVerifier, } } type AuthorizationToken = z.infer<typeof AuthorizationToken> const AuthorizationToken = z.object({ access_token: z.string(), expires_in: z.number(), refresh_token: z.string(), scope: z.string(), token_type: z.string(), }) /** * Fetches an authorization token from Cloudflare. * * @param {Object} options * @param {string} options.client_id - The client ID of the application. * @param {string} options.client_secret - The client secret of the application. * @param {string} options.code - The authorization code. * @param {string} options.redirect_uri - The redirect URI of the application. * * @returns {Promise<[string, null] | [null, Response]>} A promise that resolves to an array containing the access token or an error response. */ export async function getAuthToken({ client_id, client_secret, redirect_uri, code_verifier, code, }: { client_id: string client_secret: string redirect_uri: string code_verifier: string code: string }): Promise<AuthorizationToken> { if (!code) { throw new McpError('Missing code', 400) } const params = new URLSearchParams({ grant_type: 'authorization_code', client_id, redirect_uri, code, code_verifier, }).toString() const resp = await fetch('https://dash.cloudflare.com/oauth2/token', { method: 'POST', headers: { Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: params, }) if (!resp.ok) { console.log(await resp.text()) throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true }) } return AuthorizationToken.parse(await resp.json()) } export async function refreshAuthToken({ client_id, client_secret, refresh_token, }: { client_id: string client_secret: string refresh_token: string }): Promise<AuthorizationToken> { const params = new URLSearchParams({ grant_type: 'refresh_token', client_id, refresh_token, }) const resp = await fetch('https://dash.cloudflare.com/oauth2/token', { method: 'POST', body: params.toString(), headers: { Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`, 'Content-Type': 'application/x-www-form-urlencoded', }, }) if (!resp.ok) { console.log(await resp.text()) throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true }) } return AuthorizationToken.parse(await resp.json()) } ``` -------------------------------------------------------------------------------- /apps/workers-bindings/src/bindings.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-vectorize.prompts' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { registerD1Tools } from '@repo/mcp-common/src/tools/d1.tools' import { registerDocsTools } from '@repo/mcp-common/src/tools/docs-vectorize.tools' import { registerHyperdriveTools } from '@repo/mcp-common/src/tools/hyperdrive.tools' import { registerKVTools } from '@repo/mcp-common/src/tools/kv_namespace.tools' import { registerR2BucketTools } from '@repo/mcp-common/src/tools/r2_bucket.tools' import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools' import { MetricsTracker } from '@repo/mcp-observability' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './bindings.context' export { UserDetails } const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) export type WorkersBindingsMCPState = { activeAccountId: string | null } // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps export class WorkersBindingsMCP extends McpAgent<Env, WorkersBindingsMCPState, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } initialState: WorkersBindingsMCPState = { activeAccountId: null, } constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, }) registerAccountTools(this) registerKVTools(this) registerWorkersTools(this) registerR2BucketTools(this) registerD1Tools(this) registerHyperdriveTools(this) // Add docs tools registerDocsTools(this, this.env) registerPrompts(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const BindingsScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'workers:write': 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.', 'd1:write': 'Create, read, and write to D1 databases', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { console.log('is token mode') return await handleApiTokenMode(WorkersBindingsMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': WorkersBindingsMCP.serve('/mcp'), '/sse': WorkersBindingsMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: BindingsScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/worker.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { handleGetWorkersService, handleWorkerScriptDownload, handleWorkersList, } from '../api/workers.api' import { getCloudflareClient } from '../cloudflare-api' import { fmt } from '../format' import { getProps } from '../get-props' import type { CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types' /** * Registers the workers tools with the MCP server * @param server The MCP server instance * @param accountId Cloudflare account ID * @param apiToken Cloudflare API token */ // Define the scriptName parameter schema const workerNameParam = z.string().describe('The name of the worker script to retrieve') export function registerWorkersTools(agent: CloudflareMcpAgent) { // Tool to list all workers agent.server.tool( 'workers_list', fmt.trim(` List all Workers in your Cloudflare account. If you only need details of a single Worker, use workers_get_worker. `), {}, { title: 'List Workers', annotations: { readOnlyHint: true, destructiveHint: false, }, }, async () => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const results = await handleWorkersList({ client: getCloudflareClient(props.accessToken), accountId, }) // Extract worker details and sort by created_on date (newest first) const workers = results .map((worker) => ({ name: worker.id, // The API client doesn't know tag exists. The tag is needed in other places such as Workers Builds id: z.object({ tag: z.string() }).parse(worker), modified_on: worker.modified_on || null, created_on: worker.created_on || null, })) // order by created_on desc ( newest first ) .sort((a, b) => { if (!a.created_on) return 1 if (!b.created_on) return -1 return new Date(b.created_on).getTime() - new Date(a.created_on).getTime() }) return { content: [ { type: 'text', text: JSON.stringify({ workers, count: workers.length, }), }, ], } } catch (e) { agent.server.recordError(e) return { content: [ { type: 'text', text: `Error listing workers: ${e instanceof Error && e.message}`, }, ], } } } ) // Tool to get a specific worker's script details agent.server.tool( 'workers_get_worker', 'Get the details of the Cloudflare Worker.', { scriptName: workerNameParam, }, { title: 'Get Worker details', annotations: { readOnlyHint: true, destructiveHint: false, }, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const { scriptName } = params const res = await handleGetWorkersService({ apiToken: props.accessToken, scriptName, accountId, }) if (!res.result) { return { content: [ { type: 'text', text: 'Worker not found', }, ], } } return { content: [ { type: 'text', text: await fmt.asTSV([ { name: res.result.id, id: res.result.default_environment.script_tag, }, ]), }, ], } } catch (e) { agent.server.recordError(e) return { content: [ { type: 'text', text: `Error retrieving worker script: ${e instanceof Error && e.message}`, }, ], } } } ) // Tool to get a specific worker's script content agent.server.tool( 'workers_get_worker_code', 'Get the source code of a Cloudflare Worker. Note: This may be a bundled version of the worker.', { scriptName: workerNameParam }, { title: 'Get Worker code', annotations: { readOnlyHint: true, destructiveHint: false, }, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const { scriptName } = params const scriptContent = await handleWorkerScriptDownload({ client: getCloudflareClient(props.accessToken), scriptName, accountId, }) return { content: [ { type: 'text', text: scriptContent, }, ], } } catch (e) { agent.server.recordError(e) return { content: [ { type: 'text', text: `Error retrieving worker script: ${e instanceof Error && e.message}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /apps/workers-observability/src/workers-observability.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { getProps } from '@repo/mcp-common/src/get-props' import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-vectorize.prompts' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { initSentryWithUser } from '@repo/mcp-common/src/sentry' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { registerDocsTools } from '@repo/mcp-common/src/tools/docs-vectorize.tools' import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerObservabilityTools } from './tools/workers-observability.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './workers-observability.context' export { UserDetails } const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null } export class ObservabilityMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined const sentry = props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, sentry, options: { instructions: `# Cloudflare Workers Observability Tool * A cloudflare worker is a serverless function * Workers Observability is the tool to inspect the logs for your cloudflare Worker * Each log is a structured JSON payload with keys and values This server allows you to analyze your Cloudflare Workers logs and metrics. `, }, }) registerAccountTools(this) // Register Cloudflare Workers tools registerWorkersTools(this) // Register Cloudflare Workers logs tools registerObservabilityTools(this) // Add docs tools registerDocsTools(this, this.env) registerPrompts(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } } const ObservabilityScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'workers:read': 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.', 'workers_observability:read': 'See observability logs for your account', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(ObservabilityMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': ObservabilityMCP.serve('/mcp'), '/sse': ObservabilityMCP.serveSSE('/sse'), }, // @ts-ignore defaultHandler: createAuthHandlers({ scopes: ObservabilityScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ``` -------------------------------------------------------------------------------- /apps/sandbox-container/server/containerMcp.ts: -------------------------------------------------------------------------------- ```typescript import { McpAgent } from 'agents/mcp' import { getProps } from '@repo/mcp-common/src/get-props' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { ExecParams, FilePathParam, FileWrite } from '../shared/schema' import { BASE_INSTRUCTIONS } from './prompts' import { stripProtocolFromFilePath } from './utils' import type { Props, UserContainer } from './sandbox.server.app' import type { Env } from './sandbox.server.context' export class ContainerMcpAgent extends McpAgent<Env, never, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } get userContainer(): DurableObjectStub<UserContainer> { const props = getProps(this) // TODO: Support account scoped tokens? if (props.type === 'account_token') { throw new Error('Container server does not currently support account scoped tokens') } const userContainer = this.env.USER_CONTAINER.idFromName(props.user.id) return this.env.USER_CONTAINER.get(userContainer) } constructor( public ctx: DurableObjectState, public env: Env ) { console.log('creating container DO') super(ctx, env) } async init() { const props = getProps(this) // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const userId = props.type === 'user_token' ? props.user.id : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, options: { instructions: BASE_INSTRUCTIONS }, }) this.server.tool( 'container_initialize', `Start or restart the container. Use this tool to initialize a container before running any python or node.js code that the user requests ro run.`, async () => { const props = getProps(this) if (props.type === 'account_token') { return { // TODO: Support account scoped tokens? // we'll need to add support for an account blocklist in that case content: [ { type: 'text', text: 'Container server does not currently support account scoped tokens.', }, ], } } const userInBlocklist = await this.env.USER_BLOCKLIST.get(props.user.id) if (userInBlocklist) { return { content: [{ type: 'text', text: 'Blocked from intializing container.' }], } } return { content: [{ type: 'text', text: await this.userContainer.container_initialize() }], } } ) this.server.tool( 'container_ping', `Ping the container for liveliness. Use this tool to check if the container is running.`, async () => { return { content: [{ type: 'text', text: await this.userContainer.container_ping() }], } } ) this.server.tool( 'container_exec', `Run a command in a container and return the results from stdout. If necessary, set a timeout. To debug, stream back standard error. If you're using python, ALWAYS use python3 alongside pip3`, { args: ExecParams }, async ({ args }) => { return { content: [{ type: 'text', text: await this.userContainer.container_exec(args) }], } } ) this.server.tool( 'container_file_delete', 'Delete file in the working directory', { args: FilePathParam }, async ({ args }) => { const path = await stripProtocolFromFilePath(args.path) const deleted = await this.userContainer.container_file_delete(path) return { content: [{ type: 'text', text: `File deleted: ${deleted}.` }], } } ) this.server.tool( 'container_file_write', 'Create a new file with the provided contents in the working direcotry, overwriting the file if it already exists', { args: FileWrite }, async ({ args }) => { args.path = await stripProtocolFromFilePath(args.path) return { content: [{ type: 'text', text: await this.userContainer.container_file_write(args) }], } } ) this.server.tool( 'container_files_list', 'List working directory file tree. This just reads the contents of the current working directory', async () => { // Begin workaround using container read rather than ls: const readFile = await this.userContainer.container_file_read('.') return { content: [ { type: 'resource', resource: { text: readFile.type === 'text' ? readFile.textOutput : readFile.base64Output, uri: `file://`, mimeType: readFile.mimeType, }, }, ], } } ) this.server.tool( 'container_file_read', 'Read a specific file or directory. Use this tool if you would like to read files or display them to the user. This allow you to get a displayable image for the user if there is an image file.', { args: FilePathParam }, async ({ args }) => { const path = await stripProtocolFromFilePath(args.path) const readFile = await this.userContainer.container_file_read(path) return { content: [ { type: 'resource', resource: { text: readFile.type === 'text' ? readFile.textOutput : readFile.base64Output, uri: `file://${path}`, mimeType: readFile.mimeType, }, }, ], } } ) } } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/api/cf1-integration.api.ts: -------------------------------------------------------------------------------- ```typescript import { fetchCloudflareApi } from '../cloudflare-api' import { AssetCategoriesResponse, AssetDetail, AssetsResponse, IntegrationResponse, IntegrationsResponse, } from '../types/cf1-integrations.types' import { V4Schema } from '../v4-api' import type { z } from 'zod' import type { zReturnedAssetCategoriesResult, zReturnedAssetsResult, zReturnedIntegrationResult, zReturnedIntegrationsResult, } from '../types/cf1-integrations.types' interface BaseParams { accountId: string apiToken: string } interface PaginationParams { page?: number pageSize?: number } type IntegrationParams = BaseParams & { integrationIdParam: string } type AssetCategoryParams = BaseParams & { type?: string; vendor?: string } type AssetSearchParams = BaseParams & { searchTerm: string } & PaginationParams type AssetByIdParams = BaseParams & { assetId: string } type AssetByCategoryParams = BaseParams & { categoryId: string } & PaginationParams type AssetByIntegrationParams = BaseParams & { integrationId: string } & PaginationParams const buildParams = (baseParams: Record<string, string>, pagination?: PaginationParams) => { const params = new URLSearchParams(baseParams) if (pagination?.page) params.append('page', String(pagination.page)) if (pagination?.pageSize) params.append('page_size', String(pagination.pageSize)) return params } const buildIntegrationEndpoint = (integrationId: string) => `/casb/integrations/${integrationId}` const buildAssetEndpoint = (assetId?: string) => assetId ? `/casb/assets/${assetId}` : '/casb/assets' const buildAssetCategoryEndpoint = () => '/casb/asset_categories' const makeApiCall = async <T>({ endpoint, accountId, apiToken, responseSchema, params, }: { endpoint: string accountId: string apiToken: string responseSchema: z.ZodType<any> params?: URLSearchParams }): Promise<T> => { try { const fullEndpoint = params ? `${endpoint}?${params.toString()}` : endpoint const data = await fetchCloudflareApi({ endpoint: fullEndpoint, accountId, apiToken, responseSchema, options: { method: 'GET', headers: { 'Content-Type': 'application/json' }, }, }) return data.result as T } catch (error) { console.error(`API call failed for ${endpoint}:`, error) throw error } } // Resource-specific API call handlers const makeIntegrationCall = <T>(params: IntegrationParams, responseSchema: z.ZodType<any>) => makeApiCall<T>({ endpoint: buildIntegrationEndpoint(params.integrationIdParam), accountId: params.accountId, apiToken: params.apiToken, responseSchema, }) const makeAssetCall = <T>( params: BaseParams & PaginationParams, responseSchema: z.ZodType<any>, assetId?: string, additionalParams?: Record<string, string> ) => makeApiCall<T>({ endpoint: buildAssetEndpoint(assetId), accountId: params.accountId, apiToken: params.apiToken, responseSchema, params: buildParams(additionalParams || {}, params), }) const makeAssetCategoryCall = <T>(params: AssetCategoryParams, responseSchema: z.ZodType<any>) => makeApiCall<T>({ endpoint: buildAssetCategoryEndpoint(), accountId: params.accountId, apiToken: params.apiToken, responseSchema, params: buildParams({ ...(params.vendor && { vendor: params.vendor }), ...(params.type && { type: params.type }), }), }) // Integration handlers export async function handleIntegrationById( params: IntegrationParams ): Promise<{ integration: zReturnedIntegrationResult | null }> { const integration = await makeIntegrationCall<zReturnedIntegrationResult>( params, V4Schema(IntegrationResponse) ) return { integration } } export async function handleIntegrations( params: BaseParams ): Promise<{ integrations: zReturnedIntegrationsResult | null }> { const integrations = await makeApiCall<zReturnedIntegrationsResult>({ endpoint: '/casb/integrations', accountId: params.accountId, apiToken: params.apiToken, responseSchema: V4Schema(IntegrationsResponse), }) return { integrations } } // Asset category handlers export async function handleAssetCategories( params: AssetCategoryParams ): Promise<{ categories: zReturnedAssetCategoriesResult | null }> { const categories = await makeAssetCategoryCall<zReturnedAssetCategoriesResult>( params, V4Schema(AssetCategoriesResponse) ) return { categories } } // Asset handlers export async function handleAssets(params: BaseParams & PaginationParams) { const assets = await makeAssetCall<zReturnedAssetsResult>(params, V4Schema(AssetsResponse)) return { assets } } export async function handleAssetsByIntegrationId(params: AssetByIntegrationParams) { const assets = await makeAssetCall<zReturnedAssetsResult>( params, V4Schema(AssetsResponse), undefined, { integration_id: params.integrationId } ) return { assets } } export async function handleAssetById(params: AssetByIdParams) { const asset = await makeAssetCall<zReturnedAssetsResult>( params, V4Schema(AssetDetail), params.assetId ) return { asset } } export async function handleAssetsByAssetCategoryId(params: AssetByCategoryParams) { const assets = await makeAssetCall<zReturnedAssetsResult>( params, V4Schema(AssetsResponse), undefined, { category_id: params.categoryId } ) return { assets } } export async function handleAssetsSearch(params: AssetSearchParams) { const assets = await makeAssetCall<zReturnedAssetsResult>( params, V4Schema(AssetsResponse), undefined, { search: params.searchTerm } ) return { assets } } ``` -------------------------------------------------------------------------------- /apps/ai-gateway/src/tools/ai-gateway.tools.ts: -------------------------------------------------------------------------------- ```typescript import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' import { getProps } from '@repo/mcp-common/src/get-props' import { GatewayIdParam, ListLogsParams, LogIdParam, pageParam, perPageParam } from '../types' import type { LogListParams } from 'cloudflare/resources/ai-gateway' import type { AIGatewayMCP } from '../ai-gateway.app' export function registerAIGatewayTools(agent: AIGatewayMCP) { agent.server.tool( 'list_gateways', 'List Gateways', { page: pageParam, per_page: perPageParam, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = await client.aiGateway.list({ account_id: accountId, page: params.page, per_page: params.per_page, }) return { content: [ { type: 'text', text: JSON.stringify({ result: r.result, result_info: r.result_info, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error listing gateways: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool('list_logs', 'List Logs', ListLogsParams, async (params) => { try { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } const { gateway_id, ...filters } = params const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = await client.aiGateway.logs.list(gateway_id, { ...filters, account_id: accountId, } as LogListParams) return { content: [ { type: 'text', text: JSON.stringify({ result: r.result, result_info: r.result_info, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error listing logs: ${error instanceof Error && error.message}`, }, ], } } }) agent.server.tool( 'get_log_details', 'Get a single Log details', { gateway_id: GatewayIdParam, log_id: LogIdParam, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = await client.aiGateway.logs.get(params.gateway_id, params.log_id, { account_id: accountId, }) return { content: [ { type: 'text', text: JSON.stringify({ result: r, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting log: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'get_log_request_body', 'Get Log Request Body', { gateway_id: GatewayIdParam, log_id: LogIdParam, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = await client.aiGateway.logs.request(params.gateway_id, params.log_id, { account_id: accountId, }) return { content: [ { type: 'text', text: JSON.stringify({ result: r, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting log request body: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'get_log_response_body', 'Get Log Response Body', { gateway_id: GatewayIdParam, log_id: LogIdParam, }, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const r = await client.aiGateway.logs.response(params.gateway_id, params.log_id, { account_id: accountId, }) return { content: [ { type: 'text', text: JSON.stringify({ result: r, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting log response body: ${error instanceof Error && error.message}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/d1.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { getCloudflareClient } from '../cloudflare-api' import { MISSING_ACCOUNT_ID_RESPONSE } from '../constants' import { getProps } from '../get-props' import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types' import { D1DatabaseNameParam, D1DatabasePrimaryLocationHintParam, D1DatabaseQueryParamsParam, D1DatabaseQuerySqlParam, } from '../types/d1.types' import { PaginationPageParam, PaginationPerPageParam } from '../types/shared.types' export function registerD1Tools(agent: CloudflareMcpAgent) { agent.server.tool( 'd1_databases_list', 'List all of the D1 databases in your Cloudflare account', { name: D1DatabaseNameParam.nullable().optional(), page: PaginationPageParam, per_page: PaginationPerPageParam, }, { title: 'List D1 databases', annotations: { readOnlyHint: true, }, }, async ({ name, page, per_page }) => { const account_id = await agent.getActiveAccountId() if (!account_id) { return MISSING_ACCOUNT_ID_RESPONSE } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const listResponse = await client.d1.database.list({ account_id, name: name ?? undefined, page: page ?? undefined, per_page: per_page ?? undefined, }) return { content: [ { type: 'text', text: JSON.stringify({ result: listResponse.result, result_info: listResponse.result_info, }), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error listing D1 databases: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'd1_database_create', 'Create a new D1 database in your Cloudflare account', { name: D1DatabaseNameParam, primary_location_hint: D1DatabasePrimaryLocationHintParam.nullable().optional(), }, { title: 'Create D1 database', annotations: { readOnlyHint: false, destructiveHint: false, }, }, async ({ name, primary_location_hint }) => { const account_id = await agent.getActiveAccountId() if (!account_id) { return MISSING_ACCOUNT_ID_RESPONSE } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const d1Database = await client.d1.database.create({ account_id, name, primary_location_hint: primary_location_hint ?? undefined, }) return { content: [ { type: 'text', text: JSON.stringify(d1Database), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error creating D1 database: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'd1_database_delete', 'Delete a d1 database in your Cloudflare account', { database_id: z.string() }, { title: 'Delete D1 database', annotations: { readOnlyHint: false, destructiveHint: true, }, }, async ({ database_id }) => { const account_id = await agent.getActiveAccountId() if (!account_id) { return MISSING_ACCOUNT_ID_RESPONSE } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const deleteResponse = await client.d1.database.delete(database_id, { account_id, }) return { content: [ { type: 'text', text: JSON.stringify(deleteResponse), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error deleting D1 database: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'd1_database_get', 'Get a D1 database in your Cloudflare account', { database_id: z.string() }, { title: 'Get D1 database', annotations: { readOnlyHint: true, }, }, async ({ database_id }) => { const account_id = await agent.getActiveAccountId() if (!account_id) { return MISSING_ACCOUNT_ID_RESPONSE } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const d1Database = await client.d1.database.get(database_id, { account_id, }) return { content: [ { type: 'text', text: JSON.stringify(d1Database), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error getting D1 database: ${error instanceof Error && error.message}`, }, ], } } } ) agent.server.tool( 'd1_database_query', 'Query a D1 database in your Cloudflare account', { database_id: z.string(), sql: D1DatabaseQuerySqlParam, params: D1DatabaseQueryParamsParam.nullable(), }, { title: 'Query D1 database', annotations: { readOnlyHint: false, destructiveHint: false, }, }, async ({ database_id, sql, params }) => { const account_id = await agent.getActiveAccountId() if (!account_id) { return MISSING_ACCOUNT_ID_RESPONSE } try { const props = getProps(agent) const client = getCloudflareClient(props.accessToken) const queryResult = await client.d1.database.query(database_id, { account_id, sql, params: params ?? undefined, }) return { content: [ { type: 'text', text: JSON.stringify(queryResult.result), }, ], } } catch (error) { return { content: [ { type: 'text', text: `Error querying D1 database: ${error instanceof Error && error.message}`, }, ], } } } ) } ``` -------------------------------------------------------------------------------- /apps/cloudflare-one-casb/src/tools/integrations.tools.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod' import { withAccountCheck } from '@repo/mcp-common/src/api/account.api' import { handleAssetById, handleAssetCategories, handleAssets, handleAssetsByAssetCategoryId, handleAssetsByIntegrationId, handleAssetsSearch, handleIntegrationById, handleIntegrations, } from '@repo/mcp-common/src/api/cf1-integration.api' import { assetCategoryTypeParam, assetCategoryVendorParam, } from '@repo/mcp-common/src/types/cf1-integrations.types' import type { ToolDefinition } from '@repo/mcp-common/src/types/tools.types' import type { CASBMCP } from '../cf1-casb.app' const PAGE_SIZE = 3 const integrationIdParam = z.string().describe('The UUID of the integration to analyze') const assetSearchTerm = z.string().describe('The search keyword for assets') const assetIdParam = z.string().describe('The UUID of the asset to analyze') const assetCategoryIdParam = z.string().describe('The UUID of the asset category to analyze') const toolDefinitions: Array<ToolDefinition<any>> = [ { name: 'integration_by_id', description: 'Analyze Cloudflare One Integration by ID', params: { integrationIdParam }, handler: async ({ integrationIdParam, accountId, apiToken, }: { integrationIdParam: string accountId: string apiToken: string }) => { const { integration } = await handleIntegrationById({ integrationIdParam, accountId, apiToken, }) return { integration } }, }, { name: 'integrations_list', description: 'List all Cloudflare One Integrations in a given account', params: {}, handler: async ({ accountId, apiToken }: { accountId: string; apiToken: string }) => { const { integrations } = await handleIntegrations({ accountId, apiToken }) return { integrations } }, }, { name: 'assets_search', description: 'Search Assets by keyword', params: { assetSearchTerm }, handler: async ({ assetSearchTerm, accountId, apiToken, }: { assetSearchTerm: string accountId: string apiToken: string }) => { const { assets } = await handleAssetsSearch({ accountId, apiToken, searchTerm: assetSearchTerm, pageSize: PAGE_SIZE, }) return { assets } }, }, { name: 'asset_by_id', description: 'Search Assets by ID', params: { assetIdParam }, handler: async ({ assetIdParam, accountId, apiToken, }: { assetIdParam: string accountId: string apiToken: string }) => { const { asset } = await handleAssetById({ accountId, apiToken, assetId: assetIdParam, }) return { asset } }, }, { name: 'assets_by_integration_id', description: 'Search Assets by Integration ID', params: { integrationIdParam }, handler: async ({ integrationIdParam, accountId, apiToken, }: { integrationIdParam: string accountId: string apiToken: string }) => { const { assets } = await handleAssetsByIntegrationId({ accountId, apiToken, integrationId: integrationIdParam, pageSize: PAGE_SIZE, }) return { assets } }, }, { name: 'assets_by_category_id', description: 'Search Assets by Asset Category ID', params: { assetCategoryIdParam }, handler: async ({ assetCategoryIdParam, accountId, apiToken, }: { assetCategoryIdParam: string accountId: string apiToken: string }) => { const { assets } = await handleAssetsByAssetCategoryId({ accountId, apiToken, categoryId: assetCategoryIdParam, pageSize: PAGE_SIZE, }) return { assets } }, }, { name: 'assets_list', description: 'Paginated list of Assets', params: {}, handler: async ({ accountId, apiToken }: { accountId: string; apiToken: string }) => { const { assets } = await handleAssets({ accountId, apiToken, pageSize: PAGE_SIZE, }) return { assets } }, }, { name: 'asset_categories_list', description: 'List Asset Categories', params: {}, handler: async ({ accountId, apiToken }: { accountId: string; apiToken: string }) => { const { categories } = await handleAssetCategories({ accountId, apiToken, }) return { categories } }, }, { name: 'asset_categories_by_vendor', description: 'List asset categories by vendor', params: { assetCategoryVendorParam }, handler: async ({ assetCategoryVendorParam, accountId, apiToken, }: { assetCategoryVendorParam: string accountId: string apiToken: string }) => { const { categories } = await handleAssetCategories({ accountId, apiToken, vendor: assetCategoryVendorParam, }) return { categories } }, }, { name: 'asset_categories_by_type', description: 'Search Asset Categories by type', params: { assetCategoryTypeParam }, handler: async ({ assetCategoryTypeParam, accountId, apiToken, }: { assetCategoryTypeParam?: string accountId: string apiToken: string }) => { const { categories } = await handleAssetCategories({ accountId, apiToken, type: assetCategoryTypeParam, }) return { categories } }, }, { name: 'asset_categories_by_vendor_and_type', description: 'Search Asset Categories by vendor and type', params: { assetCategoryTypeParam, assetCategoryVendorParam }, handler: async ({ assetCategoryTypeParam, assetCategoryVendorParam, accountId, apiToken, }: { assetCategoryTypeParam?: string assetCategoryVendorParam: string accountId: string apiToken: string }) => { const { categories } = await handleAssetCategories({ accountId, apiToken, type: assetCategoryTypeParam, vendor: assetCategoryVendorParam, }) return { categories } }, }, ] /** * Registers the logs analysis tool with the MCP server * @param agent The MCP server instance */ export function registerIntegrationsTools(agent: CASBMCP) { toolDefinitions.forEach(({ name, description, params, handler }) => { agent.server.tool(name, description, params, withAccountCheck(agent, handler)) }) } ``` -------------------------------------------------------------------------------- /implementation-guides/type-validators.md: -------------------------------------------------------------------------------- ```markdown # MCP Tool Type Validator Implementation Guide This guide outlines the best practices for creating Zod validators for the parameters of MCP (Model Context Protocol) tools, particularly those interacting with Cloudflare resources via the Cloudflare Typescript SDK. ## Purpose Zod validators serve several critical functions: 1. **Runtime Validation:** They ensure that the arguments provided to tools (often by an LLM) match the expected format, constraints, and types before the tool logic executes or interacts with external APIs (like the Cloudflare SDK). 2. **Type Safety:** They provide strong typing for tool parameters within the TypeScript codebase. 3. **Schema Definition:** They act as a clear, machine-readable definition of a tool's expected inputs, aiding both developers and LLMs in understanding how to use the tool correctly. 4. **SDK Alignment:** They help maintain alignment with underlying SDKs, catching potential breaking changes. ## Core Principles ### 1. Link to SDK Types with `z.ZodType` When a tool parameter corresponds directly to a parameter in the Cloudflare Node SDK (`cloudflare/resources/...`), **always** link your Zod schema to the SDK type using `z.ZodType<SDKType>`. This creates a compile-time dependency. **Why?** - **Detect SDK Changes:** If the underlying SDK type changes (e.g., type alias renamed, property added/removed/renamed, type changed from `string` to `string | null`), your Zod schema definition will likely cause a TypeScript error during compilation. This immediately flags the need to update the validator and potentially the tool logic, preventing runtime errors caused by SDK misalignment. - **Accuracy:** Ensures your validator accurately reflects the type expected by the SDK function you intend to call. **Example (`hyperdrive.types.ts`):** ```typescript import { z } from 'zod' import type { ConfigCreateParams } from 'cloudflare/resources/hyperdrive/configs.mjs' /** Zod schema for a Hyperdrive config name. */ export const HyperdriveConfigNameSchema: z.ZodType<ConfigCreateParams['name']> = z .string() .min(1) .max(64) .regex(/^[a-zA-Z0-9_-]+$/) .describe('The name of the Hyperdrive configuration (alphanumeric, underscore, hyphen)') /** Zod schema for the origin database host (IPv4). */ export const HyperdriveOriginHostSchema: z.ZodType<ConfigCreateParams.PublicDatabase['host']> = z .string() .ip({ version: 'v4' }) .describe('The database host IPv4 address') ``` ### 2. Define Individual Validators Per Field (Avoid Object Schemas for Tool Parameters) Define a separate, named Zod schema for **each individual field** that a tool might accept as a parameter. Do **not** group multiple parameters into a single Zod object schema (`z.object({...})`) that the tool accepts as a single `params` argument. **Why?** - **LLM Clarity:** LLMs generally understand and handle distinct, named parameters better than complex nested objects. Providing individual schemas makes the tool's signature clearer for the LLM to interpret and use correctly. - **Reusability:** Individual field schemas (like `HyperdriveConfigIdSchema`, `HyperdriveConfigNameSchema`) can be reused across different tools (e.g., `hyperdrive_create`, `hyperdrive_update`, `hyperdrive_get`). - **Modularity:** Easier to manage, update, and test individual validation rules. **Example (`hyperdrive.types.ts` Structure):** ```typescript // --- Base Field Schemas --- export const HyperdriveConfigIdSchema = z.string().describe(...); export const HyperdriveConfigNameSchema: z.ZodType<...> = z.string()...describe(...); export const HyperdriveOriginHostSchema: z.ZodType<...> = z.string()...describe(...); export const HyperdriveOriginPortSchema: z.ZodType<...> = z.number()...describe(...); // ... other individual fields ``` **Conceptual Tool Definition (Illustrative):** Instead of: ```typescript // DON'T DO THIS for tool params const CreateParamsSchema = z.object({ name: HyperdriveConfigNameSchema, host: HyperdriveOriginHostSchema, port: HyperdriveOriginPortSchema, // ... other fields }) // Tool definition would accept one arg: { params: CreateParamsSchema } ``` Do: ```typescript // DO THIS: Tool definition accepts multiple named args // tool('hyperdrive_create', { // name: HyperdriveConfigNameSchema, // host: HyperdriveOriginHostSchema, // port: HyperdriveOriginPortSchema, // // ... other named parameters with their individual schemas // }, ...) ``` ### 3. Use `.describe()` Extensively Add a clear, concise `.describe('...')` call to **every** Zod schema you define. **Why?** - **LLM Context:** The description is often extracted and provided to the LLM as part of the tool's definition, helping it understand the purpose and constraints of each parameter. - **Developer Documentation:** Serves as inline documentation for developers working with the code. **Example (`hyperdrive.types.ts`):** ```typescript /** Zod schema for the list page number. */ export const HyperdriveListParamPageSchema = z .number() .int() .positive() .optional() .describe('Page number of results') // <-- Good description! ``` ## Naming Conventions Use a consistent naming convention for your validator schemas. A recommended pattern is: `ServiceNameFieldNameSchema` - `ServiceName`: The Cloudflare service (e.g., `Hyperdrive`, `KV`, `D1`, `R2`). - `FieldName`: The specific field being validated (e.g., `ConfigId`, `ConfigName`, `OriginHost`, `ListParamPage`). - `Schema`: Suffix indicating it's a Zod schema. **Examples:** - `HyperdriveConfigIdSchema` - `KVKeySchema` - `D1DatabaseIdSchema` - `R2BucketNameSchema` ## Location Place validators related to a specific service or concept in dedicated files within the `packages/mcp-common/src/types/` directory (e.g., `hyperdrive.types.ts`, `kv.ts`). ## Summary By following these principles – linking to SDK types, using granular named validators, providing clear descriptions, and maintaining consistent naming – you create robust, maintainable, and LLM-friendly type validation for MCP tools. ``` -------------------------------------------------------------------------------- /apps/workers-builds/src/workers-builds.app.ts: -------------------------------------------------------------------------------- ```typescript import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' import { getEnv } from '@repo/mcp-common/src/env' import { fmt } from '@repo/mcp-common/src/format' import { getProps } from '@repo/mcp-common/src/get-props' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { initSentryWithUser } from '@repo/mcp-common/src/sentry' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerBuildsTools } from './tools/workers-builds.tools' import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './workers-builds.context' export { UserDetails } const env = getEnv<Env>() const metrics = new MetricsTracker(env.MCP_METRICS, { name: env.MCP_SERVER_NAME, version: env.MCP_SERVER_VERSION, }) // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props type Props = AuthProps type State = { activeAccountId: string | null activeBuildUUID: string | null activeWorkerId: string | null } export class BuildsMCP extends McpAgent<Env, State, Props> { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server } get server(): CloudflareMCPServer { if (!this._server) { throw new Error('Tried to access server before it was initialized') } return this._server } async init() { // TODO: Probably we'll want to track account tokens usage through an account identifier at some point const props = getProps(this) const userId = props.type === 'user_token' ? props.user.id : undefined const sentry = props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined this.server = new CloudflareMCPServer({ userId, wae: this.env.MCP_METRICS, serverInfo: { name: this.env.MCP_SERVER_NAME, version: this.env.MCP_SERVER_VERSION, }, sentry, options: { instructions: fmt.trim(` # Cloudflare Workers Builds Tool * A Cloudflare Worker is a serverless function * Workers Builds is a CI/CD system for building and deploying your Worker whenever you push code to GitHub/GitLab. This server allows you to view and debug Cloudflare Workers Builds for your Workers (NOT Cloudflare Pages). To get started, you can list your accounts (accounts_list) and then set an active account (set_active_account). Once you have an active account, you can list your Workers (workers_list) and set an active Worker (workers_builds_set_active_worker). You can then list the builds for your Worker (workers_builds_list_builds) and set an active build (workers_builds_set_active_build). Once you have an active build, you can view the logs (workers_builds_get_build_logs). `), }, }) registerAccountTools(this) // Register Cloudflare Workers tools registerWorkersTools(this) // Register Cloudflare Workers logs tools registerBuildsTools(this) } async getActiveAccountId() { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return props.account.id } // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it // we do this so we can persist activeAccountId across sessions const userDetails = getUserDetails(env, props.user.id) return await userDetails.getActiveAccountId() } catch (e) { this.server.recordError(e) return null } } async setActiveAccountId(accountId: string) { try { const props = getProps(this) // account tokens are scoped to one account if (props.type === 'account_token') { return } const userDetails = getUserDetails(env, props.user.id) await userDetails.setActiveAccountId(accountId) } catch (e) { this.server.recordError(e) } } async getActiveBuildUUID(): Promise<string | null> { try { return this.state.activeBuildUUID } catch (e) { this.server.recordError(e) return null } } async setActiveBuildUUID(buildUUID: string | null): Promise<void> { try { this.setState({ ...this.state, activeBuildUUID: buildUUID, }) } catch (e) { this.server.recordError(e) } } async getActiveWorkerId(): Promise<string | null> { try { return this.state.activeWorkerId } catch (e) { this.server.recordError(e) return null } } async setActiveWorkerId(workerId: string | null): Promise<void> { try { this.setState({ ...this.state, activeWorkerId: workerId, }) } catch (e) { this.server.recordError(e) } } } const BuildsScopes = { ...RequiredScopes, 'account:read': 'See your account info such as account details, analytics, and memberships.', 'workers:read': 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.', 'workers_builds:read': 'See and change Cloudflare Workers Builds data such as builds, build configuration, and logs.', } as const export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (await isApiTokenRequest(req, env)) { return await handleApiTokenMode(BuildsMCP, req, env, ctx) } return new OAuthProvider({ apiHandlers: { '/mcp': BuildsMCP.serve('/mcp'), '/sse': BuildsMCP.serveSSE('/sse'), }, // @ts-expect-error defaultHandler: createAuthHandlers({ scopes: BuildsScopes, metrics }), authorizeEndpoint: '/oauth/authorize', tokenEndpoint: '/token', tokenExchangeCallback: (options) => handleTokenExchangeCallback( options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET ), // Cloudflare access token TTL accessTokenTTL: 3600, clientRegistrationEndpoint: '/register', }).fetch(req, env, ctx) }, } ```