This is page 4 of 29. Use http://codebase.md/cloudflare/mcp-server-cloudflare?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .dockerignore
├── .editorconfig
├── .eslintrc.cjs
├── .github
│ ├── actions
│ │ └── setup
│ │ └── action.yml
│ ├── ISSUE_TEMPLATE
│ │ └── bug_report.md
│ └── workflows
│ ├── branches.yml
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.cjs
├── .syncpackrc.cjs
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── apps
│ ├── ai-gateway
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── ai-gateway.app.ts
│ │ │ ├── ai-gateway.context.ts
│ │ │ ├── tools
│ │ │ │ └── ai-gateway.tools.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── auditlogs
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── auditlogs.app.ts
│ │ │ ├── auditlogs.context.ts
│ │ │ └── tools
│ │ │ └── auditlogs.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── autorag
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── autorag.app.ts
│ │ │ ├── autorag.context.ts
│ │ │ ├── tools
│ │ │ │ └── autorag.tools.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── browser-rendering
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── browser.app.ts
│ │ │ ├── browser.context.ts
│ │ │ └── tools
│ │ │ └── browser.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── cloudflare-one-casb
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── cf1-casb.app.ts
│ │ │ ├── cf1-casb.context.ts
│ │ │ └── tools
│ │ │ └── integrations.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── demo-day
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── frontend
│ │ │ ├── index.html
│ │ │ ├── public
│ │ │ │ ├── anthropic.svg
│ │ │ │ ├── asana.svg
│ │ │ │ ├── atlassian.svg
│ │ │ │ ├── canva.svg
│ │ │ │ ├── cloudflare_logo.svg
│ │ │ │ ├── cloudflare.svg
│ │ │ │ ├── dina.jpg
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── favicon.png
│ │ │ │ ├── intercom.svg
│ │ │ │ ├── linear.svg
│ │ │ │ ├── matt.jpg
│ │ │ │ ├── mcp_demo_day.svg
│ │ │ │ ├── mcpog.png
│ │ │ │ ├── more.svg
│ │ │ │ ├── paypal.svg
│ │ │ │ ├── pete.jpeg
│ │ │ │ ├── sentry.svg
│ │ │ │ ├── special_guest.png
│ │ │ │ ├── square.svg
│ │ │ │ ├── stripe.svg
│ │ │ │ ├── sunil.jpg
│ │ │ │ └── webflow.svg
│ │ │ ├── script.js
│ │ │ └── styles.css
│ │ ├── package.json
│ │ ├── src
│ │ │ └── demo-day.app.ts
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.json
│ ├── dex-analysis
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── dex-analysis.app.ts
│ │ │ ├── dex-analysis.context.ts
│ │ │ ├── tools
│ │ │ │ └── dex-analysis.tools.ts
│ │ │ └── warp_diag_reader.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── dns-analytics
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── dns-analytics.app.ts
│ │ │ ├── dns-analytics.context.ts
│ │ │ └── tools
│ │ │ └── dex-analytics.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-ai-search
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-ai-search.app.ts
│ │ │ └── docs-ai-search.context.ts
│ │ ├── tsconfig.json
│ │ ├── 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-ai-search.prompts.ts
│ │ │ │ └── docs-vectorize.prompts.ts
│ │ │ ├── scopes.ts
│ │ │ ├── sentry.ts
│ │ │ ├── server.ts
│ │ │ ├── tools
│ │ │ │ ├── account.tools.ts
│ │ │ │ ├── d1.tools.ts
│ │ │ │ ├── docs-ai-search.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
│ │ │ └── workers-oauth-utils.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/mcp-common/src/types/hyperdrive.types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import type { ConfigCreateParams } from 'cloudflare/resources/hyperdrive/configs.mjs'
4 |
5 | // --- Base Field Schemas ---
6 |
7 | /** Zod schema for a Hyperdrive config ID. */
8 | export const HyperdriveConfigIdSchema = z
9 | .string()
10 | .describe('The ID of the Hyperdrive configuration')
11 |
12 | /** Zod schema for a Hyperdrive config name. */
13 | export const HyperdriveConfigNameSchema: z.ZodType<ConfigCreateParams['name']> = z
14 | .string()
15 | .min(1)
16 | .max(64)
17 | .regex(/^[a-zA-Z0-9_-]+$/)
18 | .describe('The name of the Hyperdrive configuration (alphanumeric, underscore, hyphen)')
19 |
20 | // --- Origin Field Schemas ---
21 |
22 | /** Zod schema for the origin database name. */
23 | export const HyperdriveOriginDatabaseSchema: z.ZodType<
24 | ConfigCreateParams.PublicDatabase['database']
25 | > = z.string().describe('The database name')
26 | /** Zod schema for the origin database host. */
27 | export const HyperdriveOriginHostSchema: z.ZodType<ConfigCreateParams.PublicDatabase['host']> = z
28 | .string()
29 | .describe('The database host address')
30 | /** Zod schema for the origin database port. */
31 | export const HyperdriveOriginPortSchema: z.ZodType<ConfigCreateParams.PublicDatabase['port']> = z
32 | .number()
33 | .int()
34 | .min(1)
35 | .max(65535)
36 | .describe('The database port')
37 | /** Zod schema for the origin database scheme. */
38 | export const HyperdriveOriginSchemeSchema: z.ZodType<ConfigCreateParams.PublicDatabase['scheme']> =
39 | z.enum(['postgresql']).describe('The database protocol')
40 | /** Zod schema for the origin database user. */
41 | export const HyperdriveOriginUserSchema: z.ZodType<ConfigCreateParams.PublicDatabase['user']> = z
42 | .string()
43 | .describe('The database user')
44 | /** Zod schema for the origin database password. */
45 | export const HyperdriveOriginPasswordSchema: z.ZodType<
46 | ConfigCreateParams.PublicDatabase['password']
47 | > = z.string().describe('The database password')
48 |
49 | // --- Caching Field Schemas (Referencing ConfigCreateParams.HyperdriveHyperdriveCachingEnabled) ---
50 |
51 | /** Zod schema for disabling caching. */
52 | export const HyperdriveCachingDisabledSchema: z.ZodType<
53 | ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['disabled']
54 | > = z.boolean().optional().describe('Whether caching is disabled')
55 | /** Zod schema for the maximum cache age. */
56 | export const HyperdriveCachingMaxAgeSchema: z.ZodType<
57 | ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['max_age']
58 | > = z.number().int().min(1).optional().describe('Maximum cache age in seconds')
59 | /** Zod schema for the stale while revalidate duration. */
60 | export const HyperdriveCachingStaleWhileRevalidateSchema: z.ZodType<
61 | ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['stale_while_revalidate']
62 | > = z.number().int().min(1).optional().describe('Stale while revalidate duration in seconds')
63 |
64 | // --- List Parameter Schemas (Cannot directly type against SDK ConfigListParams which only has account_id) ---
65 |
66 | /** Zod schema for the list page number. */
67 | export const HyperdriveListParamPageSchema = z
68 | .number()
69 | .int()
70 | .positive()
71 | .optional()
72 | .describe('Page number of results')
73 | /** Zod schema for the list results per page. */
74 | export const HyperdriveListParamPerPageSchema = z
75 | .number()
76 | .int()
77 | .min(1)
78 | .max(100)
79 | .optional()
80 | .describe('Number of results per page')
81 | /** Zod schema for the list order field. */
82 | export const HyperdriveListParamOrderSchema = z
83 | .enum(['id', 'name'])
84 | .optional()
85 | .describe('Field to order by')
86 | /** Zod schema for the list order direction. */
87 | export const HyperdriveListParamDirectionSchema = z
88 | .enum(['asc', 'desc'])
89 | .optional()
90 | .describe('Direction to order')
91 |
92 | // --- Tool Parameter Schemas ---
93 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/format.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it } from 'vitest'
2 |
3 | import { fmt } from './format'
4 |
5 | describe('fmt', () => {
6 | describe('trim()', () => {
7 | it('should return an empty string for an empty input', () => {
8 | expect(fmt.trim('')).toBe('')
9 | })
10 |
11 | it('should trim leading and trailing spaces', () => {
12 | expect(fmt.trim(' hello ')).toBe('hello')
13 | })
14 |
15 | it('should trim leading and trailing newlines', () => {
16 | expect(fmt.trim('\n\nhello\n\n')).toBe('hello')
17 | })
18 |
19 | it('should trim leading/trailing spaces and newlines from each line but not remove empty lines', () => {
20 | const input = `
21 | line1
22 | line2
23 |
24 | line3
25 | `
26 | const expected = `line1
27 | line2
28 |
29 | line3`
30 | expect(fmt.trim(input)).toBe(expected)
31 | })
32 |
33 | it('should handle a string that is already trimmed', () => {
34 | expect(fmt.trim('hello\nworld')).toBe('hello\nworld')
35 | })
36 |
37 | it('should handle a string with only spaces', () => {
38 | expect(fmt.trim(' ')).toBe('')
39 | })
40 |
41 | it('should handle a string with only newlines', () => {
42 | expect(fmt.trim('\n\n\n')).toBe('')
43 | })
44 |
45 | it('should preserve empty lines from the middle', () => {
46 | expect(fmt.trim('hello\n\nworld')).toBe('hello\n\nworld')
47 | })
48 | })
49 |
50 | describe('oneLine()', () => {
51 | it('should return an empty string for an empty input', () => {
52 | expect(fmt.oneLine('')).toBe('')
53 | })
54 |
55 | it('should convert a multi-line string to a single line', () => {
56 | expect(fmt.oneLine('hello\nworld')).toBe('hello world')
57 | })
58 |
59 | it('should trim leading/trailing spaces and newlines before joining', () => {
60 | expect(fmt.oneLine(' hello \n world \n')).toBe('hello world')
61 | })
62 |
63 | it('should remove empty lines before joining', () => {
64 | expect(fmt.oneLine('hello\n\nworld')).toBe('hello world')
65 | })
66 |
67 | it('should handle a string that is already a single line', () => {
68 | expect(fmt.oneLine('hello world')).toBe('hello world')
69 | })
70 |
71 | it('should handle a string with only spaces and newlines', () => {
72 | expect(fmt.oneLine(' \n \n ')).toBe('')
73 | })
74 | })
75 |
76 | describe('asTSV()', () => {
77 | it('should convert an empty array to an empty string', async () => {
78 | expect(await fmt.asTSV([])).toBe('')
79 | })
80 |
81 | it('should convert an array of one object to a TSV string', async () => {
82 | const data = [{ a: 1, b: 'hello' }]
83 | expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello')
84 | })
85 |
86 | it('should convert an array of multiple objects to a TSV string', async () => {
87 | const data = [
88 | { a: 1, b: 'hello' },
89 | { a: 2, b: 'world' },
90 | ]
91 | expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\tworld')
92 | })
93 |
94 | it('should handle objects with different keys (using keys from the first object as headers)', async () => {
95 | const data = [
96 | { a: 1, b: 'hello' },
97 | { a: 2, c: 'world' },
98 | ]
99 | expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\t')
100 | expect(await fmt.asTSV(data)).toMatchInlineSnapshot(`
101 | "a b
102 | 1 hello
103 | 2 "
104 | `)
105 | })
106 |
107 | it('should handle values with tabs and newlines (fast-csv should quote them)', async () => {
108 | const data = [{ name: 'John\tDoe', description: 'Line1\nLine2' }]
109 | expect(await fmt.asTSV(data)).toBe('name\tdescription\n"John\tDoe"\t"Line1\nLine2"')
110 | expect(await fmt.asTSV(data)).toMatchInlineSnapshot(`
111 | "name description
112 | "John Doe" "Line1
113 | Line2""
114 | `)
115 | })
116 |
117 | it('should handle values with quotes (fast-csv should escape them)', async () => {
118 | const data = [{ name: 'James "Jim" Raynor' }]
119 | expect(await fmt.asTSV(data)).toBe('name\n"James ""Jim"" Raynor"')
120 | expect(await fmt.asTSV(data)).toMatchInlineSnapshot(`
121 | "name
122 | "James ""Jim"" Raynor""
123 | `)
124 | })
125 | })
126 | })
127 |
```
--------------------------------------------------------------------------------
/apps/logpush/src/tools/logpush.tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api'
4 | import { getProps } from '@repo/mcp-common/src/get-props'
5 |
6 | import type { LogsMCP } from '../logpush.app'
7 |
8 | const zJobIdentifier = z.number().int().min(1).optional().describe('Unique id of the job.')
9 | const zEnabled = z.boolean().optional().describe('Flag that indicates if the job is enabled.')
10 | const zName = z
11 | .string()
12 | .regex(/^[a-zA-Z0-9\-.]*$/)
13 | .max(512)
14 | .nullable()
15 | .optional()
16 | .describe('Optional human readable job name. Not unique.')
17 | const zDataset = z
18 | .string()
19 | .regex(/^[a-zA-Z0-9_-]*$/)
20 | .max(256)
21 | .nullable()
22 | .optional()
23 | .describe('Name of the dataset.')
24 | const zLastComplete = z
25 | .string()
26 | .datetime()
27 | .nullable()
28 | .optional()
29 | .describe('Records the last time for which logs have been successfully pushed.')
30 | const zLastError = z
31 | .string()
32 | .datetime()
33 | .nullable()
34 | .optional()
35 | .describe('Records the last time the job failed.')
36 | const zErrorMessage = z
37 | .string()
38 | .nullable()
39 | .optional()
40 | .describe('If not null, the job is currently failing.')
41 |
42 | export const zLogpushJob = z
43 | .object({
44 | id: zJobIdentifier,
45 | enabled: zEnabled,
46 | name: zName,
47 | dataset: zDataset,
48 | last_complete: zLastComplete,
49 | last_error: zLastError,
50 | error_message: zErrorMessage,
51 | })
52 | .nullable()
53 | .optional()
54 |
55 | const zApiResponseCommon = z.object({
56 | success: z.literal(true),
57 | errors: z.array(z.object({ message: z.string() })).optional(),
58 | })
59 |
60 | const zLogPushJobResults = z.array(zLogpushJob).optional()
61 |
62 | // The complete schema for zone_logpush_job_response_collection
63 | export const zLogpushJobResponseCollection = zApiResponseCommon.extend({
64 | result: zLogPushJobResults,
65 | })
66 |
67 | /**
68 | * Fetches available telemetry keys for a specified Cloudflare Worker
69 | * @param accountId Cloudflare account ID
70 | * @param apiToken Cloudflare API token
71 | * @returns List of telemetry keys available for the worker
72 | */
73 |
74 | export async function handleGetAccountLogPushJobs(
75 | accountId: string,
76 | apiToken: string
77 | ): Promise<z.infer<typeof zLogPushJobResults>> {
78 | // Call the Public API
79 | const data = await fetchCloudflareApi({
80 | endpoint: `/logpush/jobs`,
81 | accountId,
82 | apiToken,
83 | responseSchema: zLogpushJobResponseCollection,
84 | options: {
85 | method: 'GET',
86 | headers: {
87 | 'Content-Type': 'application/json',
88 | 'portal-version': '2',
89 | },
90 | },
91 | })
92 |
93 | const res = data as z.infer<typeof zLogpushJobResponseCollection>
94 | return (res.result ?? []).slice(0, 100)
95 | }
96 |
97 | /**
98 | * Registers the logs analysis tool with the MCP server
99 | * @param server The MCP server instance
100 | * @param accountId Cloudflare account ID
101 | * @param apiToken Cloudflare API token
102 | */
103 | export function registerLogsTools(agent: LogsMCP) {
104 | // Register the worker logs analysis tool by worker name
105 | agent.server.tool(
106 | 'logpush_jobs_by_account_id',
107 | `All Logpush jobs by Account ID.
108 |
109 | You should use this tool when:
110 | - You have questions or wish to request information about their Cloudflare Logpush jobs by account
111 | - You want a condensed version for the output results of your account's Cloudflare Logpush job
112 |
113 | This tool returns at most the first 100 jobs.
114 | `,
115 | {},
116 | async () => {
117 | const accountId = await agent.getActiveAccountId()
118 | if (!accountId) {
119 | return {
120 | content: [
121 | {
122 | type: 'text',
123 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
124 | },
125 | ],
126 | }
127 | }
128 | try {
129 | const props = getProps(agent)
130 | const result = await handleGetAccountLogPushJobs(accountId, props.accessToken)
131 | return {
132 | content: [
133 | {
134 | type: 'text',
135 | text: JSON.stringify({
136 | result,
137 | }),
138 | },
139 | ],
140 | }
141 | } catch (e) {
142 | agent.server.recordError(e)
143 | return {
144 | content: [
145 | {
146 | type: 'text',
147 | text: JSON.stringify({
148 | error: `Error analyzing logpush jobs: ${e instanceof Error && e.message}`,
149 | }),
150 | },
151 | ],
152 | }
153 | }
154 | }
155 | )
156 | }
157 |
```
--------------------------------------------------------------------------------
/apps/browser-rendering/src/tools/browser.tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
4 | import { getProps } from '@repo/mcp-common/src/get-props'
5 |
6 | import type { BrowserMCP } from '../browser.app'
7 |
8 | export function registerBrowserTools(agent: BrowserMCP) {
9 | agent.server.tool(
10 | 'get_url_html_content',
11 | 'Get page HTML content',
12 | {
13 | url: z.string().url(),
14 | },
15 | async (params) => {
16 | const accountId = await agent.getActiveAccountId()
17 | if (!accountId) {
18 | return {
19 | content: [
20 | {
21 | type: 'text',
22 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
23 | },
24 | ],
25 | }
26 | }
27 | try {
28 | const props = getProps(agent)
29 | const client = getCloudflareClient(props.accessToken)
30 | const r = await client.browserRendering.content.create({
31 | account_id: accountId,
32 | url: params.url,
33 | })
34 |
35 | return {
36 | content: [
37 | {
38 | type: 'text',
39 | text: JSON.stringify({
40 | result: r,
41 | }),
42 | },
43 | ],
44 | }
45 | } catch (error) {
46 | return {
47 | content: [
48 | {
49 | type: 'text',
50 | text: `Error getting page html: ${error instanceof Error && error.message}`,
51 | },
52 | ],
53 | }
54 | }
55 | }
56 | )
57 |
58 | agent.server.tool(
59 | 'get_url_markdown',
60 | 'Get page converted into Markdown',
61 | {
62 | url: z.string().url(),
63 | },
64 | async (params) => {
65 | const accountId = await agent.getActiveAccountId()
66 | if (!accountId) {
67 | return {
68 | content: [
69 | {
70 | type: 'text',
71 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
72 | },
73 | ],
74 | }
75 | }
76 | try {
77 | const props = getProps(agent)
78 | const client = getCloudflareClient(props.accessToken)
79 | const r = (await client.post(`/accounts/${accountId}/browser-rendering/markdown`, {
80 | body: {
81 | url: params.url,
82 | },
83 | })) as { result: string }
84 |
85 | return {
86 | content: [
87 | {
88 | type: 'text',
89 | text: JSON.stringify({
90 | result: r.result,
91 | }),
92 | },
93 | ],
94 | }
95 | } catch (error) {
96 | return {
97 | content: [
98 | {
99 | type: 'text',
100 | text: `Error getting page in markdown: ${error instanceof Error && error.message}`,
101 | },
102 | ],
103 | }
104 | }
105 | }
106 | )
107 |
108 | agent.server.tool(
109 | 'get_url_screenshot',
110 | 'Get page screenshot',
111 | {
112 | url: z.string().url(),
113 | viewport: z
114 | .object({
115 | height: z.number().default(600),
116 | width: z.number().default(800),
117 | })
118 | .optional(),
119 | },
120 | async (params) => {
121 | const accountId = await agent.getActiveAccountId()
122 | if (!accountId) {
123 | return {
124 | content: [
125 | {
126 | type: 'text',
127 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
128 | },
129 | ],
130 | }
131 | }
132 | try {
133 | const props = getProps(agent)
134 | // Cf client appears to be broken, so we use the raw API instead.
135 | // const client = getCloudflareClient(props.accessToken)
136 | // const r = await client.browserRendering.screenshot.create({
137 | // account_id: accountId,
138 | // url: params.url,
139 | // viewport: params.viewport,
140 | // })
141 |
142 | const r = await fetch(
143 | `https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/screenshot`,
144 | {
145 | method: 'POST',
146 | headers: {
147 | 'Content-Type': 'application/json',
148 | Authorization: `Bearer ${props.accessToken}`,
149 | },
150 | body: JSON.stringify({
151 | url: params.url,
152 | viewport: params.viewport,
153 | }),
154 | }
155 | )
156 |
157 | const arrayBuffer = await r.arrayBuffer()
158 | const base64Image = Buffer.from(arrayBuffer).toString('base64')
159 |
160 | return {
161 | content: [
162 | {
163 | type: 'image',
164 | mimeType: 'image/png',
165 | data: base64Image,
166 | },
167 | ],
168 | }
169 | } catch (error) {
170 | return {
171 | content: [
172 | {
173 | type: 'text',
174 | text: `Error getting page in markdown: ${error instanceof Error && error.message}`,
175 | },
176 | ],
177 | }
178 | }
179 | }
180 | )
181 | }
182 |
```
--------------------------------------------------------------------------------
/apps/cloudflare-one-casb/src/cf1-casb.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 |
16 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
17 | import { registerIntegrationsTools } from './tools/integrations.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './cf1-casb.context'
21 |
22 | export { UserDetails }
23 |
24 | const env = getEnv<Env>()
25 |
26 | const metrics = new MetricsTracker(env.MCP_METRICS, {
27 | name: env.MCP_SERVER_NAME,
28 | version: env.MCP_SERVER_VERSION,
29 | })
30 |
31 | // Context from the auth process, encrypted & stored in the auth token
32 | // and provided to the DurableMCP as this.props
33 | type Props = AuthProps
34 |
35 | type State = { activeAccountId: string | null }
36 | export class CASBMCP extends McpAgent<Env, State, Props> {
37 | _server: CloudflareMCPServer | undefined
38 | set server(server: CloudflareMCPServer) {
39 | this._server = server
40 | }
41 |
42 | get server(): CloudflareMCPServer {
43 | if (!this._server) {
44 | throw new Error('Tried to access server before it was initialized')
45 | }
46 |
47 | return this._server
48 | }
49 |
50 | constructor(ctx: DurableObjectState, env: Env) {
51 | super(ctx, env)
52 | }
53 |
54 | async init() {
55 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
56 | const props = getProps(this)
57 | const userId = props.type === 'user_token' ? props.user.id : undefined
58 |
59 | this.server = new CloudflareMCPServer({
60 | userId,
61 | wae: this.env.MCP_METRICS,
62 | serverInfo: {
63 | name: this.env.MCP_SERVER_NAME,
64 | version: this.env.MCP_SERVER_VERSION,
65 | },
66 | })
67 |
68 | registerAccountTools(this)
69 | registerIntegrationsTools(this)
70 | }
71 |
72 | async getActiveAccountId() {
73 | try {
74 | const props = getProps(this)
75 | // account tokens are scoped to one account
76 | if (props.type === 'account_token') {
77 | return props.account.id
78 | }
79 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
80 | // we do this so we can persist activeAccountId across sessions
81 | const userDetails = getUserDetails(env, props.user.id)
82 | return await userDetails.getActiveAccountId()
83 | } catch (e) {
84 | this.server.recordError(e)
85 | return null
86 | }
87 | }
88 |
89 | async setActiveAccountId(accountId: string) {
90 | try {
91 | const props = getProps(this)
92 | // account tokens are scoped to one account
93 | if (props.type === 'account_token') {
94 | return
95 | }
96 | const userDetails = getUserDetails(env, props.user.id)
97 | await userDetails.setActiveAccountId(accountId)
98 | } catch (e) {
99 | this.server.recordError(e)
100 | }
101 | }
102 | }
103 | const CloudflareOneCasbScopes = {
104 | ...RequiredScopes,
105 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
106 | 'teams:read': 'See Cloudflare One Resources',
107 | } as const
108 |
109 | export default {
110 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
111 | if (await isApiTokenRequest(req, env)) {
112 | return await handleApiTokenMode(CASBMCP, req, env, ctx)
113 | }
114 |
115 | return new OAuthProvider({
116 | apiHandlers: {
117 | '/mcp': CASBMCP.serve('/mcp'),
118 | '/sse': CASBMCP.serveSSE('/sse'),
119 | },
120 | // @ts-ignore
121 | defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }),
122 | authorizeEndpoint: '/oauth/authorize',
123 | tokenEndpoint: '/token',
124 | tokenExchangeCallback: (options) =>
125 | handleTokenExchangeCallback(
126 | options,
127 | env.CLOUDFLARE_CLIENT_ID,
128 | env.CLOUDFLARE_CLIENT_SECRET
129 | ),
130 | // Cloudflare access token TTL
131 | accessTokenTTL: 3600,
132 | clientRegistrationEndpoint: '/register',
133 | }).fetch(req, env, ctx)
134 | },
135 | }
136 |
```
--------------------------------------------------------------------------------
/apps/autorag/src/autorag.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 |
16 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
17 | import { registerAutoRAGTools } from './tools/autorag.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './autorag.context'
21 |
22 | const env = getEnv<Env>()
23 |
24 | export { UserDetails }
25 |
26 | const metrics = new MetricsTracker(env.MCP_METRICS, {
27 | name: env.MCP_SERVER_NAME,
28 | version: env.MCP_SERVER_VERSION,
29 | })
30 |
31 | // Context from the auth process, encrypted & stored in the auth token
32 | // and provided to the DurableMCP as this.props
33 | type Props = AuthProps
34 | type State = { activeAccountId: string | null }
35 |
36 | export class AutoRAGMCP extends McpAgent<Env, State, Props> {
37 | _server: CloudflareMCPServer | undefined
38 | set server(server: CloudflareMCPServer) {
39 | this._server = server
40 | }
41 | get server(): CloudflareMCPServer {
42 | if (!this._server) {
43 | throw new Error('Tried to access server before it was initialized')
44 | }
45 |
46 | return this._server
47 | }
48 |
49 | constructor(ctx: DurableObjectState, env: Env) {
50 | super(ctx, env)
51 | }
52 |
53 | async init() {
54 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
55 | const props = getProps(this)
56 | const userId = props.type === 'user_token' ? props.user.id : undefined
57 |
58 | this.server = new CloudflareMCPServer({
59 | userId,
60 | wae: this.env.MCP_METRICS,
61 | serverInfo: {
62 | name: this.env.MCP_SERVER_NAME,
63 | version: this.env.MCP_SERVER_VERSION,
64 | },
65 | })
66 |
67 | registerAccountTools(this)
68 |
69 | // Register Cloudflare Log Push tools
70 | registerAutoRAGTools(this)
71 | }
72 |
73 | async getActiveAccountId() {
74 | try {
75 | const props = getProps(this)
76 | // account tokens are scoped to one account
77 | if (props.type === 'account_token') {
78 | return props.account.id
79 | }
80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
81 | // we do this so we can persist activeAccountId across sessions
82 | const userDetails = getUserDetails(env, props.user.id)
83 | return await userDetails.getActiveAccountId()
84 | } catch (e) {
85 | this.server.recordError(e)
86 | return null
87 | }
88 | }
89 |
90 | async setActiveAccountId(accountId: string) {
91 | try {
92 | const props = getProps(this)
93 | // account tokens are scoped to one account
94 | if (props.type === 'account_token') {
95 | return
96 | }
97 | const userDetails = getUserDetails(env, props.user.id)
98 | await userDetails.setActiveAccountId(accountId)
99 | } catch (e) {
100 | this.server.recordError(e)
101 | }
102 | }
103 | }
104 |
105 | const LogPushScopes = {
106 | ...RequiredScopes,
107 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
108 | 'rag:write': 'Grants write level access to AutoRag.',
109 | } as const
110 |
111 | export default {
112 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
113 | if (await isApiTokenRequest(req, env)) {
114 | return await handleApiTokenMode(AutoRAGMCP, req, env, ctx)
115 | }
116 |
117 | return new OAuthProvider({
118 | apiHandlers: {
119 | '/mcp': AutoRAGMCP.serve('/mcp'),
120 | '/sse': AutoRAGMCP.serveSSE('/sse'),
121 | },
122 | // @ts-ignore
123 | defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }),
124 | authorizeEndpoint: '/oauth/authorize',
125 | tokenEndpoint: '/token',
126 | tokenExchangeCallback: (options) =>
127 | handleTokenExchangeCallback(
128 | options,
129 | env.CLOUDFLARE_CLIENT_ID,
130 | env.CLOUDFLARE_CLIENT_SECRET
131 | ),
132 | // Cloudflare access token TTL
133 | accessTokenTTL: 3600,
134 | clientRegistrationEndpoint: '/register',
135 | }).fetch(req, env, ctx)
136 | },
137 | }
138 |
```
--------------------------------------------------------------------------------
/apps/browser-rendering/src/browser.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 |
16 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
17 | import { registerBrowserTools } from './tools/browser.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './browser.context'
21 |
22 | const env = getEnv<Env>()
23 |
24 | export { UserDetails }
25 |
26 | const metrics = new MetricsTracker(env.MCP_METRICS, {
27 | name: env.MCP_SERVER_NAME,
28 | version: env.MCP_SERVER_VERSION,
29 | })
30 |
31 | // Context from the auth process, encrypted & stored in the auth token
32 | // and provided to the DurableMCP as this.props
33 | type Props = AuthProps
34 | type State = { activeAccountId: string | null }
35 |
36 | export class BrowserMCP extends McpAgent<Env, State, Props> {
37 | _server: CloudflareMCPServer | undefined
38 | set server(server: CloudflareMCPServer) {
39 | this._server = server
40 | }
41 | get server(): CloudflareMCPServer {
42 | if (!this._server) {
43 | throw new Error('Tried to access server before it was initialized')
44 | }
45 |
46 | return this._server
47 | }
48 |
49 | constructor(ctx: DurableObjectState, env: Env) {
50 | super(ctx, env)
51 | }
52 |
53 | async init() {
54 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
55 | const props = getProps(this)
56 | const userId = props.type === 'user_token' ? props.user.id : undefined
57 |
58 | this.server = new CloudflareMCPServer({
59 | userId,
60 | wae: this.env.MCP_METRICS,
61 | serverInfo: {
62 | name: this.env.MCP_SERVER_NAME,
63 | version: this.env.MCP_SERVER_VERSION,
64 | },
65 | })
66 |
67 | registerAccountTools(this)
68 |
69 | // Register Cloudflare Log Push tools
70 | registerBrowserTools(this)
71 | }
72 |
73 | async getActiveAccountId() {
74 | try {
75 | const props = getProps(this)
76 | // account tokens are scoped to one account
77 | if (props.type === 'account_token') {
78 | return props.account.id
79 | }
80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
81 | // we do this so we can persist activeAccountId across sessions
82 | const userDetails = getUserDetails(env, props.user.id)
83 | return await userDetails.getActiveAccountId()
84 | } catch (e) {
85 | this.server.recordError(e)
86 | return null
87 | }
88 | }
89 |
90 | async setActiveAccountId(accountId: string) {
91 | try {
92 | const props = getProps(this)
93 | // account tokens are scoped to one account
94 | if (props.type === 'account_token') {
95 | return
96 | }
97 | const userDetails = getUserDetails(env, props.user.id)
98 | await userDetails.setActiveAccountId(accountId)
99 | } catch (e) {
100 | this.server.recordError(e)
101 | }
102 | }
103 | }
104 |
105 | const BrowserScopes = {
106 | ...RequiredScopes,
107 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
108 | 'browser:write': 'Grants write level access to Browser Rendering.',
109 | } as const
110 |
111 | export default {
112 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
113 | if (await isApiTokenRequest(req, env)) {
114 | return await handleApiTokenMode(BrowserMCP, req, env, ctx)
115 | }
116 |
117 | return new OAuthProvider({
118 | apiHandlers: {
119 | '/mcp': BrowserMCP.serve('/mcp'),
120 | '/sse': BrowserMCP.serveSSE('/sse'),
121 | },
122 | // @ts-ignore
123 | defaultHandler: createAuthHandlers({ scopes: BrowserScopes, metrics }),
124 | authorizeEndpoint: '/oauth/authorize',
125 | tokenEndpoint: '/token',
126 | tokenExchangeCallback: (options) =>
127 | handleTokenExchangeCallback(
128 | options,
129 | env.CLOUDFLARE_CLIENT_ID,
130 | env.CLOUDFLARE_CLIENT_SECRET
131 | ),
132 | // Cloudflare access token TTL
133 | accessTokenTTL: 3600,
134 | clientRegistrationEndpoint: '/register',
135 | }).fetch(req, env, ctx)
136 | },
137 | }
138 |
```
--------------------------------------------------------------------------------
/apps/ai-gateway/src/ai-gateway.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 |
16 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
17 | import { registerAIGatewayTools } from './tools/ai-gateway.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './ai-gateway.context'
21 |
22 | const env = getEnv<Env>()
23 |
24 | export { UserDetails }
25 |
26 | const metrics = new MetricsTracker(env.MCP_METRICS, {
27 | name: env.MCP_SERVER_NAME,
28 | version: env.MCP_SERVER_VERSION,
29 | })
30 |
31 | // Context from the auth process, encrypted & stored in the auth token
32 | // and provided to the DurableMCP as this.props
33 | type Props = AuthProps
34 | type State = { activeAccountId: string | null }
35 |
36 | export class AIGatewayMCP extends McpAgent<Env, State, Props> {
37 | _server: CloudflareMCPServer | undefined
38 | set server(server: CloudflareMCPServer) {
39 | this._server = server
40 | }
41 | get server(): CloudflareMCPServer {
42 | if (!this._server) {
43 | throw new Error('Tried to access server before it was initialized')
44 | }
45 |
46 | return this._server
47 | }
48 |
49 | constructor(ctx: DurableObjectState, env: Env) {
50 | super(ctx, env)
51 | }
52 |
53 | async init() {
54 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
55 | const props = getProps(this)
56 | const userId = props.type === 'user_token' ? props.user.id : undefined
57 |
58 | this.server = new CloudflareMCPServer({
59 | userId,
60 | wae: this.env.MCP_METRICS,
61 | serverInfo: {
62 | name: this.env.MCP_SERVER_NAME,
63 | version: this.env.MCP_SERVER_VERSION,
64 | },
65 | })
66 |
67 | registerAccountTools(this)
68 |
69 | // Register Cloudflare Log Push tools
70 | registerAIGatewayTools(this)
71 | }
72 |
73 | async getActiveAccountId() {
74 | try {
75 | const props = getProps(this)
76 | // account tokens are scoped to one account
77 | if (props.type === 'account_token') {
78 | return props.account.id
79 | }
80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
81 | // we do this so we can persist activeAccountId across sessions
82 | const userDetails = getUserDetails(env, props.user.id)
83 | return await userDetails.getActiveAccountId()
84 | } catch (e) {
85 | this.server.recordError(e)
86 | return null
87 | }
88 | }
89 |
90 | async setActiveAccountId(accountId: string) {
91 | try {
92 | const props = getProps(this)
93 | // account tokens are scoped to one account
94 | if (props.type === 'account_token') {
95 | return
96 | }
97 | const userDetails = getUserDetails(env, props.user.id)
98 | await userDetails.setActiveAccountId(accountId)
99 | } catch (e) {
100 | this.server.recordError(e)
101 | }
102 | }
103 | }
104 |
105 | const AIGatewayScopes = {
106 | ...RequiredScopes,
107 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
108 | 'aig:read': 'Grants read level access to AI Gateway.',
109 | } as const
110 |
111 | export default {
112 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
113 | if (await isApiTokenRequest(req, env)) {
114 | return await handleApiTokenMode(AIGatewayMCP, req, env, ctx)
115 | }
116 |
117 | return new OAuthProvider({
118 | apiHandlers: {
119 | '/mcp': AIGatewayMCP.serve('/mcp'),
120 | '/sse': AIGatewayMCP.serveSSE('/sse'),
121 | },
122 | // @ts-ignore
123 | defaultHandler: createAuthHandlers({ scopes: AIGatewayScopes, metrics }),
124 | authorizeEndpoint: '/oauth/authorize',
125 | tokenEndpoint: '/token',
126 | tokenExchangeCallback: (options) =>
127 | handleTokenExchangeCallback(
128 | options,
129 | env.CLOUDFLARE_CLIENT_ID,
130 | env.CLOUDFLARE_CLIENT_SECRET
131 | ),
132 | // Cloudflare access token TTL
133 | accessTokenTTL: 3600,
134 | clientRegistrationEndpoint: '/register',
135 | }).fetch(req, env, ctx)
136 | },
137 | }
138 |
```
--------------------------------------------------------------------------------
/apps/auditlogs/src/auditlogs.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 |
16 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
17 | import { registerAuditLogTools } from './tools/auditlogs.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './auditlogs.context'
21 |
22 | const env = getEnv<Env>()
23 |
24 | export { UserDetails }
25 |
26 | const metrics = new MetricsTracker(env.MCP_METRICS, {
27 | name: env.MCP_SERVER_NAME,
28 | version: env.MCP_SERVER_VERSION,
29 | })
30 |
31 | // Context from the auth process, encrypted & stored in the auth token
32 | // and provided to the DurableMCP as this.props
33 | type Props = AuthProps
34 |
35 | export type State = { activeAccountId: string | null }
36 |
37 | export class AuditlogMCP extends McpAgent<Env, State, Props> {
38 | _server: CloudflareMCPServer | undefined
39 | set server(server: CloudflareMCPServer) {
40 | this._server = server
41 | }
42 | get server(): CloudflareMCPServer {
43 | if (!this._server) {
44 | throw new Error('Tried to access server before it was initialized')
45 | }
46 |
47 | return this._server
48 | }
49 |
50 | constructor(ctx: DurableObjectState, env: Env) {
51 | super(ctx, env)
52 | }
53 |
54 | async init() {
55 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
56 | const props = getProps(this)
57 | const userId = props.type === 'user_token' ? props.user.id : undefined
58 |
59 | this.server = new CloudflareMCPServer({
60 | userId,
61 | wae: this.env.MCP_METRICS,
62 | serverInfo: {
63 | name: this.env.MCP_SERVER_NAME,
64 | version: this.env.MCP_SERVER_VERSION,
65 | },
66 | })
67 | registerAccountTools(this)
68 |
69 | // Register Cloudflare Audit Log tools
70 | registerAuditLogTools(this)
71 | }
72 |
73 | async getActiveAccountId() {
74 | try {
75 | const props = getProps(this)
76 | // account tokens are scoped to one account
77 | if (props.type === 'account_token') {
78 | return props.account.id
79 | }
80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
81 | // we do this so we can persist activeAccountId across sessions
82 | const userDetails = getUserDetails(env, props.user.id)
83 | return await userDetails.getActiveAccountId()
84 | } catch (e) {
85 | this.server.recordError(e)
86 | return null
87 | }
88 | }
89 |
90 | async setActiveAccountId(accountId: string) {
91 | try {
92 | const props = getProps(this)
93 | // account tokens are scoped to one account
94 | if (props.type === 'account_token') {
95 | return
96 | }
97 | const userDetails = getUserDetails(env, props.user.id)
98 | await userDetails.setActiveAccountId(accountId)
99 | } catch (e) {
100 | this.server.recordError(e)
101 | }
102 | }
103 | }
104 |
105 | const AuditlogScopes = {
106 | ...RequiredScopes,
107 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
108 | 'auditlogs:read': 'See your resource configuration changes.',
109 | } as const
110 |
111 | export default {
112 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
113 | if (await isApiTokenRequest(req, env)) {
114 | return await handleApiTokenMode(AuditlogMCP, req, env, ctx)
115 | }
116 |
117 | return new OAuthProvider({
118 | apiHandlers: {
119 | '/mcp': AuditlogMCP.serve('/mcp'),
120 | '/sse': AuditlogMCP.serveSSE('/sse'),
121 | },
122 | // @ts-ignore
123 | defaultHandler: createAuthHandlers({ scopes: AuditlogScopes, metrics }),
124 | authorizeEndpoint: '/oauth/authorize',
125 | tokenEndpoint: '/token',
126 | tokenExchangeCallback: (options) =>
127 | handleTokenExchangeCallback(
128 | options,
129 | env.CLOUDFLARE_CLIENT_ID,
130 | env.CLOUDFLARE_CLIENT_SECRET
131 | ),
132 | // Cloudflare access token TTL
133 | accessTokenTTL: 3600,
134 | clientRegistrationEndpoint: '/register',
135 | }).fetch(req, env, ctx)
136 | },
137 | }
138 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/docs-vectorize.tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4 |
5 | interface RequiredEnv {
6 | AI: Ai
7 | VECTORIZE: VectorizeIndex
8 | }
9 |
10 | // Always return 10 results for simplicity, don't make it configurable
11 | const TOP_K = 10
12 |
13 | /**
14 | * Registers the docs search tool with the MCP server
15 | * @param server The MCP server instance
16 | */
17 | export function registerDocsTools(server: McpServer, env: RequiredEnv) {
18 | server.tool(
19 | 'search_cloudflare_documentation',
20 | `Search the Cloudflare documentation.
21 |
22 | This tool should be used to answer any question about Cloudflare products or features, including:
23 | - Workers, Pages, R2, Images, Stream, D1, Durable Objects, KV, Workflows, Hyperdrive, Queues
24 | - AI Search, Workers AI, Vectorize, AI Gateway, Browser Rendering
25 | - Zero Trust, Access, Tunnel, Gateway, Browser Isolation, WARP, DDOS, Magic Transit, Magic WAN
26 | - CDN, Cache, DNS, Zaraz, Argo, Rulesets, Terraform, Account and Billing
27 |
28 | Results are returned as semantically similar chunks to the query.
29 | `,
30 | {
31 | query: z.string(),
32 | },
33 | {
34 | title: 'Search Cloudflare docs',
35 | annotations: {
36 | readOnlyHint: true,
37 | },
38 | },
39 | async ({ query }) => {
40 | const results = await queryVectorize(env.AI, env.VECTORIZE, query, TOP_K)
41 | const resultsAsXml = results
42 | .map((result) => {
43 | return `<result>
44 | <url>${result.url}</url>
45 | <title>${result.title}</title>
46 | <text>
47 | ${result.text}
48 | </text>
49 | </result>`
50 | })
51 | .join('\n')
52 | return {
53 | content: [{ type: 'text', text: resultsAsXml }],
54 | }
55 | }
56 | )
57 |
58 | // Note: this is a tool instead of a prompt because
59 | // prompt support is much less common than tools.
60 | server.tool(
61 | 'migrate_pages_to_workers_guide',
62 | `ALWAYS read this guide before migrating Pages projects to Workers.`,
63 | {},
64 | {
65 | title: 'Get Pages migration guide',
66 | annotations: {
67 | readOnlyHint: true,
68 | },
69 | },
70 | async () => {
71 | const res = await fetch(
72 | 'https://developers.cloudflare.com/workers/prompts/pages-to-workers.txt',
73 | {
74 | cf: { cacheEverything: true, cacheTtl: 3600 },
75 | }
76 | )
77 |
78 | if (!res.ok) {
79 | return {
80 | content: [{ type: 'text', text: 'Error: Failed to fetch guide. Please try again.' }],
81 | }
82 | }
83 |
84 | return {
85 | content: [
86 | {
87 | type: 'text',
88 | text: await res.text(),
89 | },
90 | ],
91 | }
92 | }
93 | )
94 | }
95 |
96 | async function queryVectorize(ai: Ai, vectorizeIndex: VectorizeIndex, query: string, topK: number) {
97 | // Recommendation from: https://ai.google.dev/gemma/docs/embeddinggemma/model_card#prompt_instructions
98 | const [queryEmbedding] = await getEmbeddings(ai, ['task: search result | query: ' + query])
99 |
100 | const { matches } = await vectorizeIndex.query(queryEmbedding, {
101 | topK,
102 | returnMetadata: 'all',
103 | returnValues: false,
104 | })
105 |
106 | return matches.map((match, _i) => ({
107 | similarity: Math.min(match.score, 1),
108 | id: match.id,
109 | url: sourceToUrl(String(match.metadata?.filePath ?? '')),
110 | title: String(match.metadata?.title ?? ''),
111 | text: String(match.metadata?.text ?? ''),
112 | }))
113 | }
114 |
115 | const TOP_DIR = 'src/content/docs'
116 | function sourceToUrl(path: string) {
117 | const prefix = `${TOP_DIR}/`
118 | return (
119 | 'https://developers.cloudflare.com/' +
120 | (path.startsWith(prefix) ? path.slice(prefix.length) : path)
121 | .replace(/index\.mdx$/, '')
122 | .replace(/\.mdx$/, '')
123 | )
124 | }
125 |
126 | async function getEmbeddings(ai: Ai, strings: string[]): Promise<number[][]> {
127 | const response = await doWithRetries(() =>
128 | // @ts-expect-error embeddinggemma not in types yet
129 | ai.run('@cf/google/embeddinggemma-300m', {
130 | text: strings,
131 | })
132 | )
133 |
134 | // @ts-expect-error embeddinggemma not in types yet
135 | return response.data
136 | }
137 |
138 | /**
139 | * @template T
140 | * @param {() => Promise<T>} action
141 | */
142 | async function doWithRetries<T>(action: () => Promise<T>) {
143 | const NUM_RETRIES = 10
144 | const INIT_RETRY_MS = 50
145 | for (let i = 0; i <= NUM_RETRIES; i++) {
146 | try {
147 | return await action()
148 | } catch (e) {
149 | // TODO: distinguish between user errors (4xx) and system errors (5xx)
150 | console.error(e)
151 | if (i === NUM_RETRIES) {
152 | throw e
153 | }
154 | // Exponential backoff with full jitter
155 | await scheduler.wait(Math.random() * INIT_RETRY_MS * Math.pow(2, i))
156 | }
157 | }
158 | // Should never reach here – last loop iteration should return
159 | throw new Error('An unknown error occurred')
160 | }
161 |
```
--------------------------------------------------------------------------------
/apps/dex-analysis/src/dex-analysis.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 | import { MetricsTracker } from '@repo/mcp-observability'
16 |
17 | import { registerDEXTools } from './tools/dex-analysis.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './dex-analysis.context'
21 |
22 | export { UserDetails }
23 | export { WarpDiagReader } from './warp_diag_reader'
24 |
25 | const env = getEnv<Env>()
26 |
27 | const metrics = new MetricsTracker(env.MCP_METRICS, {
28 | name: env.MCP_SERVER_NAME,
29 | version: env.MCP_SERVER_VERSION,
30 | })
31 |
32 | // Context from the auth process, encrypted & stored in the auth token
33 | // and provided to the DurableMCP as this.props
34 | type Props = AuthProps
35 |
36 | type State = { activeAccountId: string | null }
37 |
38 | export class CloudflareDEXMCP extends McpAgent<Env, State, Props> {
39 | _server: CloudflareMCPServer | undefined
40 | set server(server: CloudflareMCPServer) {
41 | this._server = server
42 | }
43 |
44 | get server(): CloudflareMCPServer {
45 | if (!this._server) {
46 | throw new Error('Tried to access server before it was initialized')
47 | }
48 |
49 | return this._server
50 | }
51 |
52 | constructor(ctx: DurableObjectState, env: Env) {
53 | super(ctx, env)
54 | }
55 |
56 | async init() {
57 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
58 | const props = getProps(this)
59 | const userId = props.type === 'user_token' ? props.user.id : undefined
60 |
61 | this.server = new CloudflareMCPServer({
62 | userId,
63 | wae: this.env.MCP_METRICS,
64 | serverInfo: {
65 | name: this.env.MCP_SERVER_NAME,
66 | version: this.env.MCP_SERVER_VERSION,
67 | },
68 | })
69 |
70 | registerAccountTools(this)
71 | registerDEXTools(this)
72 | }
73 |
74 | async getActiveAccountId() {
75 | try {
76 | const props = getProps(this)
77 | // account tokens are scoped to one account
78 | if (props.type === 'account_token') {
79 | return props.account.id
80 | }
81 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
82 | // we do this so we can persist activeAccountId across sessions
83 | const userDetails = getUserDetails(env, props.user.id)
84 | return await userDetails.getActiveAccountId()
85 | } catch (e) {
86 | this.server.recordError(e)
87 | return null
88 | }
89 | }
90 |
91 | async setActiveAccountId(accountId: string) {
92 | try {
93 | const props = getProps(this)
94 | // account tokens are scoped to one account
95 | if (props.type === 'account_token') {
96 | return
97 | }
98 | const userDetails = getUserDetails(env, props.user.id)
99 | await userDetails.setActiveAccountId(accountId)
100 | } catch (e) {
101 | this.server.recordError(e)
102 | }
103 | }
104 | }
105 |
106 | const DexScopes = {
107 | ...RequiredScopes,
108 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
109 | 'dex:write':
110 | 'Grants write level access to DEX resources like tests, fleet status, and remote captures.',
111 | } as const
112 |
113 | export default {
114 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
115 | if (await isApiTokenRequest(req, env)) {
116 | return await handleApiTokenMode(CloudflareDEXMCP, req, env, ctx)
117 | }
118 |
119 | return new OAuthProvider({
120 | apiHandlers: {
121 | '/mcp': CloudflareDEXMCP.serve('/mcp'),
122 | '/sse': CloudflareDEXMCP.serveSSE('/sse'),
123 | },
124 | // @ts-ignore
125 | defaultHandler: createAuthHandlers({ scopes: DexScopes, metrics }),
126 | authorizeEndpoint: '/oauth/authorize',
127 | tokenEndpoint: '/token',
128 | tokenExchangeCallback: (options) =>
129 | handleTokenExchangeCallback(
130 | options,
131 | env.CLOUDFLARE_CLIENT_ID,
132 | env.CLOUDFLARE_CLIENT_SECRET
133 | ),
134 | // Cloudflare access token TTL
135 | accessTokenTTL: 3600,
136 | clientRegistrationEndpoint: '/register',
137 | }).fetch(req, env, ctx)
138 | },
139 | }
140 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/sentry.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { APIError } from 'cloudflare'
2 | import { Toucan, zodErrorsIntegration } from 'toucan-js'
3 |
4 | import { McpError } from './mcp-error'
5 |
6 | import type { BaseTransportOptions, Client, ClientOptions, Event, EventHint } from '@sentry/types'
7 | import type { Context, Next } from 'hono'
8 | import type { Context as SentryContext } from 'toucan-js/dist/types'
9 | import type { MCPEnvironment } from './config'
10 |
11 | function is5xxError(status: number): boolean {
12 | return status >= 500 && status <= 599
13 | }
14 |
15 | export class SentryClient {
16 | private sentry: Toucan
17 | constructor(sentry: Toucan) {
18 | this.sentry = sentry
19 | }
20 |
21 | public recordError(e: unknown) {
22 | if (this.sentry) {
23 | // ignore errors from McpError and APIError (cloudflare) that have reportToSentry = false, or aren't 5xx errors
24 | if (e instanceof McpError) {
25 | if (e.reportToSentry === false) {
26 | return
27 | }
28 | } else if (e instanceof APIError) {
29 | if (!is5xxError(e.status)) {
30 | return
31 | }
32 | }
33 | this.sentry.captureException(e)
34 | }
35 | }
36 |
37 | public setUser(userId: string) {
38 | this.sentry.setUser({ ...this.sentry.getUser(), user_id: userId })
39 | }
40 | }
41 |
42 | interface BaseBindings {
43 | ENVIRONMENT: MCPEnvironment
44 | GIT_HASH: string
45 | SENTRY_DSN: string
46 | SENTRY_ACCESS_CLIENT_ID: string
47 | SENTRY_ACCESS_CLIENT_SECRET: string
48 | }
49 |
50 | export interface BaseHonoContext {
51 | Bindings: BaseBindings
52 | Variables: {
53 | sentry?: SentryClient
54 | }
55 | }
56 |
57 | export function initSentry<T extends BaseBindings>(
58 | env: T,
59 | ctx: SentryContext,
60 | req?: Request<unknown, CfProperties>
61 | ): SentryClient {
62 | const sentry = new Toucan({
63 | dsn: env.SENTRY_DSN,
64 | request: req,
65 | environment: env.ENVIRONMENT,
66 | context: ctx,
67 | release: env.GIT_HASH,
68 | requestDataOptions: {
69 | allowedHeaders: [
70 | 'user-agent',
71 | 'cf-challenge',
72 | 'accept-encoding',
73 | 'accept-language',
74 | 'cf-ray',
75 | 'content-length',
76 | 'content-type',
77 | 'host',
78 | ],
79 | // Allow ONLY the “scope” param in order to avoid recording jwt, code, state and any other callback params
80 | allowedSearchParams: /^scope$/,
81 | },
82 | integrations: [
83 | zodErrorsIntegration({ saveAttachments: true }),
84 | {
85 | name: 'mcp-api-errors',
86 | processEvent(
87 | event: Event,
88 | _hint: EventHint,
89 | _client: Client<ClientOptions<BaseTransportOptions>>
90 | ): Event {
91 | const processedEvent = applyMcpErrorsToEvent(event)
92 | return processedEvent
93 | },
94 | },
95 | ],
96 | transportOptions: {
97 | headers: {
98 | 'CF-Access-Client-ID': env.SENTRY_ACCESS_CLIENT_ID,
99 | 'CF-Access-Client-Secret': env.SENTRY_ACCESS_CLIENT_SECRET,
100 | },
101 | },
102 | })
103 | return new SentryClient(sentry)
104 | }
105 |
106 | export function initSentryWithUser<T extends BaseBindings>(
107 | env: T,
108 | ctx: SentryContext,
109 | userId: string,
110 | req?: Request<unknown, CfProperties>
111 | ): SentryClient {
112 | const sentryClient = initSentry(env, ctx, req)
113 | sentryClient.setUser(userId)
114 | return sentryClient
115 | }
116 |
117 | export async function useSentry<T extends BaseHonoContext>(
118 | c: Context<T>,
119 | next: Next
120 | ): Promise<void> {
121 | c.set('sentry', initSentry(c.env, c.executionCtx, c.req.raw))
122 | await next()
123 | }
124 |
125 | export function setSentryRequestHeaders(sentry: Toucan, req: Request<unknown, CfProperties>) {
126 | const colo: string = req.cf && typeof req.cf.colo === 'string' ? req.cf.colo : 'UNKNOWN'
127 | sentry.setTag('colo', colo)
128 |
129 | const ip_address = req.headers.get('cf-connecting-ip') ?? ''
130 | const userAgent = req.headers.get('user-agent') ?? ''
131 | sentry.setUser({
132 | ...sentry.getUser(),
133 | ip_address,
134 | userAgent,
135 | colo,
136 | })
137 | }
138 |
139 | function applyMcpErrorsToEvent(event: Event): Event {
140 | if (event.exception === undefined || event.exception.values === undefined) {
141 | return event
142 | }
143 |
144 | if (event.exception instanceof McpError) {
145 | try {
146 | return {
147 | ...event,
148 | extra: {
149 | ...event.extra,
150 | statusCode: event.exception.code,
151 | internalMessage: event.exception.internalMessage,
152 | },
153 | }
154 | } catch (e) {
155 | // Hopefully we never throw errors here, but record it
156 | // with the event just in case.
157 | return {
158 | ...event,
159 | extra: {
160 | ...event.extra,
161 | 'McpError sentry integration parse error': {
162 | message: `an exception was thrown while processing McpError within applyMcpErrorsToEvent()`,
163 | error: e instanceof Error ? `${e.name}: ${e.cause}\n${e.stack}` : 'unknown',
164 | },
165 | },
166 | }
167 | }
168 | }
169 |
170 | return event
171 | }
172 |
```
--------------------------------------------------------------------------------
/apps/logpush/src/logpush.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 |
16 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
17 | import { registerLogsTools } from './tools/logpush.tools'
18 |
19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20 | import type { Env } from './logpush.context'
21 |
22 | const env = getEnv<Env>()
23 |
24 | export { UserDetails }
25 |
26 | const metrics = new MetricsTracker(env.MCP_METRICS, {
27 | name: env.MCP_SERVER_NAME,
28 | version: env.MCP_SERVER_VERSION,
29 | })
30 |
31 | // Context from the auth process, encrypted & stored in the auth token
32 | // and provided to the DurableMCP as this.props
33 | type Props = AuthProps
34 | type State = { activeAccountId: string | null }
35 |
36 | export class LogsMCP extends McpAgent<Env, State, Props> {
37 | _server: CloudflareMCPServer | undefined
38 | set server(server: CloudflareMCPServer) {
39 | this._server = server
40 | }
41 | get server(): CloudflareMCPServer {
42 | if (!this._server) {
43 | throw new Error('Tried to access server before it was initialized')
44 | }
45 |
46 | return this._server
47 | }
48 |
49 | constructor(ctx: DurableObjectState, env: Env) {
50 | super(ctx, env)
51 | }
52 |
53 | async init() {
54 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
55 | const props = getProps(this)
56 | const userId = props.type === 'user_token' ? props.user.id : undefined
57 |
58 | this.server = new CloudflareMCPServer({
59 | userId,
60 | wae: this.env.MCP_METRICS,
61 | serverInfo: {
62 | name: this.env.MCP_SERVER_NAME,
63 | version: this.env.MCP_SERVER_VERSION,
64 | },
65 | })
66 |
67 | registerAccountTools(this)
68 |
69 | // Register Cloudflare Log Push tools
70 | registerLogsTools(this)
71 | }
72 |
73 | async getActiveAccountId() {
74 | try {
75 | const props = getProps(this)
76 | // account tokens are scoped to one account
77 | if (props.type === 'account_token') {
78 | return props.account.id
79 | }
80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
81 | // we do this so we can persist activeAccountId across sessions
82 | const userDetails = getUserDetails(env, props.user.id)
83 | return await userDetails.getActiveAccountId()
84 | } catch (e) {
85 | this.server.recordError(e)
86 | return null
87 | }
88 | }
89 |
90 | async setActiveAccountId(accountId: string) {
91 | try {
92 | const props = getProps(this)
93 | // account tokens are scoped to one account
94 | if (props.type === 'account_token') {
95 | return
96 | }
97 | const userDetails = getUserDetails(env, props.user.id)
98 | await userDetails.setActiveAccountId(accountId)
99 | } catch (e) {
100 | this.server.recordError(e)
101 | }
102 | }
103 | }
104 |
105 | const LogPushScopes = {
106 | ...RequiredScopes,
107 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
108 | 'logpush:write':
109 | '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.',
110 | } as const
111 |
112 | export default {
113 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
114 | if (await isApiTokenRequest(req, env)) {
115 | return await handleApiTokenMode(LogsMCP, req, env, ctx)
116 | }
117 |
118 | return new OAuthProvider({
119 | apiHandlers: {
120 | '/mcp': LogsMCP.serve('/mcp'),
121 | '/sse': LogsMCP.serveSSE('/sse'),
122 | },
123 | // @ts-ignore
124 | defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }),
125 | authorizeEndpoint: '/oauth/authorize',
126 | tokenEndpoint: '/token',
127 | tokenExchangeCallback: (options) =>
128 | handleTokenExchangeCallback(
129 | options,
130 | env.CLOUDFLARE_CLIENT_ID,
131 | env.CLOUDFLARE_CLIENT_SECRET
132 | ),
133 | // Cloudflare access token TTL
134 | accessTokenTTL: 3600,
135 | clientRegistrationEndpoint: '/register',
136 | }).fetch(req, env, ctx)
137 | },
138 | }
139 |
```
--------------------------------------------------------------------------------
/apps/workers-bindings/evals/kv_namespaces.eval.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { expect } from 'vitest'
2 | import { describeEval } from 'vitest-evals'
3 |
4 | import { runTask } from '@repo/eval-tools/src/runTask'
5 | import { checkFactuality } from '@repo/eval-tools/src/scorers'
6 | import { eachModel } from '@repo/eval-tools/src/test-models'
7 | import { KV_NAMESPACE_TOOLS } from '@repo/mcp-common/src/tools/kv_namespace.tools'
8 |
9 | import { initializeClient } from './utils' // Assuming utils.ts will exist here
10 |
11 | eachModel('$modelName', ({ model }) => {
12 | describeEval('Create Cloudflare KV Namespace', {
13 | data: async () => [
14 | {
15 | input: 'Create a new Cloudflare KV Namespace called "my-test-namespace".',
16 | expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_create} tool should be called to create a new kv namespace.`,
17 | },
18 | ],
19 | task: async (input: string) => {
20 | const client = await initializeClient(/* Pass necessary mocks/config */)
21 | const { promptOutput, toolCalls } = await runTask(client, model, input)
22 | const toolCall = toolCalls.find(
23 | (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_create
24 | )
25 | expect(toolCall, 'Tool kv_namespace_create was not called').toBeDefined()
26 |
27 | return promptOutput
28 | },
29 | scorers: [checkFactuality],
30 | threshold: 1,
31 | timeout: 60000, // 60 seconds
32 | })
33 | describeEval('List Cloudflare KV Namespaces', {
34 | data: async () => [
35 | {
36 | input: 'List all my Cloudflare KV Namespaces.',
37 | 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.`,
38 | },
39 | ],
40 | task: async (input: string) => {
41 | const client = await initializeClient(/* Pass necessary mocks/config */)
42 | const { promptOutput, toolCalls } = await runTask(client, model, input)
43 | const toolCall = toolCalls.find(
44 | (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespaces_list
45 | )
46 | expect(toolCall, 'Tool kv_namespaces_list was not called').toBeDefined()
47 |
48 | return promptOutput
49 | },
50 | scorers: [checkFactuality],
51 | threshold: 1,
52 | timeout: 60000, // 60 seconds
53 | })
54 | describeEval('Rename Cloudflare KV Namespace', {
55 | data: async () => [
56 | {
57 | input: 'Rename my Cloudflare KV Namespace with ID 1234 to "my-new-test-namespace".',
58 | expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_update} tool should be called to rename the kv namespace.`,
59 | },
60 | ],
61 | task: async (input: string) => {
62 | const client = await initializeClient(/* Pass necessary mocks/config */)
63 | const { promptOutput, toolCalls } = await runTask(client, model, input)
64 | const toolCall = toolCalls.find(
65 | (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_update
66 | )
67 | expect(toolCall, 'Tool kv_namespace_update was not called').toBeDefined()
68 |
69 | return promptOutput
70 | },
71 | scorers: [checkFactuality],
72 | threshold: 1,
73 | timeout: 60000, // 60 seconds
74 | })
75 | describeEval('Get Cloudflare KV Namespace Details', {
76 | data: async () => [
77 | {
78 | input: 'Get details of my Cloudflare KV Namespace with ID 1234.',
79 | expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_get} tool should be called to retrieve the details of the kv namespace.`,
80 | },
81 | ],
82 | task: async (input: string) => {
83 | const client = await initializeClient(/* Pass necessary mocks/config */)
84 | const { promptOutput, toolCalls } = await runTask(client, model, input)
85 | const toolCall = toolCalls.find(
86 | (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_get
87 | )
88 | expect(toolCall, 'Tool kv_namespace_get was not called').toBeDefined()
89 |
90 | return promptOutput
91 | },
92 | scorers: [checkFactuality],
93 | threshold: 1,
94 | timeout: 60000, // 60 seconds
95 | })
96 | describeEval('Delete Cloudflare KV Namespace', {
97 | data: async () => [
98 | {
99 | input: 'Delete the kv namespace with ID 1234.',
100 | expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_delete} tool should be called to delete the kv namespace.`,
101 | },
102 | ],
103 | task: async (input: string) => {
104 | const client = await initializeClient(/* Pass necessary mocks/config */)
105 | const { promptOutput, toolCalls } = await runTask(client, model, input)
106 | const toolCall = toolCalls.find(
107 | (call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_delete
108 | )
109 | expect(toolCall, 'Tool kv_namespace_delete was not called').toBeDefined()
110 |
111 | return promptOutput
112 | },
113 | scorers: [checkFactuality],
114 | threshold: 1,
115 | timeout: 60000, // 60 seconds
116 | })
117 | })
118 |
```
--------------------------------------------------------------------------------
/apps/radar/src/radar.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 | import { MetricsTracker } from '@repo/mcp-observability'
16 |
17 | import { BASE_INSTRUCTIONS } from './radar.context'
18 | import { registerRadarTools } from './tools/radar.tools'
19 | import { registerUrlScannerTools } from './tools/url-scanner.tools'
20 |
21 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
22 | import type { Env } from './radar.context'
23 |
24 | const env = getEnv<Env>()
25 |
26 | export { UserDetails }
27 |
28 | const metrics = new MetricsTracker(env.MCP_METRICS, {
29 | name: env.MCP_SERVER_NAME,
30 | version: env.MCP_SERVER_VERSION,
31 | })
32 |
33 | // Context from the auth process, encrypted & stored in the auth token
34 | // and provided to the DurableMCP as this.props
35 | type Props = AuthProps
36 | type State = { activeAccountId: string | null }
37 |
38 | export class RadarMCP extends McpAgent<Env, State, Props> {
39 | _server: CloudflareMCPServer | undefined
40 | set server(server: CloudflareMCPServer) {
41 | this._server = server
42 | }
43 | get server(): CloudflareMCPServer {
44 | if (!this._server) {
45 | throw new Error('Tried to access server before it was initialized')
46 | }
47 |
48 | return this._server
49 | }
50 |
51 | constructor(ctx: DurableObjectState, env: Env) {
52 | super(ctx, env)
53 | }
54 |
55 | async init() {
56 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
57 | const props = getProps(this)
58 | const userId = props.type === 'user_token' ? props.user.id : undefined
59 |
60 | this.server = new CloudflareMCPServer({
61 | userId,
62 | wae: this.env.MCP_METRICS,
63 | serverInfo: {
64 | name: this.env.MCP_SERVER_NAME,
65 | version: this.env.MCP_SERVER_VERSION,
66 | },
67 | options: { instructions: BASE_INSTRUCTIONS },
68 | })
69 |
70 | registerAccountTools(this)
71 | registerRadarTools(this)
72 | registerUrlScannerTools(this)
73 | }
74 |
75 | async getActiveAccountId() {
76 | try {
77 | const props = getProps(this)
78 | // account tokens are scoped to one account
79 | if (props.type === 'account_token') {
80 | return props.account.id
81 | }
82 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
83 | // we do this so we can persist activeAccountId across sessions
84 | const userDetails = getUserDetails(env, props.user.id)
85 | return await userDetails.getActiveAccountId()
86 | } catch (e) {
87 | this.server.recordError(e)
88 | return null
89 | }
90 | }
91 |
92 | async setActiveAccountId(accountId: string) {
93 | try {
94 | const props = getProps(this)
95 | // account tokens are scoped to one account
96 | if (props.type === 'account_token') {
97 | return
98 | }
99 | const userDetails = getUserDetails(env, props.user.id)
100 | await userDetails.setActiveAccountId(accountId)
101 | } catch (e) {
102 | this.server.recordError(e)
103 | }
104 | }
105 | }
106 |
107 | const RadarScopes = {
108 | ...RequiredScopes,
109 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
110 | 'radar:read': 'Grants access to read Cloudflare Radar data.',
111 | 'url_scanner:write': 'Grants write level access to URL Scanner',
112 | } as const
113 |
114 | export default {
115 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
116 | if (await isApiTokenRequest(req, env)) {
117 | return await handleApiTokenMode(RadarMCP, req, env, ctx)
118 | }
119 |
120 | return new OAuthProvider({
121 | apiHandlers: {
122 | '/mcp': RadarMCP.serve('/mcp'),
123 | '/sse': RadarMCP.serveSSE('/sse'),
124 | },
125 | // @ts-ignore
126 | defaultHandler: createAuthHandlers({ scopes: RadarScopes, metrics }),
127 | authorizeEndpoint: '/oauth/authorize',
128 | tokenEndpoint: '/token',
129 | tokenExchangeCallback: (options) =>
130 | handleTokenExchangeCallback(
131 | options,
132 | env.CLOUDFLARE_CLIENT_ID,
133 | env.CLOUDFLARE_CLIENT_SECRET
134 | ),
135 | // Cloudflare access token TTL
136 | accessTokenTTL: 3600,
137 | clientRegistrationEndpoint: '/register',
138 | }).fetch(req, env, ctx)
139 | },
140 | }
141 |
```
--------------------------------------------------------------------------------
/packages/mcp-observability/src/analytics-engine.ts:
--------------------------------------------------------------------------------
```typescript
1 | export type MetricsBindings = {
2 | MCP_METRICS: AnalyticsEngineDataset
3 | }
4 |
5 | /**
6 | * Generic metrics event utilities
7 | * @description Wrapper for RA binding
8 | */
9 | export class MetricsTracker {
10 | constructor(
11 | private wae: AnalyticsEngineDataset,
12 | private mcpServerInfo: {
13 | name: string
14 | version: string
15 | }
16 | ) {}
17 |
18 | logEvent(event: MetricsEvent): void {
19 | try {
20 | event.serverInfo = this.mcpServerInfo
21 | let dataPoint = event.toDataPoint()
22 | this.wae.writeDataPoint(dataPoint)
23 | } catch (e) {
24 | console.error(`Failed to log metrics event, ${e}`)
25 | }
26 | }
27 | }
28 |
29 | /**
30 | * MetricsEvent
31 | *
32 | * Each event type is stored with a different indexId and has an associated class which
33 | * maps a more ergonomic event object to a ReadyAnalyticsEvent
34 | */
35 | export abstract class MetricsEvent {
36 | public _serverInfo: { name: string; version: string } | undefined
37 | set serverInfo(serverInfo: { name: string; version: string }) {
38 | this._serverInfo = serverInfo
39 | }
40 |
41 | get serverInfo(): { name: string; version: string } {
42 | if (!this._serverInfo) {
43 | throw new Error('Server info not set')
44 | }
45 | return this._serverInfo
46 | }
47 |
48 | /**
49 | * Output a valid AnalyticsEngineDataPoint. Use `mapBlobs` and `mapDoubles` to write well defined
50 | * analytics engine datapoints. The first and second blob entries are reserved for the MCP server name and
51 | * MCP server version.
52 | */
53 | abstract toDataPoint(): AnalyticsEngineDataPoint
54 |
55 | mapBlobs(blobs: Blobs): Array<string | null> {
56 | if (blobs.blob1 || blobs.blob2) {
57 | throw new MetricsError(
58 | 'Failed to map blobs, blob1 and blob2 are reserved for MCP server info'
59 | )
60 | }
61 | // add placeholder blobs, filled in by the MetricsTracker later
62 | blobs.blob1 = this.serverInfo.name
63 | blobs.blob2 = this.serverInfo.version
64 | const blobsArray = new Array(Object.keys(blobs).length)
65 | for (const [key, value] of Object.entries(blobs)) {
66 | const match = key.match(/^blob(\d+)$/)
67 | if (match === null || match.length < 2) {
68 | // we should never hit this because of the typedefinitions above,
69 | // but this error is for safety
70 | throw new MetricsError('Failed to map blobs, invalid key')
71 | }
72 | const index = parseInt(match[1], 10)
73 | if (isNaN(index)) {
74 | // we should never hit this because of the typedefinitions above,
75 | // but this esrror is for safety
76 | throw new MetricsError('Failed to map blobs, invalid index')
77 | }
78 | if (index - 1 >= blobsArray.length) {
79 | throw new MetricsError('Failed to map blobs, missing blob')
80 | }
81 | blobsArray[index - 1] = value
82 | }
83 | return blobsArray
84 | }
85 |
86 | mapDoubles(doubles: Doubles): number[] {
87 | const doublesArray = new Array(Object.keys(doubles).length)
88 | for (const [key, value] of Object.entries(doubles)) {
89 | const match = key.match(/^double(\d+)$/)
90 | if (match === null || match.length < 2) {
91 | // we should never hit this because of the typedefinitions above,
92 | // but this error is for safety
93 | throw new MetricsError(': Failed to map doubles, invalid key')
94 | }
95 | const index = parseInt(match[1], 10)
96 | if (isNaN(index)) {
97 | // we should never hit this because of the typedefinitions above,
98 | // but this error is for safety
99 | throw new MetricsError('Failed to map doubles, invalid index')
100 | }
101 | if (index - 1 >= doublesArray.length) {
102 | throw new MetricsError('Failed to map doubles, missing blob')
103 | }
104 | doublesArray[index - 1] = value
105 | }
106 | return doublesArray
107 | }
108 | }
109 |
110 | export enum MetricsEventIndexIds {
111 | AUTH_USER = 'auth_user',
112 | SESSION_START = 'session_start',
113 | TOOL_CALL = 'tool_call',
114 | CONTAINER_MANAGER = 'container_manager',
115 | }
116 |
117 | /**
118 | * Utility functions to map named blob/double objects to an array
119 | * We do this so we don't have to annotate `blob1`, `blob2`, etc in comments.
120 | *
121 | * I prefer this to just writing it in an array because it'll be easier to reference
122 | * later when we are writing ready analytics queries.
123 | *
124 | * IMO named tuples and raw arrays aren't as ergonomic to work with, but they require less of this code below
125 | */
126 | type Range1To20 =
127 | | 1
128 | | 2
129 | | 3
130 | | 4
131 | | 5
132 | | 6
133 | | 7
134 | | 8
135 | | 9
136 | | 10
137 | | 11
138 | | 12
139 | | 13
140 | | 14
141 | | 15
142 | | 16
143 | | 17
144 | | 18
145 | | 19
146 | | 20
147 |
148 | // blob1 and blob2 are reserved for server name and version
149 | type Blobs = {
150 | [key in `blob${Range1To20}`]?: string | null
151 | }
152 |
153 | type Doubles = {
154 | [key in `double${Range1To20}`]?: number
155 | }
156 |
157 | export class MetricsError extends Error {
158 | constructor(message: string) {
159 | super(message)
160 | this.name = 'MetricsError'
161 | }
162 | }
163 |
```
--------------------------------------------------------------------------------
/apps/dns-analytics/src/dns-analytics.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
15 | import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools'
16 |
17 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
18 | import { registerAnalyticTools } from './tools/dex-analytics.tools'
19 |
20 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
21 | import type { Env } from './dns-analytics.context'
22 |
23 | export { UserDetails }
24 |
25 | const env = getEnv<Env>()
26 |
27 | const metrics = new MetricsTracker(env.MCP_METRICS, {
28 | name: env.MCP_SERVER_NAME,
29 | version: env.MCP_SERVER_VERSION,
30 | })
31 |
32 | // Context from the auth process, encrypted & stored in the auth token
33 | // and provided to the DurableMCP as this.props
34 | export type Props = AuthProps
35 |
36 | export type State = { activeAccountId: string | null }
37 |
38 | export class DNSAnalyticsMCP extends McpAgent<Env, State, Props> {
39 | _server: CloudflareMCPServer | undefined
40 | set server(server: CloudflareMCPServer) {
41 | this._server = server
42 | }
43 |
44 | get server(): CloudflareMCPServer {
45 | if (!this._server) {
46 | throw new Error('Tried to access server before it was initialized')
47 | }
48 |
49 | return this._server
50 | }
51 |
52 | constructor(ctx: DurableObjectState, env: Env) {
53 | super(ctx, env)
54 | }
55 |
56 | async init() {
57 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
58 | const props = getProps(this)
59 | const userId = props.type === 'user_token' ? props.user.id : undefined
60 |
61 | this.server = new CloudflareMCPServer({
62 | userId,
63 | wae: this.env.MCP_METRICS,
64 | serverInfo: {
65 | name: this.env.MCP_SERVER_NAME,
66 | version: this.env.MCP_SERVER_VERSION,
67 | },
68 | })
69 |
70 | registerAccountTools(this)
71 |
72 | // Register Cloudflare DNS Analytics tools
73 | registerAnalyticTools(this)
74 |
75 | registerZoneTools(this)
76 | }
77 |
78 | async getActiveAccountId() {
79 | try {
80 | const props = getProps(this)
81 | // account tokens are scoped to one account
82 | if (props.type === 'account_token') {
83 | return props.account.id
84 | }
85 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
86 | // we do this so we can persist activeAccountId across sessions
87 | const userDetails = getUserDetails(env, props.user.id)
88 | return await userDetails.getActiveAccountId()
89 | } catch (e) {
90 | this.server.recordError(e)
91 | return null
92 | }
93 | }
94 |
95 | async setActiveAccountId(accountId: string) {
96 | try {
97 | const props = getProps(this)
98 | // account tokens are scoped to one account
99 | if (props.type === 'account_token') {
100 | return
101 | }
102 | const userDetails = getUserDetails(env, props.user.id)
103 | await userDetails.setActiveAccountId(accountId)
104 | } catch (e) {
105 | this.server.recordError(e)
106 | }
107 | }
108 | }
109 |
110 | const AnalyticsScopes = {
111 | ...RequiredScopes,
112 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
113 | 'zone:read': 'See your zones',
114 | 'dns_settings:read': 'See your DNS settings',
115 | 'dns_analytics:read': 'See your DNS analytics',
116 | } as const
117 |
118 | export default {
119 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
120 | if (await isApiTokenRequest(req, env)) {
121 | return await handleApiTokenMode(DNSAnalyticsMCP, req, env, ctx)
122 | }
123 |
124 | return new OAuthProvider({
125 | apiHandlers: {
126 | '/mcp': DNSAnalyticsMCP.serve('/mcp'),
127 | '/sse': DNSAnalyticsMCP.serveSSE('/sse'),
128 | },
129 | // @ts-ignore
130 | defaultHandler: createAuthHandlers({ scopes: AnalyticsScopes, metrics }),
131 | authorizeEndpoint: '/oauth/authorize',
132 | tokenEndpoint: '/token',
133 | tokenExchangeCallback: (options) =>
134 | handleTokenExchangeCallback(
135 | options,
136 | env.CLOUDFLARE_CLIENT_ID,
137 | env.CLOUDFLARE_CLIENT_SECRET
138 | ),
139 | // Cloudflare access token TTL
140 | accessTokenTTL: 3600,
141 | clientRegistrationEndpoint: '/register',
142 | }).fetch(req, env, ctx)
143 | },
144 | }
145 |
```
--------------------------------------------------------------------------------
/apps/sandbox-container/container/sandbox.container.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { exec } from 'node:child_process'
2 | import * as fs from 'node:fs/promises'
3 | import path from 'node:path'
4 | import { serve } from '@hono/node-server'
5 | import { zValidator } from '@hono/zod-validator'
6 | import { Hono } from 'hono'
7 | import { streamText } from 'hono/streaming'
8 | import mime from 'mime'
9 |
10 | import { ExecParams, FileWrite } from '../shared/schema.ts'
11 | import {
12 | DIRECTORY_CONTENT_TYPE,
13 | get_file_name_from_path,
14 | get_mime_type,
15 | list_files_in_directory,
16 | } from './fileUtils.ts'
17 |
18 | import type { FileList } from '../shared/schema.ts'
19 |
20 | process.chdir('workdir')
21 |
22 | const app = new Hono()
23 |
24 | app.get('/ping', (c) => c.text('pong!'))
25 |
26 | /**
27 | * GET /files/ls
28 | *
29 | * Gets all files in a directory
30 | */
31 | app.get('/files/ls', async (c) => {
32 | const directoriesToRead = ['.']
33 | const files: FileList = { resources: [] }
34 |
35 | while (directoriesToRead.length > 0) {
36 | const curr = directoriesToRead.pop()
37 | if (!curr) {
38 | throw new Error('Popped empty stack, error while listing directories')
39 | }
40 | const fullPath = path.join(process.cwd(), curr)
41 | const dir = await fs.readdir(fullPath, { withFileTypes: true })
42 | for (const dirent of dir) {
43 | const relPath = path.relative(process.cwd(), `${fullPath}/${dirent.name}`)
44 | if (dirent.isDirectory()) {
45 | directoriesToRead.push(dirent.name)
46 | files.resources.push({
47 | uri: `file:///${relPath}`,
48 | name: dirent.name,
49 | mimeType: 'inode/directory',
50 | })
51 | } else {
52 | const mimeType = mime.getType(dirent.name)
53 | files.resources.push({
54 | uri: `file:///${relPath}`,
55 | name: dirent.name,
56 | mimeType: mimeType ?? undefined,
57 | })
58 | }
59 | }
60 | }
61 |
62 | return c.json(files)
63 | })
64 |
65 | /**
66 | * GET /files/contents/{filepath}
67 | *
68 | * Get the contents of a file or directory
69 | */
70 | app.get('/files/contents/*', async (c) => {
71 | const reqPath = await get_file_name_from_path(c.req.path)
72 | try {
73 | const mimeType = await get_mime_type(reqPath)
74 | const headers = mimeType ? { 'Content-Type': mimeType } : undefined
75 | const contents = await fs.readFile(path.join(process.cwd(), reqPath))
76 | return c.newResponse(contents, 200, headers)
77 | } catch (e: any) {
78 | if (e.code) {
79 | if (e.code === 'EISDIR') {
80 | const files = await list_files_in_directory(reqPath)
81 | return c.newResponse(files.join('\n'), 200, {
82 | 'Content-Type': DIRECTORY_CONTENT_TYPE,
83 | })
84 | }
85 | if (e.code === 'ENOENT') {
86 | return c.notFound()
87 | }
88 | }
89 |
90 | throw e
91 | }
92 | })
93 |
94 | /**
95 | * POST /files/contents
96 | *
97 | * Create or update file contents
98 | */
99 | app.post('/files/contents', zValidator('json', FileWrite), async (c) => {
100 | const file = c.req.valid('json')
101 | const reqPath = await get_file_name_from_path(file.path)
102 |
103 | try {
104 | await fs.writeFile(reqPath, file.text)
105 | return c.newResponse(null, 200)
106 | } catch (e) {
107 | return c.newResponse(`Error: ${e}`, 400)
108 | }
109 | })
110 |
111 | /**
112 | * DELETE /files/contents/{filepath}
113 | *
114 | * Delete a file or directory
115 | */
116 | app.delete('/files/contents/*', async (c) => {
117 | const reqPath = await get_file_name_from_path(c.req.path)
118 |
119 | try {
120 | await fs.rm(path.join(process.cwd(), reqPath), { recursive: true })
121 | return c.newResponse('ok', 200)
122 | } catch (e: any) {
123 | if (e.code) {
124 | if (e.code === 'ENOENT') {
125 | return c.notFound()
126 | }
127 | }
128 |
129 | throw e
130 | }
131 | })
132 |
133 | /**
134 | * POST /exec
135 | *
136 | * Execute a command in a shell
137 | */
138 | app.post('/exec', zValidator('json', ExecParams), (c) => {
139 | const execParams = c.req.valid('json')
140 | const proc = exec(execParams.args)
141 | return streamText(c, async (stream) => {
142 | return new Promise((resolve, reject) => {
143 | if (proc.stdout) {
144 | // Stream data from stdout
145 | proc.stdout.on('data', async (data) => {
146 | await stream.write(data.toString())
147 | })
148 | } else {
149 | void stream.write('WARNING: no stdout stream for process')
150 | }
151 |
152 | if (execParams.streamStderr) {
153 | if (proc.stderr) {
154 | proc.stderr.on('data', async (data) => {
155 | await stream.write(data.toString())
156 | })
157 | } else {
158 | void stream.write('WARNING: no stderr stream for process')
159 | }
160 | }
161 |
162 | // Handle process exit
163 | proc.on('exit', async (code) => {
164 | await stream.write(`Process exited with code: ${code}`)
165 | if (code === 0) {
166 | await stream.close()
167 | resolve()
168 | } else {
169 | console.error(`Process exited with code ${code}`)
170 | reject(new Error(`Process failed with code ${code}`))
171 | }
172 | })
173 |
174 | proc.on('error', (err) => {
175 | console.error('Error with process: ', err)
176 | reject(err)
177 | })
178 | })
179 | })
180 | })
181 |
182 | serve({
183 | fetch: app.fetch,
184 | port: 8080,
185 | })
186 |
```
--------------------------------------------------------------------------------
/apps/graphql/src/graphql.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
13 | import { initSentryWithUser } from '@repo/mcp-common/src/sentry'
14 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
15 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
16 | import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools'
17 | import { MetricsTracker } from '@repo/mcp-observability'
18 |
19 | import { registerGraphQLTools } from './tools/graphql.tools'
20 |
21 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
22 | import type { Env } from './graphql.context'
23 |
24 | export { UserDetails }
25 |
26 | const env = getEnv<Env>()
27 |
28 | const metrics = new MetricsTracker(env.MCP_METRICS, {
29 | name: env.MCP_SERVER_NAME,
30 | version: env.MCP_SERVER_VERSION,
31 | })
32 |
33 | // Context from the auth process, encrypted & stored in the auth token
34 | // and provided to the DurableMCP as this.props
35 | type Props = AuthProps
36 | type State = { activeAccountId: string | null }
37 |
38 | export class GraphQLMCP extends McpAgent<Env, State, Props> {
39 | _server: CloudflareMCPServer | undefined
40 | set server(server: CloudflareMCPServer) {
41 | this._server = server
42 | }
43 |
44 | get server(): CloudflareMCPServer {
45 | if (!this._server) {
46 | throw new Error('Tried to access server before it was initialized')
47 | }
48 |
49 | return this._server
50 | }
51 |
52 | constructor(ctx: DurableObjectState, env: Env) {
53 | super(ctx, env)
54 | }
55 |
56 | async init() {
57 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
58 | const props = getProps(this)
59 | const userId = props.type === 'user_token' ? props.user.id : undefined
60 | const sentry =
61 | props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined
62 |
63 | this.server = new CloudflareMCPServer({
64 | userId,
65 | wae: this.env.MCP_METRICS,
66 | serverInfo: {
67 | name: this.env.MCP_SERVER_NAME,
68 | version: this.env.MCP_SERVER_VERSION,
69 | },
70 | sentry,
71 | })
72 |
73 | // Register account tools
74 | registerAccountTools(this)
75 |
76 | // Register zone tools
77 | registerZoneTools(this)
78 |
79 | // Register GraphQL tools
80 | registerGraphQLTools(this)
81 | }
82 |
83 | async getActiveAccountId() {
84 | try {
85 | const props = getProps(this)
86 | // account tokens are scoped to one account
87 | if (props.type === 'account_token') {
88 | return props.account.id
89 | }
90 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
91 | // we do this so we can persist activeAccountId across sessions
92 | const userDetails = getUserDetails(env, props.user.id)
93 | return await userDetails.getActiveAccountId()
94 | } catch (e) {
95 | this.server.recordError(e)
96 | return null
97 | }
98 | }
99 |
100 | async setActiveAccountId(accountId: string) {
101 | try {
102 | const props = getProps(this)
103 | // account tokens are scoped to one account
104 | if (props.type === 'account_token') {
105 | return
106 | }
107 | const userDetails = getUserDetails(env, props.user.id)
108 | await userDetails.setActiveAccountId(accountId)
109 | } catch (e) {
110 | this.server.recordError(e)
111 | }
112 | }
113 | }
114 |
115 | const GraphQLScopes = {
116 | ...RequiredScopes,
117 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
118 | 'zone:read': 'See zone data such as settings, analytics, and DNS records.',
119 | } as const
120 |
121 | export default {
122 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
123 | if (await isApiTokenRequest(req, env)) {
124 | return await handleApiTokenMode(GraphQLMCP, req, env, ctx)
125 | }
126 |
127 | return new OAuthProvider({
128 | apiHandlers: {
129 | '/mcp': GraphQLMCP.serve('/mcp'),
130 | '/sse': GraphQLMCP.serveSSE('/sse'),
131 | },
132 | // @ts-ignore
133 | defaultHandler: createAuthHandlers({ scopes: GraphQLScopes, metrics }),
134 | authorizeEndpoint: '/oauth/authorize',
135 | tokenEndpoint: '/token',
136 | tokenExchangeCallback: (options) =>
137 | handleTokenExchangeCallback(
138 | options,
139 | env.CLOUDFLARE_CLIENT_ID,
140 | env.CLOUDFLARE_CLIENT_SECRET
141 | ),
142 | // Cloudflare access token TTL
143 | accessTokenTTL: 3600,
144 | clientRegistrationEndpoint: '/register',
145 | }).fetch(req, env, ctx)
146 | },
147 | }
148 |
```
--------------------------------------------------------------------------------
/apps/autorag/src/tools/autorag.tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { V4PagePaginationArray } from 'cloudflare/src/pagination.js'
2 | import { z } from 'zod'
3 |
4 | import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
5 | import { getProps } from '@repo/mcp-common/src/get-props'
6 |
7 | import { pageParam, perPageParam } from '../types'
8 |
9 | import type { AutoRAGMCP } from '../autorag.app'
10 |
11 | export function registerAutoRAGTools(agent: AutoRAGMCP) {
12 | agent.server.tool(
13 | 'list_rags',
14 | 'List AutoRAGs (vector stores)',
15 | {
16 | page: pageParam,
17 | per_page: perPageParam,
18 | },
19 | async (params) => {
20 | const accountId = await agent.getActiveAccountId()
21 | if (!accountId) {
22 | return {
23 | content: [
24 | {
25 | type: 'text',
26 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
27 | },
28 | ],
29 | }
30 | }
31 | try {
32 | const props = getProps(agent)
33 | const client = getCloudflareClient(props.accessToken)
34 | const r = (await client.getAPIList(
35 | `/accounts/${accountId}/autorag/rags`,
36 | // @ts-ignore
37 | V4PagePaginationArray,
38 | { query: { page: params.page, per_page: params.per_page } }
39 | )) as unknown as {
40 | result: Array<{ id: string; source: string; paused: boolean }>
41 | result_info: { total_count: number }
42 | }
43 |
44 | return {
45 | content: [
46 | {
47 | type: 'text',
48 | text: JSON.stringify({
49 | autorags: r.result.map((obj) => {
50 | return {
51 | id: obj.id,
52 | source: obj.source,
53 | paused: obj.paused,
54 | }
55 | }),
56 | total_count: r.result_info.total_count,
57 | }),
58 | },
59 | ],
60 | }
61 | } catch (error) {
62 | return {
63 | content: [
64 | {
65 | type: 'text',
66 | text: `Error listing rags: ${error instanceof Error && error.message}`,
67 | },
68 | ],
69 | }
70 | }
71 | }
72 | )
73 |
74 | agent.server.tool(
75 | 'search',
76 | 'Search Documents using AutoRAG (vector store)',
77 | {
78 | rag_id: z.string().describe('ID of the AutoRAG to search'),
79 | query: z.string().describe('Query to search for. Can be a URL, a title, or a snippet.'),
80 | },
81 | async (params) => {
82 | try {
83 | const accountId = await agent.getActiveAccountId()
84 | if (!accountId) {
85 | return {
86 | content: [
87 | {
88 | type: 'text',
89 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
90 | },
91 | ],
92 | }
93 | }
94 |
95 | const props = getProps(agent)
96 | const client = getCloudflareClient(props.accessToken)
97 | const r = (await client.post(
98 | `/accounts/${accountId}/autorag/rags/${params.rag_id}/search`,
99 | {
100 | body: {
101 | query: params.query,
102 | max_num_results: 5,
103 | },
104 | }
105 | )) as { result: { data: Array<{ filename: string; content: Array<{ text: string }> }> } }
106 |
107 | const chunks = r.result.data
108 | .map((item) => {
109 | const data = item.content
110 | .map((content) => {
111 | return content.text
112 | })
113 | .join('\n\n')
114 |
115 | return `<file name="${item.filename}">${data}</file>`
116 | })
117 | .join('\n\n')
118 |
119 | return {
120 | content: [
121 | {
122 | type: 'text',
123 | text: chunks,
124 | },
125 | ],
126 | }
127 | } catch (error) {
128 | return {
129 | content: [
130 | {
131 | type: 'text',
132 | text: `Error searching rag: ${error instanceof Error && error.message}`,
133 | },
134 | ],
135 | }
136 | }
137 | }
138 | )
139 |
140 | agent.server.tool(
141 | 'ai_search',
142 | 'AI Search Documents using AutoRAG (vector store)',
143 | {
144 | rag_id: z.string().describe('ID of the AutoRAG to search'),
145 | query: z.string().describe('Query to search for. Can be a URL, a title, or a snippet.'),
146 | },
147 | async (params) => {
148 | try {
149 | const accountId = await agent.getActiveAccountId()
150 | if (!accountId) {
151 | return {
152 | content: [
153 | {
154 | type: 'text',
155 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
156 | },
157 | ],
158 | }
159 | }
160 |
161 | const props = getProps(agent)
162 | const client = getCloudflareClient(props.accessToken)
163 | const r = (await client.post(
164 | `/accounts/${accountId}/autorag/rags/${params.rag_id}/ai-search`,
165 | {
166 | body: {
167 | query: params.query,
168 | max_num_results: 10, // Limit can be bigger here, since llm is only getting the end response and not individual chunks
169 | },
170 | }
171 | )) as { result: { response: string } }
172 |
173 | return {
174 | content: [
175 | {
176 | type: 'text',
177 | text: r.result.response,
178 | },
179 | ],
180 | }
181 | } catch (error) {
182 | return {
183 | content: [
184 | {
185 | type: 'text',
186 | text: `Error searching rag: ${error instanceof Error && error.message}`,
187 | },
188 | ],
189 | }
190 | }
191 | }
192 | )
193 | }
194 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/cloudflare-auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import { McpError } from './mcp-error'
4 |
5 | import type { AuthRequest } from '@cloudflare/workers-oauth-provider'
6 |
7 | // Constants
8 | const PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
9 | const RECOMMENDED_CODE_VERIFIER_LENGTH = 96
10 | function base64urlEncode(value: string): string {
11 | let base64 = btoa(value)
12 | base64 = base64.replace(/\+/g, '-')
13 | base64 = base64.replace(/\//g, '_')
14 | base64 = base64.replace(/=/g, '')
15 | return base64
16 | }
17 |
18 | interface PKCECodes {
19 | codeChallenge: string
20 | codeVerifier: string
21 | }
22 | export async function generatePKCECodes(): Promise<PKCECodes> {
23 | const output = new Uint32Array(RECOMMENDED_CODE_VERIFIER_LENGTH)
24 | crypto.getRandomValues(output)
25 | const codeVerifier = base64urlEncode(
26 | Array.from(output)
27 | .map((num: number) => PKCE_CHARSET[num % PKCE_CHARSET.length])
28 | .join('')
29 | )
30 | const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
31 | const hash = new Uint8Array(buffer)
32 | let binary = ''
33 | const hashLength = hash.byteLength
34 | for (let i = 0; i < hashLength; i++) {
35 | binary += String.fromCharCode(hash[i])
36 | }
37 | const codeChallenge = base64urlEncode(binary) //btoa(binary);
38 | return { codeChallenge, codeVerifier }
39 | }
40 |
41 | function generateAuthUrl({
42 | client_id,
43 | redirect_uri,
44 | state,
45 | code_challenge,
46 | scopes,
47 | }: {
48 | client_id: string
49 | redirect_uri: string
50 | code_challenge: string
51 | state: string
52 | scopes: Record<string, string>
53 | }) {
54 | const params = new URLSearchParams({
55 | response_type: 'code',
56 | client_id,
57 | redirect_uri,
58 | state,
59 | code_challenge,
60 | code_challenge_method: 'S256',
61 | scope: Object.keys(scopes).join(' '),
62 | })
63 |
64 | const upstream = new URL(`https://dash.cloudflare.com/oauth2/auth?${params.toString()}`)
65 | return upstream.href
66 | }
67 |
68 | /**
69 | * Constructs an authorization URL for Cloudflare.
70 | *
71 | * @param {Object} options
72 | * @param {string} options.client_id - The client ID of the application.
73 | * @param {string} options.redirect_uri - The redirect URI of the application.
74 | * @param {string} [options.state] - The state parameter.
75 | *
76 | * @returns {string} The authorization URL.
77 | */
78 | export async function getAuthorizationURL({
79 | client_id,
80 | redirect_uri,
81 | state,
82 | scopes,
83 | codeChallenge,
84 | }: {
85 | client_id: string
86 | redirect_uri: string
87 | state: AuthRequest
88 | scopes: Record<string, string>
89 | codeChallenge: string
90 | }): Promise<{ authUrl: string }> {
91 | return {
92 | authUrl: generateAuthUrl({
93 | client_id,
94 | redirect_uri,
95 | state: btoa(JSON.stringify(state)),
96 | code_challenge: codeChallenge,
97 | scopes,
98 | }),
99 | }
100 | }
101 |
102 | type AuthorizationToken = z.infer<typeof AuthorizationToken>
103 | const AuthorizationToken = z.object({
104 | access_token: z.string(),
105 | expires_in: z.number(),
106 | refresh_token: z.string(),
107 | scope: z.string(),
108 | token_type: z.string(),
109 | })
110 | /**
111 | * Fetches an authorization token from Cloudflare.
112 | *
113 | * @param {Object} options
114 | * @param {string} options.client_id - The client ID of the application.
115 | * @param {string} options.client_secret - The client secret of the application.
116 | * @param {string} options.code - The authorization code.
117 | * @param {string} options.redirect_uri - The redirect URI of the application.
118 | *
119 | * @returns {Promise<[string, null] | [null, Response]>} A promise that resolves to an array containing the access token or an error response.
120 | */
121 | export async function getAuthToken({
122 | client_id,
123 | client_secret,
124 | redirect_uri,
125 | code_verifier,
126 | code,
127 | }: {
128 | client_id: string
129 | client_secret: string
130 | redirect_uri: string
131 | code_verifier: string
132 | code: string
133 | }): Promise<AuthorizationToken> {
134 | if (!code) {
135 | throw new McpError('Missing code', 400)
136 | }
137 |
138 | const params = new URLSearchParams({
139 | grant_type: 'authorization_code',
140 | client_id,
141 | redirect_uri,
142 | code,
143 | code_verifier,
144 | }).toString()
145 | const resp = await fetch('https://dash.cloudflare.com/oauth2/token', {
146 | method: 'POST',
147 | headers: {
148 | Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`,
149 | 'Content-Type': 'application/x-www-form-urlencoded',
150 | },
151 | body: params,
152 | })
153 |
154 | if (!resp.ok) {
155 | console.log(await resp.text())
156 | throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true })
157 | }
158 |
159 | return AuthorizationToken.parse(await resp.json())
160 | }
161 |
162 | export async function refreshAuthToken({
163 | client_id,
164 | client_secret,
165 | refresh_token,
166 | }: {
167 | client_id: string
168 | client_secret: string
169 | refresh_token: string
170 | }): Promise<AuthorizationToken> {
171 | const params = new URLSearchParams({
172 | grant_type: 'refresh_token',
173 | client_id,
174 | refresh_token,
175 | })
176 |
177 | const resp = await fetch('https://dash.cloudflare.com/oauth2/token', {
178 | method: 'POST',
179 | body: params.toString(),
180 | headers: {
181 | Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`,
182 | 'Content-Type': 'application/x-www-form-urlencoded',
183 | },
184 | })
185 | if (!resp.ok) {
186 | console.log(await resp.text())
187 | throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true })
188 | }
189 |
190 | return AuthorizationToken.parse(await resp.json())
191 | }
192 |
```
--------------------------------------------------------------------------------
/apps/sandbox-container/server/userContainer.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { DurableObject } from 'cloudflare:workers'
2 |
3 | import { OPEN_CONTAINER_PORT } from '../shared/consts'
4 | import { MAX_CONTAINERS, proxyFetch, startAndWaitForPort } from './containerHelpers'
5 | import { getContainerManager } from './containerManager'
6 | import { fileToBase64 } from './utils'
7 |
8 | import type { ExecParams, FileList, FileWrite } from '../shared/schema'
9 | import type { Env } from './sandbox.server.context'
10 |
11 | export class UserContainer extends DurableObject<Env> {
12 | constructor(
13 | public ctx: DurableObjectState,
14 | public env: Env
15 | ) {
16 | console.log('creating user container DO')
17 | super(ctx, env)
18 | }
19 |
20 | async destroyContainer(): Promise<void> {
21 | await this.ctx.container?.destroy()
22 | }
23 |
24 | async killContainer(): Promise<void> {
25 | console.log('Reaping container')
26 | const containerManager = getContainerManager(this.env)
27 | const active = await containerManager.listActive()
28 | if (this.ctx.id.toString() in active) {
29 | console.log('killing container')
30 | await this.destroyContainer()
31 | await containerManager.killContainer(this.ctx.id.toString())
32 | }
33 | }
34 |
35 | async container_initialize(): Promise<string> {
36 | // kill container
37 | await this.killContainer()
38 |
39 | // try to cleanup cleanup old containers
40 | const containerManager = getContainerManager(this.env)
41 |
42 | // if more than half of our containers are being used, let's try reaping
43 | if ((await containerManager.listActive()).length >= MAX_CONTAINERS / 2) {
44 | await containerManager.tryKillOldContainers()
45 | if ((await containerManager.listActive()).length >= MAX_CONTAINERS) {
46 | throw new Error(
47 | `Unable to reap enough containers. There are ${MAX_CONTAINERS} active container sandboxes, please wait`
48 | )
49 | }
50 | }
51 |
52 | // start container
53 | let startedContainer = false
54 | await this.ctx.blockConcurrencyWhile(async () => {
55 | startedContainer = await startAndWaitForPort(
56 | this.env.ENVIRONMENT,
57 | this.ctx.container,
58 | OPEN_CONTAINER_PORT
59 | )
60 | })
61 | if (!startedContainer) {
62 | throw new Error('Failed to start container')
63 | }
64 |
65 | // track and manage lifecycle
66 | await containerManager.trackContainer(this.ctx.id.toString())
67 |
68 | return `Created new container`
69 | }
70 |
71 | async container_ping(): Promise<string> {
72 | const res = await proxyFetch(
73 | this.env.ENVIRONMENT,
74 | this.ctx.container,
75 | new Request(`http://host:${OPEN_CONTAINER_PORT}/ping`),
76 | OPEN_CONTAINER_PORT
77 | )
78 | if (!res || !res.ok) {
79 | throw new Error(`Request to container failed: ${await res.text()}`)
80 | }
81 | return await res.text()
82 | }
83 |
84 | async container_exec(params: ExecParams): Promise<string> {
85 | const res = await proxyFetch(
86 | this.env.ENVIRONMENT,
87 | this.ctx.container,
88 | new Request(`http://host:${OPEN_CONTAINER_PORT}/exec`, {
89 | method: 'POST',
90 | body: JSON.stringify(params),
91 | headers: {
92 | 'content-type': 'application/json',
93 | },
94 | }),
95 | OPEN_CONTAINER_PORT
96 | )
97 | if (!res || !res.ok) {
98 | throw new Error(`Request to container failed: ${await res.text()}`)
99 | }
100 | const txt = await res.text()
101 | return txt
102 | }
103 |
104 | async container_ls(): Promise<FileList> {
105 | const res = await proxyFetch(
106 | this.env.ENVIRONMENT,
107 | this.ctx.container,
108 | new Request(`http://host:${OPEN_CONTAINER_PORT}/files/ls`),
109 | OPEN_CONTAINER_PORT
110 | )
111 | if (!res || !res.ok) {
112 | throw new Error(`Request to container failed: ${await res.text()}`)
113 | }
114 | const json = (await res.json()) as FileList
115 | return json
116 | }
117 |
118 | async container_file_delete(filePath: string): Promise<boolean> {
119 | const res = await proxyFetch(
120 | this.env.ENVIRONMENT,
121 | this.ctx.container,
122 | new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`, {
123 | method: 'DELETE',
124 | }),
125 | OPEN_CONTAINER_PORT
126 | )
127 | return res.ok
128 | }
129 | async container_file_read(
130 | filePath: string
131 | ): Promise<
132 | | { type: 'text'; textOutput: string; mimeType: string | undefined }
133 | | { type: 'base64'; base64Output: string; mimeType: string | undefined }
134 | > {
135 | const res = await proxyFetch(
136 | this.env.ENVIRONMENT,
137 | this.ctx.container,
138 | new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`),
139 | OPEN_CONTAINER_PORT
140 | )
141 | if (!res || !res.ok) {
142 | throw new Error(`Request to container failed: ${await res.text()}`)
143 | }
144 |
145 | const mimeType = res.headers.get('Content-Type') ?? undefined
146 | const blob = await res.blob()
147 |
148 | if (mimeType && mimeType.startsWith('text')) {
149 | return {
150 | type: 'text',
151 | textOutput: await blob.text(),
152 | mimeType,
153 | }
154 | } else {
155 | return {
156 | type: 'base64',
157 | base64Output: await fileToBase64(blob),
158 | mimeType,
159 | }
160 | }
161 | }
162 |
163 | async container_file_write(file: FileWrite): Promise<string> {
164 | const res = await proxyFetch(
165 | this.env.ENVIRONMENT,
166 | this.ctx.container,
167 | new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents`, {
168 | method: 'POST',
169 | body: JSON.stringify(file),
170 | headers: {
171 | 'content-type': 'application/json',
172 | },
173 | }),
174 | OPEN_CONTAINER_PORT
175 | )
176 | if (!res || !res.ok) {
177 | throw new Error(`Request to container failed: ${await res.text()}`)
178 | }
179 | return `Wrote file: ${file.path}`
180 | }
181 | }
182 |
```
--------------------------------------------------------------------------------
/apps/workers-bindings/src/bindings.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-ai-search.prompts'
13 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
14 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
15 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
16 | import { registerD1Tools } from '@repo/mcp-common/src/tools/d1.tools'
17 | import { registerDocsTools } from '@repo/mcp-common/src/tools/docs-ai-search.tools'
18 | import { registerHyperdriveTools } from '@repo/mcp-common/src/tools/hyperdrive.tools'
19 | import { registerKVTools } from '@repo/mcp-common/src/tools/kv_namespace.tools'
20 | import { registerR2BucketTools } from '@repo/mcp-common/src/tools/r2_bucket.tools'
21 | import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools'
22 | import { MetricsTracker } from '@repo/mcp-observability'
23 |
24 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
25 | import type { Env } from './bindings.context'
26 |
27 | export { UserDetails }
28 |
29 | const env = getEnv<Env>()
30 |
31 | const metrics = new MetricsTracker(env.MCP_METRICS, {
32 | name: env.MCP_SERVER_NAME,
33 | version: env.MCP_SERVER_VERSION,
34 | })
35 |
36 | export type WorkersBindingsMCPState = { activeAccountId: string | null }
37 |
38 | // Context from the auth process, encrypted & stored in the auth token
39 | // and provided to the DurableMCP as this.props
40 | type Props = AuthProps
41 |
42 | export class WorkersBindingsMCP extends McpAgent<Env, WorkersBindingsMCPState, Props> {
43 | _server: CloudflareMCPServer | undefined
44 | set server(server: CloudflareMCPServer) {
45 | this._server = server
46 | }
47 |
48 | get server(): CloudflareMCPServer {
49 | if (!this._server) {
50 | throw new Error('Tried to access server before it was initialized')
51 | }
52 |
53 | return this._server
54 | }
55 |
56 | initialState: WorkersBindingsMCPState = {
57 | activeAccountId: null,
58 | }
59 |
60 | constructor(ctx: DurableObjectState, env: Env) {
61 | super(ctx, env)
62 | }
63 |
64 | async init() {
65 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
66 | const props = getProps(this)
67 | const userId = props.type === 'user_token' ? props.user.id : undefined
68 |
69 | this.server = new CloudflareMCPServer({
70 | userId,
71 | wae: this.env.MCP_METRICS,
72 | serverInfo: {
73 | name: this.env.MCP_SERVER_NAME,
74 | version: this.env.MCP_SERVER_VERSION,
75 | },
76 | })
77 |
78 | registerAccountTools(this)
79 | registerKVTools(this)
80 | registerWorkersTools(this)
81 | registerR2BucketTools(this)
82 | registerD1Tools(this)
83 | registerHyperdriveTools(this)
84 |
85 | // Add docs tools
86 | registerDocsTools(this.server, this.env)
87 | registerPrompts(this.server)
88 | }
89 |
90 | async getActiveAccountId() {
91 | try {
92 | const props = getProps(this)
93 | // account tokens are scoped to one account
94 | if (props.type === 'account_token') {
95 | return props.account.id
96 | }
97 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
98 | // we do this so we can persist activeAccountId across sessions
99 | const userDetails = getUserDetails(env, props.user.id)
100 | return await userDetails.getActiveAccountId()
101 | } catch (e) {
102 | this.server.recordError(e)
103 | return null
104 | }
105 | }
106 |
107 | async setActiveAccountId(accountId: string) {
108 | try {
109 | const props = getProps(this)
110 | // account tokens are scoped to one account
111 | if (props.type === 'account_token') {
112 | return
113 | }
114 | const userDetails = getUserDetails(env, props.user.id)
115 | await userDetails.setActiveAccountId(accountId)
116 | } catch (e) {
117 | this.server.recordError(e)
118 | }
119 | }
120 | }
121 |
122 | const BindingsScopes = {
123 | ...RequiredScopes,
124 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
125 | 'workers:write':
126 | 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
127 | 'd1:write': 'Create, read, and write to D1 databases',
128 | } as const
129 |
130 | export default {
131 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
132 | if (await isApiTokenRequest(req, env)) {
133 | console.log('is token mode')
134 | return await handleApiTokenMode(WorkersBindingsMCP, req, env, ctx)
135 | }
136 |
137 | return new OAuthProvider({
138 | apiHandlers: {
139 | '/mcp': WorkersBindingsMCP.serve('/mcp'),
140 | '/sse': WorkersBindingsMCP.serveSSE('/sse'),
141 | },
142 | // @ts-ignore
143 | defaultHandler: createAuthHandlers({ scopes: BindingsScopes, metrics }),
144 | authorizeEndpoint: '/oauth/authorize',
145 | tokenEndpoint: '/token',
146 | tokenExchangeCallback: (options) =>
147 | handleTokenExchangeCallback(
148 | options,
149 | env.CLOUDFLARE_CLIENT_ID,
150 | env.CLOUDFLARE_CLIENT_SECRET
151 | ),
152 | // Cloudflare access token TTL
153 | accessTokenTTL: 3600,
154 | clientRegistrationEndpoint: '/register',
155 | }).fetch(req, env, ctx)
156 | },
157 | }
158 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/worker.tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import {
4 | handleGetWorkersService,
5 | handleWorkerScriptDownload,
6 | handleWorkersList,
7 | } from '../api/workers.api'
8 | import { getCloudflareClient } from '../cloudflare-api'
9 | import { fmt } from '../format'
10 | import { getProps } from '../get-props'
11 |
12 | import type { CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types'
13 |
14 | /**
15 | * Registers the workers tools with the MCP server
16 | * @param server The MCP server instance
17 | * @param accountId Cloudflare account ID
18 | * @param apiToken Cloudflare API token
19 | */
20 | // Define the scriptName parameter schema
21 | const workerNameParam = z.string().describe('The name of the worker script to retrieve')
22 |
23 | export function registerWorkersTools(agent: CloudflareMcpAgent) {
24 | // Tool to list all workers
25 | agent.server.tool(
26 | 'workers_list',
27 | fmt.trim(`
28 | List all Workers in your Cloudflare account.
29 |
30 | If you only need details of a single Worker, use workers_get_worker.
31 | `),
32 | {},
33 | {
34 | title: 'List Workers',
35 | annotations: {
36 | readOnlyHint: true,
37 | destructiveHint: false,
38 | },
39 | },
40 | async () => {
41 | const accountId = await agent.getActiveAccountId()
42 | if (!accountId) {
43 | return {
44 | content: [
45 | {
46 | type: 'text',
47 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
48 | },
49 | ],
50 | }
51 | }
52 |
53 | try {
54 | const props = getProps(agent)
55 | const results = await handleWorkersList({
56 | client: getCloudflareClient(props.accessToken),
57 | accountId,
58 | })
59 | // Extract worker details and sort by created_on date (newest first)
60 | const workers = results
61 | .map((worker) => ({
62 | name: worker.id,
63 | // The API client doesn't know tag exists. The tag is needed in other places such as Workers Builds
64 | id: z.object({ tag: z.string() }).parse(worker),
65 | modified_on: worker.modified_on || null,
66 | created_on: worker.created_on || null,
67 | }))
68 | // order by created_on desc ( newest first )
69 | .sort((a, b) => {
70 | if (!a.created_on) return 1
71 | if (!b.created_on) return -1
72 | return new Date(b.created_on).getTime() - new Date(a.created_on).getTime()
73 | })
74 |
75 | return {
76 | content: [
77 | {
78 | type: 'text',
79 | text: JSON.stringify({
80 | workers,
81 | count: workers.length,
82 | }),
83 | },
84 | ],
85 | }
86 | } catch (e) {
87 | agent.server.recordError(e)
88 | return {
89 | content: [
90 | {
91 | type: 'text',
92 | text: `Error listing workers: ${e instanceof Error && e.message}`,
93 | },
94 | ],
95 | }
96 | }
97 | }
98 | )
99 |
100 | // Tool to get a specific worker's script details
101 | agent.server.tool(
102 | 'workers_get_worker',
103 | 'Get the details of the Cloudflare Worker.',
104 | {
105 | scriptName: workerNameParam,
106 | },
107 | {
108 | title: 'Get Worker details',
109 | annotations: {
110 | readOnlyHint: true,
111 | destructiveHint: false,
112 | },
113 | },
114 | async (params) => {
115 | const accountId = await agent.getActiveAccountId()
116 | if (!accountId) {
117 | return {
118 | content: [
119 | {
120 | type: 'text',
121 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
122 | },
123 | ],
124 | }
125 | }
126 |
127 | try {
128 | const props = getProps(agent)
129 | const { scriptName } = params
130 | const res = await handleGetWorkersService({
131 | apiToken: props.accessToken,
132 | scriptName,
133 | accountId,
134 | })
135 |
136 | if (!res.result) {
137 | return {
138 | content: [
139 | {
140 | type: 'text',
141 | text: 'Worker not found',
142 | },
143 | ],
144 | }
145 | }
146 |
147 | return {
148 | content: [
149 | {
150 | type: 'text',
151 | text: await fmt.asTSV([
152 | {
153 | name: res.result.id,
154 | id: res.result.default_environment.script_tag,
155 | },
156 | ]),
157 | },
158 | ],
159 | }
160 | } catch (e) {
161 | agent.server.recordError(e)
162 | return {
163 | content: [
164 | {
165 | type: 'text',
166 | text: `Error retrieving worker script: ${e instanceof Error && e.message}`,
167 | },
168 | ],
169 | }
170 | }
171 | }
172 | )
173 |
174 | // Tool to get a specific worker's script content
175 | agent.server.tool(
176 | 'workers_get_worker_code',
177 | 'Get the source code of a Cloudflare Worker. Note: This may be a bundled version of the worker.',
178 | { scriptName: workerNameParam },
179 | {
180 | title: 'Get Worker code',
181 | annotations: {
182 | readOnlyHint: true,
183 | destructiveHint: false,
184 | },
185 | },
186 | async (params) => {
187 | const accountId = await agent.getActiveAccountId()
188 | if (!accountId) {
189 | return {
190 | content: [
191 | {
192 | type: 'text',
193 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
194 | },
195 | ],
196 | }
197 | }
198 |
199 | try {
200 | const props = getProps(agent)
201 | const { scriptName } = params
202 | const scriptContent = await handleWorkerScriptDownload({
203 | client: getCloudflareClient(props.accessToken),
204 | scriptName,
205 | accountId,
206 | })
207 | return {
208 | content: [
209 | {
210 | type: 'text',
211 | text: scriptContent,
212 | },
213 | ],
214 | }
215 | } catch (e) {
216 | agent.server.recordError(e)
217 | return {
218 | content: [
219 | {
220 | type: 'text',
221 | text: `Error retrieving worker script: ${e instanceof Error && e.message}`,
222 | },
223 | ],
224 | }
225 | }
226 | }
227 | )
228 | }
229 |
```
--------------------------------------------------------------------------------
/apps/workers-observability/src/workers-observability.app.ts:
--------------------------------------------------------------------------------
```typescript
1 | import OAuthProvider from '@cloudflare/workers-oauth-provider'
2 | import { McpAgent } from 'agents/mcp'
3 |
4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
5 | import {
6 | createAuthHandlers,
7 | handleTokenExchangeCallback,
8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler'
9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
10 | import { getEnv } from '@repo/mcp-common/src/env'
11 | import { getProps } from '@repo/mcp-common/src/get-props'
12 | import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-ai-search.prompts'
13 | import { RequiredScopes } from '@repo/mcp-common/src/scopes'
14 | import { initSentryWithUser } from '@repo/mcp-common/src/sentry'
15 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
16 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
17 | import { registerDocsTools } from '@repo/mcp-common/src/tools/docs-ai-search.tools'
18 | import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools'
19 |
20 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
21 | import { registerObservabilityTools } from './tools/workers-observability.tools'
22 |
23 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
24 | import type { Env } from './workers-observability.context'
25 |
26 | export { UserDetails }
27 |
28 | const env = getEnv<Env>()
29 |
30 | const metrics = new MetricsTracker(env.MCP_METRICS, {
31 | name: env.MCP_SERVER_NAME,
32 | version: env.MCP_SERVER_VERSION,
33 | })
34 |
35 | // Context from the auth process, encrypted & stored in the auth token
36 | // and provided to the DurableMCP as this.props
37 | type Props = AuthProps
38 |
39 | type State = { activeAccountId: string | null }
40 |
41 | export class ObservabilityMCP extends McpAgent<Env, State, Props> {
42 | _server: CloudflareMCPServer | undefined
43 | set server(server: CloudflareMCPServer) {
44 | this._server = server
45 | }
46 | get server(): CloudflareMCPServer {
47 | if (!this._server) {
48 | throw new Error('Tried to access server before it was initialized')
49 | }
50 |
51 | return this._server
52 | }
53 |
54 | async init() {
55 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
56 | const props = getProps(this)
57 | const userId = props.type === 'user_token' ? props.user.id : undefined
58 | const sentry =
59 | props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined
60 |
61 | this.server = new CloudflareMCPServer({
62 | userId,
63 | wae: this.env.MCP_METRICS,
64 | serverInfo: {
65 | name: this.env.MCP_SERVER_NAME,
66 | version: this.env.MCP_SERVER_VERSION,
67 | },
68 | sentry,
69 | options: {
70 | instructions: `# Cloudflare Workers Observability Tool
71 | * A cloudflare worker is a serverless function
72 | * Workers Observability is the tool to inspect the logs for your cloudflare Worker
73 | * Each log is a structured JSON payload with keys and values
74 |
75 |
76 | This server allows you to analyze your Cloudflare Workers logs and metrics.
77 | `,
78 | },
79 | })
80 |
81 | registerAccountTools(this)
82 |
83 | // Register Cloudflare Workers tools
84 | registerWorkersTools(this)
85 |
86 | // Register Cloudflare Workers logs tools
87 | registerObservabilityTools(this)
88 |
89 | // Add docs tools
90 | registerDocsTools(this.server, this.env)
91 | registerPrompts(this.server)
92 | }
93 |
94 | async getActiveAccountId() {
95 | try {
96 | const props = getProps(this)
97 | // account tokens are scoped to one account
98 | if (props.type === 'account_token') {
99 | return props.account.id
100 | }
101 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
102 | // we do this so we can persist activeAccountId across sessions
103 | const userDetails = getUserDetails(env, props.user.id)
104 | return await userDetails.getActiveAccountId()
105 | } catch (e) {
106 | this.server.recordError(e)
107 | return null
108 | }
109 | }
110 |
111 | async setActiveAccountId(accountId: string) {
112 | try {
113 | const props = getProps(this)
114 | // account tokens are scoped to one account
115 | if (props.type === 'account_token') {
116 | return
117 | }
118 | const userDetails = getUserDetails(env, props.user.id)
119 | await userDetails.setActiveAccountId(accountId)
120 | } catch (e) {
121 | this.server.recordError(e)
122 | }
123 | }
124 | }
125 |
126 | const ObservabilityScopes = {
127 | ...RequiredScopes,
128 | 'account:read': 'See your account info such as account details, analytics, and memberships.',
129 | 'workers:read':
130 | 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
131 | 'workers_observability:read': 'See observability logs for your account',
132 | } as const
133 |
134 | export default {
135 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
136 | if (await isApiTokenRequest(req, env)) {
137 | return await handleApiTokenMode(ObservabilityMCP, req, env, ctx)
138 | }
139 |
140 | return new OAuthProvider({
141 | apiHandlers: {
142 | '/mcp': ObservabilityMCP.serve('/mcp'),
143 | '/sse': ObservabilityMCP.serveSSE('/sse'),
144 | },
145 | // @ts-ignore
146 | defaultHandler: createAuthHandlers({ scopes: ObservabilityScopes, metrics }),
147 | authorizeEndpoint: '/oauth/authorize',
148 | tokenEndpoint: '/token',
149 | tokenExchangeCallback: (options) =>
150 | handleTokenExchangeCallback(
151 | options,
152 | env.CLOUDFLARE_CLIENT_ID,
153 | env.CLOUDFLARE_CLIENT_SECRET
154 | ),
155 | // Cloudflare access token TTL
156 | accessTokenTTL: 3600,
157 | clientRegistrationEndpoint: '/register',
158 | }).fetch(req, env, ctx)
159 | },
160 | }
161 |
```
--------------------------------------------------------------------------------
/apps/sandbox-container/server/containerMcp.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpAgent } from 'agents/mcp'
2 |
3 | import { getProps } from '@repo/mcp-common/src/get-props'
4 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
5 |
6 | import { ExecParams, FilePathParam, FileWrite } from '../shared/schema'
7 | import { BASE_INSTRUCTIONS } from './prompts'
8 | import { stripProtocolFromFilePath } from './utils'
9 |
10 | import type { Props, UserContainer } from './sandbox.server.app'
11 | import type { Env } from './sandbox.server.context'
12 |
13 | export class ContainerMcpAgent extends McpAgent<Env, never, Props> {
14 | _server: CloudflareMCPServer | undefined
15 | set server(server: CloudflareMCPServer) {
16 | this._server = server
17 | }
18 |
19 | get server(): CloudflareMCPServer {
20 | if (!this._server) {
21 | throw new Error('Tried to access server before it was initialized')
22 | }
23 |
24 | return this._server
25 | }
26 |
27 | get userContainer(): DurableObjectStub<UserContainer> {
28 | const props = getProps(this)
29 | // TODO: Support account scoped tokens?
30 | if (props.type === 'account_token') {
31 | throw new Error('Container server does not currently support account scoped tokens')
32 | }
33 | const userContainer = this.env.USER_CONTAINER.idFromName(props.user.id)
34 | return this.env.USER_CONTAINER.get(userContainer)
35 | }
36 |
37 | constructor(
38 | public ctx: DurableObjectState,
39 | public env: Env
40 | ) {
41 | console.log('creating container DO')
42 | super(ctx, env)
43 | }
44 |
45 | async init() {
46 | const props = getProps(this)
47 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point
48 | const userId = props.type === 'user_token' ? props.user.id : undefined
49 |
50 | this.server = new CloudflareMCPServer({
51 | userId,
52 | wae: this.env.MCP_METRICS,
53 | serverInfo: {
54 | name: this.env.MCP_SERVER_NAME,
55 | version: this.env.MCP_SERVER_VERSION,
56 | },
57 | options: { instructions: BASE_INSTRUCTIONS },
58 | })
59 |
60 | this.server.tool(
61 | 'container_initialize',
62 | `Start or restart the container.
63 | Use this tool to initialize a container before running any python or node.js code that the user requests ro run.`,
64 | async () => {
65 | const props = getProps(this)
66 | if (props.type === 'account_token') {
67 | return {
68 | // TODO: Support account scoped tokens?
69 | // we'll need to add support for an account blocklist in that case
70 | content: [
71 | {
72 | type: 'text',
73 | text: 'Container server does not currently support account scoped tokens.',
74 | },
75 | ],
76 | }
77 | }
78 |
79 | const userInBlocklist = await this.env.USER_BLOCKLIST.get(props.user.id)
80 | if (userInBlocklist) {
81 | return {
82 | content: [{ type: 'text', text: 'Blocked from intializing container.' }],
83 | }
84 | }
85 | return {
86 | content: [{ type: 'text', text: await this.userContainer.container_initialize() }],
87 | }
88 | }
89 | )
90 |
91 | this.server.tool(
92 | 'container_ping',
93 | `Ping the container for liveliness. Use this tool to check if the container is running.`,
94 | async () => {
95 | return {
96 | content: [{ type: 'text', text: await this.userContainer.container_ping() }],
97 | }
98 | }
99 | )
100 | this.server.tool(
101 | 'container_exec',
102 | `Run a command in a container and return the results from stdout.
103 | If necessary, set a timeout. To debug, stream back standard error.
104 | If you're using python, ALWAYS use python3 alongside pip3`,
105 | { args: ExecParams },
106 | async ({ args }) => {
107 | return {
108 | content: [{ type: 'text', text: await this.userContainer.container_exec(args) }],
109 | }
110 | }
111 | )
112 | this.server.tool(
113 | 'container_file_delete',
114 | 'Delete file in the working directory',
115 | { args: FilePathParam },
116 | async ({ args }) => {
117 | const path = await stripProtocolFromFilePath(args.path)
118 | const deleted = await this.userContainer.container_file_delete(path)
119 | return {
120 | content: [{ type: 'text', text: `File deleted: ${deleted}.` }],
121 | }
122 | }
123 | )
124 | this.server.tool(
125 | 'container_file_write',
126 | 'Create a new file with the provided contents in the working direcotry, overwriting the file if it already exists',
127 | { args: FileWrite },
128 | async ({ args }) => {
129 | args.path = await stripProtocolFromFilePath(args.path)
130 | return {
131 | content: [{ type: 'text', text: await this.userContainer.container_file_write(args) }],
132 | }
133 | }
134 | )
135 | this.server.tool(
136 | 'container_files_list',
137 | 'List working directory file tree. This just reads the contents of the current working directory',
138 | async () => {
139 | // Begin workaround using container read rather than ls:
140 | const readFile = await this.userContainer.container_file_read('.')
141 | return {
142 | content: [
143 | {
144 | type: 'resource',
145 | resource: {
146 | text: readFile.type === 'text' ? readFile.textOutput : readFile.base64Output,
147 | uri: `file://`,
148 | mimeType: readFile.mimeType,
149 | },
150 | },
151 | ],
152 | }
153 | }
154 | )
155 | this.server.tool(
156 | 'container_file_read',
157 | '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.',
158 | { args: FilePathParam },
159 | async ({ args }) => {
160 | const path = await stripProtocolFromFilePath(args.path)
161 | const readFile = await this.userContainer.container_file_read(path)
162 |
163 | return {
164 | content: [
165 | {
166 | type: 'resource',
167 | resource: {
168 | ...(readFile.type === 'text'
169 | ? { text: readFile.textOutput }
170 | : { blob: readFile.base64Output }),
171 | uri: `file://${path}`,
172 | mimeType: readFile.mimeType,
173 | },
174 | },
175 | ],
176 | }
177 | }
178 | )
179 | }
180 | }
181 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/api/cf1-integration.api.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { fetchCloudflareApi } from '../cloudflare-api'
2 | import {
3 | AssetCategoriesResponse,
4 | AssetDetail,
5 | AssetsResponse,
6 | IntegrationResponse,
7 | IntegrationsResponse,
8 | } from '../types/cf1-integrations.types'
9 | import { V4Schema } from '../v4-api'
10 |
11 | import type { z } from 'zod'
12 | import type {
13 | zReturnedAssetCategoriesResult,
14 | zReturnedAssetsResult,
15 | zReturnedIntegrationResult,
16 | zReturnedIntegrationsResult,
17 | } from '../types/cf1-integrations.types'
18 |
19 | interface BaseParams {
20 | accountId: string
21 | apiToken: string
22 | }
23 |
24 | interface PaginationParams {
25 | page?: number
26 | pageSize?: number
27 | }
28 |
29 | type IntegrationParams = BaseParams & { integrationIdParam: string }
30 | type AssetCategoryParams = BaseParams & { type?: string; vendor?: string }
31 | type AssetSearchParams = BaseParams & { searchTerm: string } & PaginationParams
32 | type AssetByIdParams = BaseParams & { assetId: string }
33 | type AssetByCategoryParams = BaseParams & { categoryId: string } & PaginationParams
34 | type AssetByIntegrationParams = BaseParams & { integrationId: string } & PaginationParams
35 |
36 | const buildParams = (baseParams: Record<string, string>, pagination?: PaginationParams) => {
37 | const params = new URLSearchParams(baseParams)
38 | if (pagination?.page) params.append('page', String(pagination.page))
39 | if (pagination?.pageSize) params.append('page_size', String(pagination.pageSize))
40 | return params
41 | }
42 |
43 | const buildIntegrationEndpoint = (integrationId: string) => `/casb/integrations/${integrationId}`
44 | const buildAssetEndpoint = (assetId?: string) =>
45 | assetId ? `/casb/assets/${assetId}` : '/casb/assets'
46 | const buildAssetCategoryEndpoint = () => '/casb/asset_categories'
47 |
48 | const makeApiCall = async <T>({
49 | endpoint,
50 | accountId,
51 | apiToken,
52 | responseSchema,
53 | params,
54 | }: {
55 | endpoint: string
56 | accountId: string
57 | apiToken: string
58 | responseSchema: z.ZodType<any>
59 | params?: URLSearchParams
60 | }): Promise<T> => {
61 | try {
62 | const fullEndpoint = params ? `${endpoint}?${params.toString()}` : endpoint
63 | const data = await fetchCloudflareApi({
64 | endpoint: fullEndpoint,
65 | accountId,
66 | apiToken,
67 | responseSchema,
68 | options: {
69 | method: 'GET',
70 | headers: { 'Content-Type': 'application/json' },
71 | },
72 | })
73 | return data.result as T
74 | } catch (error) {
75 | console.error(`API call failed for ${endpoint}:`, error)
76 | throw error
77 | }
78 | }
79 |
80 | // Resource-specific API call handlers
81 | const makeIntegrationCall = <T>(params: IntegrationParams, responseSchema: z.ZodType<any>) =>
82 | makeApiCall<T>({
83 | endpoint: buildIntegrationEndpoint(params.integrationIdParam),
84 | accountId: params.accountId,
85 | apiToken: params.apiToken,
86 | responseSchema,
87 | })
88 |
89 | const makeAssetCall = <T>(
90 | params: BaseParams & PaginationParams,
91 | responseSchema: z.ZodType<any>,
92 | assetId?: string,
93 | additionalParams?: Record<string, string>
94 | ) =>
95 | makeApiCall<T>({
96 | endpoint: buildAssetEndpoint(assetId),
97 | accountId: params.accountId,
98 | apiToken: params.apiToken,
99 | responseSchema,
100 | params: buildParams(additionalParams || {}, params),
101 | })
102 |
103 | const makeAssetCategoryCall = <T>(params: AssetCategoryParams, responseSchema: z.ZodType<any>) =>
104 | makeApiCall<T>({
105 | endpoint: buildAssetCategoryEndpoint(),
106 | accountId: params.accountId,
107 | apiToken: params.apiToken,
108 | responseSchema,
109 | params: buildParams({
110 | ...(params.vendor && { vendor: params.vendor }),
111 | ...(params.type && { type: params.type }),
112 | }),
113 | })
114 |
115 | // Integration handlers
116 | export async function handleIntegrationById(
117 | params: IntegrationParams
118 | ): Promise<{ integration: zReturnedIntegrationResult | null }> {
119 | const integration = await makeIntegrationCall<zReturnedIntegrationResult>(
120 | params,
121 | V4Schema(IntegrationResponse)
122 | )
123 | return { integration }
124 | }
125 |
126 | export async function handleIntegrations(
127 | params: BaseParams
128 | ): Promise<{ integrations: zReturnedIntegrationsResult | null }> {
129 | const integrations = await makeApiCall<zReturnedIntegrationsResult>({
130 | endpoint: '/casb/integrations',
131 | accountId: params.accountId,
132 | apiToken: params.apiToken,
133 | responseSchema: V4Schema(IntegrationsResponse),
134 | })
135 | return { integrations }
136 | }
137 |
138 | // Asset category handlers
139 | export async function handleAssetCategories(
140 | params: AssetCategoryParams
141 | ): Promise<{ categories: zReturnedAssetCategoriesResult | null }> {
142 | const categories = await makeAssetCategoryCall<zReturnedAssetCategoriesResult>(
143 | params,
144 | V4Schema(AssetCategoriesResponse)
145 | )
146 | return { categories }
147 | }
148 |
149 | // Asset handlers
150 | export async function handleAssets(params: BaseParams & PaginationParams) {
151 | const assets = await makeAssetCall<zReturnedAssetsResult>(params, V4Schema(AssetsResponse))
152 | return { assets }
153 | }
154 |
155 | export async function handleAssetsByIntegrationId(params: AssetByIntegrationParams) {
156 | const assets = await makeAssetCall<zReturnedAssetsResult>(
157 | params,
158 | V4Schema(AssetsResponse),
159 | undefined,
160 | { integration_id: params.integrationId }
161 | )
162 | return { assets }
163 | }
164 |
165 | export async function handleAssetById(params: AssetByIdParams) {
166 | const asset = await makeAssetCall<zReturnedAssetsResult>(
167 | params,
168 | V4Schema(AssetDetail),
169 | params.assetId
170 | )
171 | return { asset }
172 | }
173 |
174 | export async function handleAssetsByAssetCategoryId(params: AssetByCategoryParams) {
175 | const assets = await makeAssetCall<zReturnedAssetsResult>(
176 | params,
177 | V4Schema(AssetsResponse),
178 | undefined,
179 | { category_id: params.categoryId }
180 | )
181 | return { assets }
182 | }
183 |
184 | export async function handleAssetsSearch(params: AssetSearchParams) {
185 | const assets = await makeAssetCall<zReturnedAssetsResult>(
186 | params,
187 | V4Schema(AssetsResponse),
188 | undefined,
189 | { search: params.searchTerm }
190 | )
191 | return { assets }
192 | }
193 |
```