#
tokens: 49846/50000 25/376 files (page 4/27)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 27. Use http://codebase.md/cloudflare/mcp-server-cloudflare?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .dockerignore
├── .editorconfig
├── .eslintrc.cjs
├── .github
│   ├── actions
│   │   └── setup
│   │       └── action.yml
│   ├── ISSUE_TEMPLATE
│   │   └── bug_report.md
│   └── workflows
│       ├── branches.yml
│       ├── main.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.cjs
├── .syncpackrc.cjs
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── apps
│   ├── ai-gateway
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── ai-gateway.app.ts
│   │   │   ├── ai-gateway.context.ts
│   │   │   ├── tools
│   │   │   │   └── ai-gateway.tools.ts
│   │   │   └── types.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── auditlogs
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── auditlogs.app.ts
│   │   │   ├── auditlogs.context.ts
│   │   │   └── tools
│   │   │       └── auditlogs.tools.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── autorag
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── autorag.app.ts
│   │   │   ├── autorag.context.ts
│   │   │   ├── tools
│   │   │   │   └── autorag.tools.ts
│   │   │   └── types.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── browser-rendering
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── browser.app.ts
│   │   │   ├── browser.context.ts
│   │   │   └── tools
│   │   │       └── browser.tools.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── cloudflare-one-casb
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── cf1-casb.app.ts
│   │   │   ├── cf1-casb.context.ts
│   │   │   └── tools
│   │   │       └── integrations.tools.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── demo-day
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── frontend
│   │   │   ├── index.html
│   │   │   ├── public
│   │   │   │   ├── anthropic.svg
│   │   │   │   ├── asana.svg
│   │   │   │   ├── atlassian.svg
│   │   │   │   ├── canva.svg
│   │   │   │   ├── cloudflare_logo.svg
│   │   │   │   ├── cloudflare.svg
│   │   │   │   ├── dina.jpg
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   ├── favicon.png
│   │   │   │   ├── intercom.svg
│   │   │   │   ├── linear.svg
│   │   │   │   ├── matt.jpg
│   │   │   │   ├── mcp_demo_day.svg
│   │   │   │   ├── mcpog.png
│   │   │   │   ├── more.svg
│   │   │   │   ├── paypal.svg
│   │   │   │   ├── pete.jpeg
│   │   │   │   ├── sentry.svg
│   │   │   │   ├── special_guest.png
│   │   │   │   ├── square.svg
│   │   │   │   ├── stripe.svg
│   │   │   │   ├── sunil.jpg
│   │   │   │   └── webflow.svg
│   │   │   ├── script.js
│   │   │   └── styles.css
│   │   ├── package.json
│   │   ├── src
│   │   │   └── demo-day.app.ts
│   │   ├── tsconfig.json
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.json
│   ├── dex-analysis
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── dex-analysis.app.ts
│   │   │   ├── dex-analysis.context.ts
│   │   │   ├── tools
│   │   │   │   └── dex-analysis.tools.ts
│   │   │   └── warp_diag_reader.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── dns-analytics
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── dns-analytics.app.ts
│   │   │   ├── dns-analytics.context.ts
│   │   │   └── tools
│   │   │       └── dex-analytics.tools.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── docs-autorag
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── docs-autorag.app.ts
│   │   │   ├── docs-autorag.context.ts
│   │   │   └── tools
│   │   │       └── docs-autorag.tools.ts
│   │   ├── tsconfig.json
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── docs-vectorize
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── docs-vectorize.app.ts
│   │   │   └── docs-vectorize.context.ts
│   │   ├── tsconfig.json
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── graphql
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── graphql.app.ts
│   │   │   ├── graphql.context.ts
│   │   │   └── tools
│   │   │       └── graphql.tools.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── logpush
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── logpush.app.ts
│   │   │   ├── logpush.context.ts
│   │   │   └── tools
│   │   │       └── logpush.tools.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── radar
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── radar.app.ts
│   │   │   ├── radar.context.ts
│   │   │   ├── tools
│   │   │   │   ├── radar.tools.ts
│   │   │   │   └── url-scanner.tools.ts
│   │   │   ├── types
│   │   │   │   ├── radar.ts
│   │   │   │   └── url-scanner.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── sandbox-container
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── container
│   │   │   ├── fileUtils.spec.ts
│   │   │   ├── fileUtils.ts
│   │   │   ├── sandbox.container.app.ts
│   │   │   └── tsconfig.json
│   │   ├── CONTRIBUTING.md
│   │   ├── Dockerfile
│   │   ├── evals
│   │   │   ├── exec.eval.ts
│   │   │   ├── files.eval.ts
│   │   │   ├── initialize.eval.ts
│   │   │   └── utils.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── server
│   │   │   ├── containerHelpers.ts
│   │   │   ├── containerManager.ts
│   │   │   ├── containerMcp.ts
│   │   │   ├── metrics.ts
│   │   │   ├── prompts.ts
│   │   │   ├── sandbox.server.app.ts
│   │   │   ├── sandbox.server.context.ts
│   │   │   ├── userContainer.ts
│   │   │   ├── utils.spec.ts
│   │   │   └── utils.ts
│   │   ├── shared
│   │   │   ├── consts.ts
│   │   │   └── schema.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.evals.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── workers-bindings
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── .gitignore
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── evals
│   │   │   ├── accounts.eval.ts
│   │   │   ├── hyperdrive.eval.ts
│   │   │   ├── kv_namespaces.eval.ts
│   │   │   ├── types.d.ts
│   │   │   └── utils.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bindings.app.ts
│   │   │   └── bindings.context.ts
│   │   ├── tsconfig.json
│   │   ├── vitest.config.evals.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   ├── workers-builds
│   │   ├── .dev.vars.example
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── CONTRIBUTING.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── tools
│   │   │   │   └── workers-builds.tools.ts
│   │   │   ├── workers-builds.app.ts
│   │   │   └── workers-builds.context.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vite.config.mts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.jsonc
│   └── workers-observability
│       ├── .dev.vars.example
│       ├── .eslintrc.cjs
│       ├── CHANGELOG.md
│       ├── CONTRIBUTING.md
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── tools
│       │   │   └── workers-observability.tools.ts
│       │   ├── workers-observability.app.ts
│       │   └── workers-observability.context.ts
│       ├── tsconfig.json
│       ├── types.d.ts
│       ├── vitest.config.ts
│       ├── worker-configuration.d.ts
│       └── wrangler.jsonc
├── CONTRIBUTING.md
├── implementation-guides
│   ├── evals.md
│   ├── tools.md
│   └── type-validators.md
├── LICENSE
├── package.json
├── packages
│   ├── eslint-config
│   │   ├── CHANGELOG.md
│   │   ├── default.cjs
│   │   ├── package.json
│   │   └── README.md
│   ├── eval-tools
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── runTask.ts
│   │   │   ├── scorers.ts
│   │   │   └── test-models.ts
│   │   ├── tsconfig.json
│   │   ├── worker-configuration.d.ts
│   │   └── wrangler.json
│   ├── mcp-common
│   │   ├── .eslintrc.cjs
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── api
│   │   │   │   ├── account.api.ts
│   │   │   │   ├── cf1-integration.api.ts
│   │   │   │   ├── workers-builds.api.ts
│   │   │   │   ├── workers-observability.api.ts
│   │   │   │   ├── workers.api.ts
│   │   │   │   └── zone.api.ts
│   │   │   ├── api-handler.ts
│   │   │   ├── api-token-mode.ts
│   │   │   ├── cloudflare-api.ts
│   │   │   ├── cloudflare-auth.ts
│   │   │   ├── cloudflare-oauth-handler.ts
│   │   │   ├── config.ts
│   │   │   ├── constants.ts
│   │   │   ├── durable-kv-store.ts
│   │   │   ├── durable-objects
│   │   │   │   └── user_details.do.ts
│   │   │   ├── env.ts
│   │   │   ├── format.spec.ts
│   │   │   ├── format.ts
│   │   │   ├── get-props.ts
│   │   │   ├── mcp-error.ts
│   │   │   ├── poll.ts
│   │   │   ├── prompts
│   │   │   │   └── docs-vectorize.prompts.ts
│   │   │   ├── scopes.ts
│   │   │   ├── sentry.ts
│   │   │   ├── server.ts
│   │   │   ├── tools
│   │   │   │   ├── account.tools.ts
│   │   │   │   ├── d1.tools.ts
│   │   │   │   ├── docs-vectorize.tools.ts
│   │   │   │   ├── hyperdrive.tools.ts
│   │   │   │   ├── kv_namespace.tools.ts
│   │   │   │   ├── r2_bucket.tools.ts
│   │   │   │   ├── worker.tools.ts
│   │   │   │   └── zone.tools.ts
│   │   │   ├── types
│   │   │   │   ├── cf1-integrations.types.ts
│   │   │   │   ├── cloudflare-mcp-agent.types.ts
│   │   │   │   ├── d1.types.ts
│   │   │   │   ├── hyperdrive.types.ts
│   │   │   │   ├── kv_namespace.types.ts
│   │   │   │   ├── r2_bucket.types.ts
│   │   │   │   ├── shared.types.ts
│   │   │   │   ├── tools.types.ts
│   │   │   │   ├── workers-builds.types.ts
│   │   │   │   ├── workers-logs.types.ts
│   │   │   │   └── workers.types.ts
│   │   │   ├── utils.spec.ts
│   │   │   ├── utils.ts
│   │   │   └── v4-api.ts
│   │   ├── tests
│   │   │   └── utils
│   │   │       └── cloudflare-mock.ts
│   │   ├── tsconfig.json
│   │   ├── types.d.ts
│   │   ├── vitest.config.ts
│   │   └── worker-configuration.d.ts
│   ├── mcp-observability
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── analytics-engine.ts
│   │   │   ├── index.ts
│   │   │   └── metrics.ts
│   │   ├── tsconfig.json
│   │   └── worker-configuration.d.ts
│   ├── tools
│   │   ├── .eslintrc.cjs
│   │   ├── bin
│   │   │   ├── run-changeset-new
│   │   │   ├── run-eslint-workers
│   │   │   ├── run-fix-deps
│   │   │   ├── run-tsc
│   │   │   ├── run-turbo
│   │   │   ├── run-vitest
│   │   │   ├── run-vitest-ci
│   │   │   ├── run-wrangler-deploy
│   │   │   ├── run-wrangler-types
│   │   │   └── runx
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bin
│   │   │   │   └── runx.ts
│   │   │   ├── changesets.spec.ts
│   │   │   ├── changesets.ts
│   │   │   ├── cmd
│   │   │   │   └── deploy-published-packages.ts
│   │   │   ├── proc.ts
│   │   │   ├── test
│   │   │   │   ├── fixtures
│   │   │   │   │   └── changesets
│   │   │   │   │       ├── empty
│   │   │   │   │       │   └── .gitkeep
│   │   │   │   │       ├── invalid-json
│   │   │   │   │       │   └── published-packages.json
│   │   │   │   │       ├── invalid-schema
│   │   │   │   │       │   └── published-packages.json
│   │   │   │   │       └── valid
│   │   │   │   │           └── published-packages.json
│   │   │   │   └── setup.ts
│   │   │   └── tsconfig.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── typescript-config
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── tools.json
│       ├── workers-lib.json
│       └── workers.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── server.json
├── tsconfig.json
├── turbo.json
└── vitest.workspace.ts
```

# Files

--------------------------------------------------------------------------------
/apps/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 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/docs-vectorize.tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod'
  2 | 
  3 | import type { CloudflareMcpAgentNoAccount } from '../types/cloudflare-mcp-agent.types'
  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 agent The MCP server instance
 16 |  */
 17 | export function registerDocsTools(agent: CloudflareMcpAgentNoAccount, env: RequiredEnv) {
 18 | 	agent.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 | 		- AutoRAG, 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 | 	agent.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/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 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/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 | 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 | }: {
 84 | 	client_id: string
 85 | 	redirect_uri: string
 86 | 	state: AuthRequest
 87 | 	scopes: Record<string, string>
 88 | }): Promise<{ authUrl: string; codeVerifier: string }> {
 89 | 	const { codeChallenge, codeVerifier } = await generatePKCECodes()
 90 | 
 91 | 	return {
 92 | 		authUrl: generateAuthUrl({
 93 | 			client_id,
 94 | 			redirect_uri,
 95 | 			state: btoa(JSON.stringify({ ...state, codeVerifier })),
 96 | 			code_challenge: codeChallenge,
 97 | 			scopes,
 98 | 		}),
 99 | 		codeVerifier: codeVerifier,
100 | 	}
101 | }
102 | 
103 | type AuthorizationToken = z.infer<typeof AuthorizationToken>
104 | const AuthorizationToken = z.object({
105 | 	access_token: z.string(),
106 | 	expires_in: z.number(),
107 | 	refresh_token: z.string(),
108 | 	scope: z.string(),
109 | 	token_type: z.string(),
110 | })
111 | /**
112 |  * Fetches an authorization token from Cloudflare.
113 |  *
114 |  * @param {Object} options
115 |  * @param {string} options.client_id - The client ID of the application.
116 |  * @param {string} options.client_secret - The client secret of the application.
117 |  * @param {string} options.code - The authorization code.
118 |  * @param {string} options.redirect_uri - The redirect URI of the application.
119 |  *
120 |  * @returns {Promise<[string, null] | [null, Response]>} A promise that resolves to an array containing the access token or an error response.
121 |  */
122 | export async function getAuthToken({
123 | 	client_id,
124 | 	client_secret,
125 | 	redirect_uri,
126 | 	code_verifier,
127 | 	code,
128 | }: {
129 | 	client_id: string
130 | 	client_secret: string
131 | 	redirect_uri: string
132 | 	code_verifier: string
133 | 	code: string
134 | }): Promise<AuthorizationToken> {
135 | 	if (!code) {
136 | 		throw new McpError('Missing code', 400)
137 | 	}
138 | 
139 | 	const params = new URLSearchParams({
140 | 		grant_type: 'authorization_code',
141 | 		client_id,
142 | 		redirect_uri,
143 | 		code,
144 | 		code_verifier,
145 | 	}).toString()
146 | 	const resp = await fetch('https://dash.cloudflare.com/oauth2/token', {
147 | 		method: 'POST',
148 | 		headers: {
149 | 			Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`,
150 | 			'Content-Type': 'application/x-www-form-urlencoded',
151 | 		},
152 | 		body: params,
153 | 	})
154 | 
155 | 	if (!resp.ok) {
156 | 		console.log(await resp.text())
157 | 		throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true })
158 | 	}
159 | 
160 | 	return AuthorizationToken.parse(await resp.json())
161 | }
162 | 
163 | export async function refreshAuthToken({
164 | 	client_id,
165 | 	client_secret,
166 | 	refresh_token,
167 | }: {
168 | 	client_id: string
169 | 	client_secret: string
170 | 	refresh_token: string
171 | }): Promise<AuthorizationToken> {
172 | 	const params = new URLSearchParams({
173 | 		grant_type: 'refresh_token',
174 | 		client_id,
175 | 		refresh_token,
176 | 	})
177 | 
178 | 	const resp = await fetch('https://dash.cloudflare.com/oauth2/token', {
179 | 		method: 'POST',
180 | 		body: params.toString(),
181 | 		headers: {
182 | 			Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`,
183 | 			'Content-Type': 'application/x-www-form-urlencoded',
184 | 		},
185 | 	})
186 | 	if (!resp.ok) {
187 | 		console.log(await resp.text())
188 | 		throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true })
189 | 	}
190 | 
191 | 	return AuthorizationToken.parse(await resp.json())
192 | }
193 | 
```

--------------------------------------------------------------------------------
/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-vectorize.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-vectorize.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, this.env)
 87 | 		registerPrompts(this)
 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-vectorize.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-vectorize.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, this.env)
 91 | 		registerPrompts(this)
 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 | 								text: readFile.type === 'text' ? readFile.textOutput : readFile.base64Output,
169 | 								uri: `file://${path}`,
170 | 								mimeType: readFile.mimeType,
171 | 							},
172 | 						},
173 | 					],
174 | 				}
175 | 			}
176 | 		)
177 | 	}
178 | }
179 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/apps/ai-gateway/src/tools/ai-gateway.tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
  2 | import { getProps } from '@repo/mcp-common/src/get-props'
  3 | 
  4 | import { GatewayIdParam, ListLogsParams, LogIdParam, pageParam, perPageParam } from '../types'
  5 | 
  6 | import type { LogListParams } from 'cloudflare/resources/ai-gateway'
  7 | import type { AIGatewayMCP } from '../ai-gateway.app'
  8 | 
  9 | export function registerAIGatewayTools(agent: AIGatewayMCP) {
 10 | 	agent.server.tool(
 11 | 		'list_gateways',
 12 | 		'List Gateways',
 13 | 		{
 14 | 			page: pageParam,
 15 | 			per_page: perPageParam,
 16 | 		},
 17 | 		async (params) => {
 18 | 			const accountId = await agent.getActiveAccountId()
 19 | 			if (!accountId) {
 20 | 				return {
 21 | 					content: [
 22 | 						{
 23 | 							type: 'text',
 24 | 							text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
 25 | 						},
 26 | 					],
 27 | 				}
 28 | 			}
 29 | 			try {
 30 | 				const props = getProps(agent)
 31 | 				const client = getCloudflareClient(props.accessToken)
 32 | 				const r = await client.aiGateway.list({
 33 | 					account_id: accountId,
 34 | 					page: params.page,
 35 | 					per_page: params.per_page,
 36 | 				})
 37 | 
 38 | 				return {
 39 | 					content: [
 40 | 						{
 41 | 							type: 'text',
 42 | 							text: JSON.stringify({
 43 | 								result: r.result,
 44 | 								result_info: r.result_info,
 45 | 							}),
 46 | 						},
 47 | 					],
 48 | 				}
 49 | 			} catch (error) {
 50 | 				return {
 51 | 					content: [
 52 | 						{
 53 | 							type: 'text',
 54 | 							text: `Error listing gateways: ${error instanceof Error && error.message}`,
 55 | 						},
 56 | 					],
 57 | 				}
 58 | 			}
 59 | 		}
 60 | 	)
 61 | 
 62 | 	agent.server.tool('list_logs', 'List Logs', ListLogsParams, async (params) => {
 63 | 		try {
 64 | 			const accountId = await agent.getActiveAccountId()
 65 | 			if (!accountId) {
 66 | 				return {
 67 | 					content: [
 68 | 						{
 69 | 							type: 'text',
 70 | 							text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
 71 | 						},
 72 | 					],
 73 | 				}
 74 | 			}
 75 | 
 76 | 			const { gateway_id, ...filters } = params
 77 | 
 78 | 			const props = getProps(agent)
 79 | 			const client = getCloudflareClient(props.accessToken)
 80 | 			const r = await client.aiGateway.logs.list(gateway_id, {
 81 | 				...filters,
 82 | 				account_id: accountId,
 83 | 			} as LogListParams)
 84 | 
 85 | 			return {
 86 | 				content: [
 87 | 					{
 88 | 						type: 'text',
 89 | 						text: JSON.stringify({
 90 | 							result: r.result,
 91 | 							result_info: r.result_info,
 92 | 						}),
 93 | 					},
 94 | 				],
 95 | 			}
 96 | 		} catch (error) {
 97 | 			return {
 98 | 				content: [
 99 | 					{
100 | 						type: 'text',
101 | 						text: `Error listing logs: ${error instanceof Error && error.message}`,
102 | 					},
103 | 				],
104 | 			}
105 | 		}
106 | 	})
107 | 
108 | 	agent.server.tool(
109 | 		'get_log_details',
110 | 		'Get a single Log details',
111 | 		{
112 | 			gateway_id: GatewayIdParam,
113 | 			log_id: LogIdParam,
114 | 		},
115 | 		async (params) => {
116 | 			const accountId = await agent.getActiveAccountId()
117 | 			if (!accountId) {
118 | 				return {
119 | 					content: [
120 | 						{
121 | 							type: 'text',
122 | 							text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
123 | 						},
124 | 					],
125 | 				}
126 | 			}
127 | 
128 | 			try {
129 | 				const props = getProps(agent)
130 | 				const client = getCloudflareClient(props.accessToken)
131 | 				const r = await client.aiGateway.logs.get(params.gateway_id, params.log_id, {
132 | 					account_id: accountId,
133 | 				})
134 | 
135 | 				return {
136 | 					content: [
137 | 						{
138 | 							type: 'text',
139 | 							text: JSON.stringify({
140 | 								result: r,
141 | 							}),
142 | 						},
143 | 					],
144 | 				}
145 | 			} catch (error) {
146 | 				return {
147 | 					content: [
148 | 						{
149 | 							type: 'text',
150 | 							text: `Error getting log: ${error instanceof Error && error.message}`,
151 | 						},
152 | 					],
153 | 				}
154 | 			}
155 | 		}
156 | 	)
157 | 
158 | 	agent.server.tool(
159 | 		'get_log_request_body',
160 | 		'Get Log Request Body',
161 | 		{
162 | 			gateway_id: GatewayIdParam,
163 | 			log_id: LogIdParam,
164 | 		},
165 | 		async (params) => {
166 | 			const accountId = await agent.getActiveAccountId()
167 | 			if (!accountId) {
168 | 				return {
169 | 					content: [
170 | 						{
171 | 							type: 'text',
172 | 							text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
173 | 						},
174 | 					],
175 | 				}
176 | 			}
177 | 
178 | 			try {
179 | 				const props = getProps(agent)
180 | 				const client = getCloudflareClient(props.accessToken)
181 | 				const r = await client.aiGateway.logs.request(params.gateway_id, params.log_id, {
182 | 					account_id: accountId,
183 | 				})
184 | 
185 | 				return {
186 | 					content: [
187 | 						{
188 | 							type: 'text',
189 | 							text: JSON.stringify({
190 | 								result: r,
191 | 							}),
192 | 						},
193 | 					],
194 | 				}
195 | 			} catch (error) {
196 | 				return {
197 | 					content: [
198 | 						{
199 | 							type: 'text',
200 | 							text: `Error getting log request body: ${error instanceof Error && error.message}`,
201 | 						},
202 | 					],
203 | 				}
204 | 			}
205 | 		}
206 | 	)
207 | 
208 | 	agent.server.tool(
209 | 		'get_log_response_body',
210 | 		'Get Log Response Body',
211 | 		{
212 | 			gateway_id: GatewayIdParam,
213 | 			log_id: LogIdParam,
214 | 		},
215 | 		async (params) => {
216 | 			const accountId = await agent.getActiveAccountId()
217 | 			if (!accountId) {
218 | 				return {
219 | 					content: [
220 | 						{
221 | 							type: 'text',
222 | 							text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
223 | 						},
224 | 					],
225 | 				}
226 | 			}
227 | 
228 | 			try {
229 | 				const props = getProps(agent)
230 | 				const client = getCloudflareClient(props.accessToken)
231 | 				const r = await client.aiGateway.logs.response(params.gateway_id, params.log_id, {
232 | 					account_id: accountId,
233 | 				})
234 | 
235 | 				return {
236 | 					content: [
237 | 						{
238 | 							type: 'text',
239 | 							text: JSON.stringify({
240 | 								result: r,
241 | 							}),
242 | 						},
243 | 					],
244 | 				}
245 | 			} catch (error) {
246 | 				return {
247 | 					content: [
248 | 						{
249 | 							type: 'text',
250 | 							text: `Error getting log response body: ${error instanceof Error && error.message}`,
251 | 						},
252 | 					],
253 | 				}
254 | 			}
255 | 		}
256 | 	)
257 | }
258 | 
```

--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/d1.tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod'
  2 | 
  3 | import { getCloudflareClient } from '../cloudflare-api'
  4 | import { MISSING_ACCOUNT_ID_RESPONSE } from '../constants'
  5 | import { getProps } from '../get-props'
  6 | import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types'
  7 | import {
  8 | 	D1DatabaseNameParam,
  9 | 	D1DatabasePrimaryLocationHintParam,
 10 | 	D1DatabaseQueryParamsParam,
 11 | 	D1DatabaseQuerySqlParam,
 12 | } from '../types/d1.types'
 13 | import { PaginationPageParam, PaginationPerPageParam } from '../types/shared.types'
 14 | 
 15 | export function registerD1Tools(agent: CloudflareMcpAgent) {
 16 | 	agent.server.tool(
 17 | 		'd1_databases_list',
 18 | 		'List all of the D1 databases in your Cloudflare account',
 19 | 		{
 20 | 			name: D1DatabaseNameParam.nullable().optional(),
 21 | 			page: PaginationPageParam,
 22 | 			per_page: PaginationPerPageParam,
 23 | 		},
 24 | 		{
 25 | 			title: 'List D1 databases',
 26 | 			annotations: {
 27 | 				readOnlyHint: true,
 28 | 			},
 29 | 		},
 30 | 		async ({ name, page, per_page }) => {
 31 | 			const account_id = await agent.getActiveAccountId()
 32 | 			if (!account_id) {
 33 | 				return MISSING_ACCOUNT_ID_RESPONSE
 34 | 			}
 35 | 			try {
 36 | 				const props = getProps(agent)
 37 | 				const client = getCloudflareClient(props.accessToken)
 38 | 				const listResponse = await client.d1.database.list({
 39 | 					account_id,
 40 | 					name: name ?? undefined,
 41 | 					page: page ?? undefined,
 42 | 					per_page: per_page ?? undefined,
 43 | 				})
 44 | 
 45 | 				return {
 46 | 					content: [
 47 | 						{
 48 | 							type: 'text',
 49 | 							text: JSON.stringify({
 50 | 								result: listResponse.result,
 51 | 								result_info: listResponse.result_info,
 52 | 							}),
 53 | 						},
 54 | 					],
 55 | 				}
 56 | 			} catch (error) {
 57 | 				return {
 58 | 					content: [
 59 | 						{
 60 | 							type: 'text',
 61 | 							text: `Error listing D1 databases: ${error instanceof Error && error.message}`,
 62 | 						},
 63 | 					],
 64 | 				}
 65 | 			}
 66 | 		}
 67 | 	)
 68 | 
 69 | 	agent.server.tool(
 70 | 		'd1_database_create',
 71 | 		'Create a new D1 database in your Cloudflare account',
 72 | 		{
 73 | 			name: D1DatabaseNameParam,
 74 | 			primary_location_hint: D1DatabasePrimaryLocationHintParam.nullable().optional(),
 75 | 		},
 76 | 		{
 77 | 			title: 'Create D1 database',
 78 | 			annotations: {
 79 | 				readOnlyHint: false,
 80 | 				destructiveHint: false,
 81 | 			},
 82 | 		},
 83 | 		async ({ name, primary_location_hint }) => {
 84 | 			const account_id = await agent.getActiveAccountId()
 85 | 			if (!account_id) {
 86 | 				return MISSING_ACCOUNT_ID_RESPONSE
 87 | 			}
 88 | 			try {
 89 | 				const props = getProps(agent)
 90 | 				const client = getCloudflareClient(props.accessToken)
 91 | 				const d1Database = await client.d1.database.create({
 92 | 					account_id,
 93 | 					name,
 94 | 					primary_location_hint: primary_location_hint ?? undefined,
 95 | 				})
 96 | 
 97 | 				return {
 98 | 					content: [
 99 | 						{
100 | 							type: 'text',
101 | 							text: JSON.stringify(d1Database),
102 | 						},
103 | 					],
104 | 				}
105 | 			} catch (error) {
106 | 				return {
107 | 					content: [
108 | 						{
109 | 							type: 'text',
110 | 							text: `Error creating D1 database: ${error instanceof Error && error.message}`,
111 | 						},
112 | 					],
113 | 				}
114 | 			}
115 | 		}
116 | 	)
117 | 
118 | 	agent.server.tool(
119 | 		'd1_database_delete',
120 | 		'Delete a d1 database in your Cloudflare account',
121 | 		{ database_id: z.string() },
122 | 		{
123 | 			title: 'Delete D1 database',
124 | 			annotations: {
125 | 				readOnlyHint: false,
126 | 				destructiveHint: true,
127 | 			},
128 | 		},
129 | 		async ({ database_id }) => {
130 | 			const account_id = await agent.getActiveAccountId()
131 | 			if (!account_id) {
132 | 				return MISSING_ACCOUNT_ID_RESPONSE
133 | 			}
134 | 			try {
135 | 				const props = getProps(agent)
136 | 				const client = getCloudflareClient(props.accessToken)
137 | 				const deleteResponse = await client.d1.database.delete(database_id, {
138 | 					account_id,
139 | 				})
140 | 				return {
141 | 					content: [
142 | 						{
143 | 							type: 'text',
144 | 							text: JSON.stringify(deleteResponse),
145 | 						},
146 | 					],
147 | 				}
148 | 			} catch (error) {
149 | 				return {
150 | 					content: [
151 | 						{
152 | 							type: 'text',
153 | 							text: `Error deleting D1 database: ${error instanceof Error && error.message}`,
154 | 						},
155 | 					],
156 | 				}
157 | 			}
158 | 		}
159 | 	)
160 | 
161 | 	agent.server.tool(
162 | 		'd1_database_get',
163 | 		'Get a D1 database in your Cloudflare account',
164 | 		{ database_id: z.string() },
165 | 		{
166 | 			title: 'Get D1 database',
167 | 			annotations: {
168 | 				readOnlyHint: true,
169 | 			},
170 | 		},
171 | 		async ({ database_id }) => {
172 | 			const account_id = await agent.getActiveAccountId()
173 | 			if (!account_id) {
174 | 				return MISSING_ACCOUNT_ID_RESPONSE
175 | 			}
176 | 			try {
177 | 				const props = getProps(agent)
178 | 				const client = getCloudflareClient(props.accessToken)
179 | 				const d1Database = await client.d1.database.get(database_id, {
180 | 					account_id,
181 | 				})
182 | 
183 | 				return {
184 | 					content: [
185 | 						{
186 | 							type: 'text',
187 | 							text: JSON.stringify(d1Database),
188 | 						},
189 | 					],
190 | 				}
191 | 			} catch (error) {
192 | 				return {
193 | 					content: [
194 | 						{
195 | 							type: 'text',
196 | 							text: `Error getting D1 database: ${error instanceof Error && error.message}`,
197 | 						},
198 | 					],
199 | 				}
200 | 			}
201 | 		}
202 | 	)
203 | 
204 | 	agent.server.tool(
205 | 		'd1_database_query',
206 | 		'Query a D1 database in your Cloudflare account',
207 | 		{
208 | 			database_id: z.string(),
209 | 			sql: D1DatabaseQuerySqlParam,
210 | 			params: D1DatabaseQueryParamsParam.nullable(),
211 | 		},
212 | 		{
213 | 			title: 'Query D1 database',
214 | 			annotations: {
215 | 				readOnlyHint: false,
216 | 				destructiveHint: false,
217 | 			},
218 | 		},
219 | 		async ({ database_id, sql, params }) => {
220 | 			const account_id = await agent.getActiveAccountId()
221 | 			if (!account_id) {
222 | 				return MISSING_ACCOUNT_ID_RESPONSE
223 | 			}
224 | 			try {
225 | 				const props = getProps(agent)
226 | 				const client = getCloudflareClient(props.accessToken)
227 | 				const queryResult = await client.d1.database.query(database_id, {
228 | 					account_id,
229 | 					sql,
230 | 					params: params ?? undefined,
231 | 				})
232 | 				return {
233 | 					content: [
234 | 						{
235 | 							type: 'text',
236 | 							text: JSON.stringify(queryResult.result),
237 | 						},
238 | 					],
239 | 				}
240 | 			} catch (error) {
241 | 				return {
242 | 					content: [
243 | 						{
244 | 							type: 'text',
245 | 							text: `Error querying D1 database: ${error instanceof Error && error.message}`,
246 | 						},
247 | 					],
248 | 				}
249 | 			}
250 | 		}
251 | 	)
252 | }
253 | 
```

--------------------------------------------------------------------------------
/apps/cloudflare-one-casb/src/tools/integrations.tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod'
  2 | 
  3 | import { withAccountCheck } from '@repo/mcp-common/src/api/account.api'
  4 | import {
  5 | 	handleAssetById,
  6 | 	handleAssetCategories,
  7 | 	handleAssets,
  8 | 	handleAssetsByAssetCategoryId,
  9 | 	handleAssetsByIntegrationId,
 10 | 	handleAssetsSearch,
 11 | 	handleIntegrationById,
 12 | 	handleIntegrations,
 13 | } from '@repo/mcp-common/src/api/cf1-integration.api'
 14 | import {
 15 | 	assetCategoryTypeParam,
 16 | 	assetCategoryVendorParam,
 17 | } from '@repo/mcp-common/src/types/cf1-integrations.types'
 18 | 
 19 | import type { ToolDefinition } from '@repo/mcp-common/src/types/tools.types'
 20 | import type { CASBMCP } from '../cf1-casb.app'
 21 | 
 22 | const PAGE_SIZE = 3
 23 | 
 24 | const integrationIdParam = z.string().describe('The UUID of the integration to analyze')
 25 | const assetSearchTerm = z.string().describe('The search keyword for assets')
 26 | const assetIdParam = z.string().describe('The UUID of the asset to analyze')
 27 | const assetCategoryIdParam = z.string().describe('The UUID of the asset category to analyze')
 28 | 
 29 | const toolDefinitions: Array<ToolDefinition<any>> = [
 30 | 	{
 31 | 		name: 'integration_by_id',
 32 | 		description: 'Analyze Cloudflare One Integration by ID',
 33 | 		params: { integrationIdParam },
 34 | 		handler: async ({
 35 | 			integrationIdParam,
 36 | 			accountId,
 37 | 			apiToken,
 38 | 		}: {
 39 | 			integrationIdParam: string
 40 | 			accountId: string
 41 | 			apiToken: string
 42 | 		}) => {
 43 | 			const { integration } = await handleIntegrationById({
 44 | 				integrationIdParam,
 45 | 				accountId,
 46 | 				apiToken,
 47 | 			})
 48 | 			return { integration }
 49 | 		},
 50 | 	},
 51 | 	{
 52 | 		name: 'integrations_list',
 53 | 		description: 'List all Cloudflare One Integrations in a given account',
 54 | 		params: {},
 55 | 		handler: async ({ accountId, apiToken }: { accountId: string; apiToken: string }) => {
 56 | 			const { integrations } = await handleIntegrations({ accountId, apiToken })
 57 | 			return { integrations }
 58 | 		},
 59 | 	},
 60 | 	{
 61 | 		name: 'assets_search',
 62 | 		description: 'Search Assets by keyword',
 63 | 		params: { assetSearchTerm },
 64 | 		handler: async ({
 65 | 			assetSearchTerm,
 66 | 			accountId,
 67 | 			apiToken,
 68 | 		}: {
 69 | 			assetSearchTerm: string
 70 | 			accountId: string
 71 | 			apiToken: string
 72 | 		}) => {
 73 | 			const { assets } = await handleAssetsSearch({
 74 | 				accountId,
 75 | 				apiToken,
 76 | 				searchTerm: assetSearchTerm,
 77 | 				pageSize: PAGE_SIZE,
 78 | 			})
 79 | 			return { assets }
 80 | 		},
 81 | 	},
 82 | 	{
 83 | 		name: 'asset_by_id',
 84 | 		description: 'Search Assets by ID',
 85 | 		params: { assetIdParam },
 86 | 		handler: async ({
 87 | 			assetIdParam,
 88 | 			accountId,
 89 | 			apiToken,
 90 | 		}: {
 91 | 			assetIdParam: string
 92 | 			accountId: string
 93 | 			apiToken: string
 94 | 		}) => {
 95 | 			const { asset } = await handleAssetById({
 96 | 				accountId,
 97 | 				apiToken,
 98 | 				assetId: assetIdParam,
 99 | 			})
100 | 			return { asset }
101 | 		},
102 | 	},
103 | 	{
104 | 		name: 'assets_by_integration_id',
105 | 		description: 'Search Assets by Integration ID',
106 | 		params: { integrationIdParam },
107 | 		handler: async ({
108 | 			integrationIdParam,
109 | 			accountId,
110 | 			apiToken,
111 | 		}: {
112 | 			integrationIdParam: string
113 | 			accountId: string
114 | 			apiToken: string
115 | 		}) => {
116 | 			const { assets } = await handleAssetsByIntegrationId({
117 | 				accountId,
118 | 				apiToken,
119 | 				integrationId: integrationIdParam,
120 | 				pageSize: PAGE_SIZE,
121 | 			})
122 | 			return { assets }
123 | 		},
124 | 	},
125 | 	{
126 | 		name: 'assets_by_category_id',
127 | 		description: 'Search Assets by Asset Category ID',
128 | 		params: { assetCategoryIdParam },
129 | 		handler: async ({
130 | 			assetCategoryIdParam,
131 | 			accountId,
132 | 			apiToken,
133 | 		}: {
134 | 			assetCategoryIdParam: string
135 | 			accountId: string
136 | 			apiToken: string
137 | 		}) => {
138 | 			const { assets } = await handleAssetsByAssetCategoryId({
139 | 				accountId,
140 | 				apiToken,
141 | 				categoryId: assetCategoryIdParam,
142 | 				pageSize: PAGE_SIZE,
143 | 			})
144 | 			return { assets }
145 | 		},
146 | 	},
147 | 	{
148 | 		name: 'assets_list',
149 | 		description: 'Paginated list of Assets',
150 | 		params: {},
151 | 		handler: async ({ accountId, apiToken }: { accountId: string; apiToken: string }) => {
152 | 			const { assets } = await handleAssets({
153 | 				accountId,
154 | 				apiToken,
155 | 				pageSize: PAGE_SIZE,
156 | 			})
157 | 			return { assets }
158 | 		},
159 | 	},
160 | 	{
161 | 		name: 'asset_categories_list',
162 | 		description: 'List Asset Categories',
163 | 		params: {},
164 | 		handler: async ({ accountId, apiToken }: { accountId: string; apiToken: string }) => {
165 | 			const { categories } = await handleAssetCategories({
166 | 				accountId,
167 | 				apiToken,
168 | 			})
169 | 			return { categories }
170 | 		},
171 | 	},
172 | 	{
173 | 		name: 'asset_categories_by_vendor',
174 | 		description: 'List asset categories by vendor',
175 | 		params: { assetCategoryVendorParam },
176 | 		handler: async ({
177 | 			assetCategoryVendorParam,
178 | 			accountId,
179 | 			apiToken,
180 | 		}: {
181 | 			assetCategoryVendorParam: string
182 | 			accountId: string
183 | 			apiToken: string
184 | 		}) => {
185 | 			const { categories } = await handleAssetCategories({
186 | 				accountId,
187 | 				apiToken,
188 | 				vendor: assetCategoryVendorParam,
189 | 			})
190 | 			return { categories }
191 | 		},
192 | 	},
193 | 	{
194 | 		name: 'asset_categories_by_type',
195 | 		description: 'Search Asset Categories by type',
196 | 		params: { assetCategoryTypeParam },
197 | 		handler: async ({
198 | 			assetCategoryTypeParam,
199 | 			accountId,
200 | 			apiToken,
201 | 		}: {
202 | 			assetCategoryTypeParam?: string
203 | 			accountId: string
204 | 			apiToken: string
205 | 		}) => {
206 | 			const { categories } = await handleAssetCategories({
207 | 				accountId,
208 | 				apiToken,
209 | 				type: assetCategoryTypeParam,
210 | 			})
211 | 			return { categories }
212 | 		},
213 | 	},
214 | 	{
215 | 		name: 'asset_categories_by_vendor_and_type',
216 | 		description: 'Search Asset Categories by vendor and type',
217 | 		params: { assetCategoryTypeParam, assetCategoryVendorParam },
218 | 		handler: async ({
219 | 			assetCategoryTypeParam,
220 | 			assetCategoryVendorParam,
221 | 			accountId,
222 | 			apiToken,
223 | 		}: {
224 | 			assetCategoryTypeParam?: string
225 | 			assetCategoryVendorParam: string
226 | 			accountId: string
227 | 			apiToken: string
228 | 		}) => {
229 | 			const { categories } = await handleAssetCategories({
230 | 				accountId,
231 | 				apiToken,
232 | 				type: assetCategoryTypeParam,
233 | 				vendor: assetCategoryVendorParam,
234 | 			})
235 | 			return { categories }
236 | 		},
237 | 	},
238 | ]
239 | 
240 | /**
241 |  * Registers the logs analysis tool with the MCP server
242 |  * @param agent The MCP server instance
243 |  */
244 | export function registerIntegrationsTools(agent: CASBMCP) {
245 | 	toolDefinitions.forEach(({ name, description, params, handler }) => {
246 | 		agent.server.tool(name, description, params, withAccountCheck(agent, handler))
247 | 	})
248 | }
249 | 
```

--------------------------------------------------------------------------------
/implementation-guides/type-validators.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Tool Type Validator Implementation Guide
  2 | 
  3 | This guide outlines the best practices for creating Zod validators for the parameters of MCP (Model Context Protocol) tools, particularly those interacting with Cloudflare resources via the Cloudflare Typescript SDK.
  4 | 
  5 | ## Purpose
  6 | 
  7 | Zod validators serve several critical functions:
  8 | 
  9 | 1.  **Runtime Validation:** They ensure that the arguments provided to tools (often by an LLM) match the expected format, constraints, and types before the tool logic executes or interacts with external APIs (like the Cloudflare SDK).
 10 | 2.  **Type Safety:** They provide strong typing for tool parameters within the TypeScript codebase.
 11 | 3.  **Schema Definition:** They act as a clear, machine-readable definition of a tool's expected inputs, aiding both developers and LLMs in understanding how to use the tool correctly.
 12 | 4.  **SDK Alignment:** They help maintain alignment with underlying SDKs, catching potential breaking changes.
 13 | 
 14 | ## Core Principles
 15 | 
 16 | ### 1. Link to SDK Types with `z.ZodType`
 17 | 
 18 | When a tool parameter corresponds directly to a parameter in the Cloudflare Node SDK (`cloudflare/resources/...`), **always** link your Zod schema to the SDK type using `z.ZodType<SDKType>`. This creates a compile-time dependency.
 19 | 
 20 | **Why?**
 21 | 
 22 | - **Detect SDK Changes:** If the underlying SDK type changes (e.g., type alias renamed, property added/removed/renamed, type changed from `string` to `string | null`), your Zod schema definition will likely cause a TypeScript error during compilation. This immediately flags the need to update the validator and potentially the tool logic, preventing runtime errors caused by SDK misalignment.
 23 | - **Accuracy:** Ensures your validator accurately reflects the type expected by the SDK function you intend to call.
 24 | 
 25 | **Example (`hyperdrive.types.ts`):**
 26 | 
 27 | ```typescript
 28 | import { z } from 'zod'
 29 | 
 30 | import type { ConfigCreateParams } from 'cloudflare/resources/hyperdrive/configs.mjs'
 31 | 
 32 | /** Zod schema for a Hyperdrive config name. */
 33 | export const HyperdriveConfigNameSchema: z.ZodType<ConfigCreateParams['name']> = z
 34 | 	.string()
 35 | 	.min(1)
 36 | 	.max(64)
 37 | 	.regex(/^[a-zA-Z0-9_-]+$/)
 38 | 	.describe('The name of the Hyperdrive configuration (alphanumeric, underscore, hyphen)')
 39 | 
 40 | /** Zod schema for the origin database host (IPv4). */
 41 | export const HyperdriveOriginHostSchema: z.ZodType<ConfigCreateParams.PublicDatabase['host']> = z
 42 | 	.string()
 43 | 	.ip({ version: 'v4' })
 44 | 	.describe('The database host IPv4 address')
 45 | ```
 46 | 
 47 | ### 2. Define Individual Validators Per Field (Avoid Object Schemas for Tool Parameters)
 48 | 
 49 | Define a separate, named Zod schema for **each individual field** that a tool might accept as a parameter. Do **not** group multiple parameters into a single Zod object schema (`z.object({...})`) that the tool accepts as a single `params` argument.
 50 | 
 51 | **Why?**
 52 | 
 53 | - **LLM Clarity:** LLMs generally understand and handle distinct, named parameters better than complex nested objects. Providing individual schemas makes the tool's signature clearer for the LLM to interpret and use correctly.
 54 | - **Reusability:** Individual field schemas (like `HyperdriveConfigIdSchema`, `HyperdriveConfigNameSchema`) can be reused across different tools (e.g., `hyperdrive_create`, `hyperdrive_update`, `hyperdrive_get`).
 55 | - **Modularity:** Easier to manage, update, and test individual validation rules.
 56 | 
 57 | **Example (`hyperdrive.types.ts` Structure):**
 58 | 
 59 | ```typescript
 60 | // --- Base Field Schemas ---
 61 | export const HyperdriveConfigIdSchema = z.string().describe(...);
 62 | export const HyperdriveConfigNameSchema: z.ZodType<...> = z.string()...describe(...);
 63 | export const HyperdriveOriginHostSchema: z.ZodType<...> = z.string()...describe(...);
 64 | export const HyperdriveOriginPortSchema: z.ZodType<...> = z.number()...describe(...);
 65 | // ... other individual fields
 66 | ```
 67 | 
 68 | **Conceptual Tool Definition (Illustrative):**
 69 | 
 70 | Instead of:
 71 | 
 72 | ```typescript
 73 | // DON'T DO THIS for tool params
 74 | const CreateParamsSchema = z.object({
 75 | 	name: HyperdriveConfigNameSchema,
 76 | 	host: HyperdriveOriginHostSchema,
 77 | 	port: HyperdriveOriginPortSchema,
 78 | 	// ... other fields
 79 | })
 80 | 
 81 | // Tool definition would accept one arg: { params: CreateParamsSchema }
 82 | ```
 83 | 
 84 | Do:
 85 | 
 86 | ```typescript
 87 | // DO THIS: Tool definition accepts multiple named args
 88 | // tool('hyperdrive_create', {
 89 | //     name: HyperdriveConfigNameSchema,
 90 | //     host: HyperdriveOriginHostSchema,
 91 | //     port: HyperdriveOriginPortSchema,
 92 | //     // ... other named parameters with their individual schemas
 93 | // }, ...)
 94 | ```
 95 | 
 96 | ### 3. Use `.describe()` Extensively
 97 | 
 98 | Add a clear, concise `.describe('...')` call to **every** Zod schema you define.
 99 | 
100 | **Why?**
101 | 
102 | - **LLM Context:** The description is often extracted and provided to the LLM as part of the tool's definition, helping it understand the purpose and constraints of each parameter.
103 | - **Developer Documentation:** Serves as inline documentation for developers working with the code.
104 | 
105 | **Example (`hyperdrive.types.ts`):**
106 | 
107 | ```typescript
108 | /** Zod schema for the list page number. */
109 | export const HyperdriveListParamPageSchema = z
110 | 	.number()
111 | 	.int()
112 | 	.positive()
113 | 	.optional()
114 | 	.describe('Page number of results') // <-- Good description!
115 | ```
116 | 
117 | ## Naming Conventions
118 | 
119 | Use a consistent naming convention for your validator schemas. A recommended pattern is:
120 | 
121 | `ServiceNameFieldNameSchema`
122 | 
123 | - `ServiceName`: The Cloudflare service (e.g., `Hyperdrive`, `KV`, `D1`, `R2`).
124 | - `FieldName`: The specific field being validated (e.g., `ConfigId`, `ConfigName`, `OriginHost`, `ListParamPage`).
125 | - `Schema`: Suffix indicating it's a Zod schema.
126 | 
127 | **Examples:**
128 | 
129 | - `HyperdriveConfigIdSchema`
130 | - `KVKeySchema`
131 | - `D1DatabaseIdSchema`
132 | - `R2BucketNameSchema`
133 | 
134 | ## Location
135 | 
136 | Place validators related to a specific service or concept in dedicated files within the `packages/mcp-common/src/types/` directory (e.g., `hyperdrive.types.ts`, `kv.ts`).
137 | 
138 | ## Summary
139 | 
140 | By following these principles – linking to SDK types, using granular named validators, providing clear descriptions, and maintaining consistent naming – you create robust, maintainable, and LLM-friendly type validation for MCP tools.
141 | 
```

--------------------------------------------------------------------------------
/apps/workers-builds/src/workers-builds.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 { fmt } from '@repo/mcp-common/src/format'
 12 | import { getProps } from '@repo/mcp-common/src/get-props'
 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 { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools'
 18 | 
 19 | import { MetricsTracker } from '../../../packages/mcp-observability/src'
 20 | import { registerBuildsTools } from './tools/workers-builds.tools'
 21 | 
 22 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
 23 | import type { Env } from './workers-builds.context'
 24 | 
 25 | export { UserDetails }
 26 | 
 27 | const env = getEnv<Env>()
 28 | 
 29 | const metrics = new MetricsTracker(env.MCP_METRICS, {
 30 | 	name: env.MCP_SERVER_NAME,
 31 | 	version: env.MCP_SERVER_VERSION,
 32 | })
 33 | 
 34 | // Context from the auth process, encrypted & stored in the auth token
 35 | // and provided to the DurableMCP as this.props
 36 | type Props = AuthProps
 37 | 
 38 | type State = {
 39 | 	activeAccountId: string | null
 40 | 	activeBuildUUID: string | null
 41 | 	activeWorkerId: string | null
 42 | }
 43 | 
 44 | export class BuildsMCP extends McpAgent<Env, State, Props> {
 45 | 	_server: CloudflareMCPServer | undefined
 46 | 	set server(server: CloudflareMCPServer) {
 47 | 		this._server = server
 48 | 	}
 49 | 	get server(): CloudflareMCPServer {
 50 | 		if (!this._server) {
 51 | 			throw new Error('Tried to access server before it was initialized')
 52 | 		}
 53 | 		return this._server
 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 | 			options: {
 72 | 				instructions: fmt.trim(`
 73 | 					# Cloudflare Workers Builds Tool
 74 | 					* A Cloudflare Worker is a serverless function
 75 | 					* Workers Builds is a CI/CD system for building and deploying your Worker whenever you push code to GitHub/GitLab.
 76 | 
 77 | 					This server allows you to view and debug Cloudflare Workers Builds for your Workers (NOT Cloudflare Pages).
 78 | 
 79 | 					To get started, you can list your accounts (accounts_list) and then set an active account (set_active_account).
 80 | 					Once you have an active account, you can list your Workers (workers_list) and set an active Worker (workers_builds_set_active_worker).
 81 | 					You can then list the builds for your Worker (workers_builds_list_builds) and set an active build (workers_builds_set_active_build).
 82 | 					Once you have an active build, you can view the logs (workers_builds_get_build_logs).
 83 | 				`),
 84 | 			},
 85 | 		})
 86 | 
 87 | 		registerAccountTools(this)
 88 | 
 89 | 		// Register Cloudflare Workers tools
 90 | 		registerWorkersTools(this)
 91 | 
 92 | 		// Register Cloudflare Workers logs tools
 93 | 		registerBuildsTools(this)
 94 | 	}
 95 | 
 96 | 	async getActiveAccountId() {
 97 | 		try {
 98 | 			const props = getProps(this)
 99 | 			// account tokens are scoped to one account
100 | 			if (props.type === 'account_token') {
101 | 				return props.account.id
102 | 			}
103 | 			// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
104 | 			// we do this so we can persist activeAccountId across sessions
105 | 			const userDetails = getUserDetails(env, props.user.id)
106 | 			return await userDetails.getActiveAccountId()
107 | 		} catch (e) {
108 | 			this.server.recordError(e)
109 | 			return null
110 | 		}
111 | 	}
112 | 
113 | 	async setActiveAccountId(accountId: string) {
114 | 		try {
115 | 			const props = getProps(this)
116 | 			// account tokens are scoped to one account
117 | 			if (props.type === 'account_token') {
118 | 				return
119 | 			}
120 | 			const userDetails = getUserDetails(env, props.user.id)
121 | 			await userDetails.setActiveAccountId(accountId)
122 | 		} catch (e) {
123 | 			this.server.recordError(e)
124 | 		}
125 | 	}
126 | 
127 | 	async getActiveBuildUUID(): Promise<string | null> {
128 | 		try {
129 | 			return this.state.activeBuildUUID
130 | 		} catch (e) {
131 | 			this.server.recordError(e)
132 | 			return null
133 | 		}
134 | 	}
135 | 
136 | 	async setActiveBuildUUID(buildUUID: string | null): Promise<void> {
137 | 		try {
138 | 			this.setState({
139 | 				...this.state,
140 | 				activeBuildUUID: buildUUID,
141 | 			})
142 | 		} catch (e) {
143 | 			this.server.recordError(e)
144 | 		}
145 | 	}
146 | 
147 | 	async getActiveWorkerId(): Promise<string | null> {
148 | 		try {
149 | 			return this.state.activeWorkerId
150 | 		} catch (e) {
151 | 			this.server.recordError(e)
152 | 			return null
153 | 		}
154 | 	}
155 | 
156 | 	async setActiveWorkerId(workerId: string | null): Promise<void> {
157 | 		try {
158 | 			this.setState({
159 | 				...this.state,
160 | 				activeWorkerId: workerId,
161 | 			})
162 | 		} catch (e) {
163 | 			this.server.recordError(e)
164 | 		}
165 | 	}
166 | }
167 | 
168 | const BuildsScopes = {
169 | 	...RequiredScopes,
170 | 	'account:read': 'See your account info such as account details, analytics, and memberships.',
171 | 	'workers:read':
172 | 		'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
173 | 	'workers_builds:read':
174 | 		'See and change Cloudflare Workers Builds data such as builds, build configuration, and logs.',
175 | } as const
176 | 
177 | export default {
178 | 	fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
179 | 		if (await isApiTokenRequest(req, env)) {
180 | 			return await handleApiTokenMode(BuildsMCP, req, env, ctx)
181 | 		}
182 | 
183 | 		return new OAuthProvider({
184 | 			apiHandlers: {
185 | 				'/mcp': BuildsMCP.serve('/mcp'),
186 | 				'/sse': BuildsMCP.serveSSE('/sse'),
187 | 			},
188 | 			// @ts-expect-error
189 | 			defaultHandler: createAuthHandlers({ scopes: BuildsScopes, metrics }),
190 | 			authorizeEndpoint: '/oauth/authorize',
191 | 			tokenEndpoint: '/token',
192 | 			tokenExchangeCallback: (options) =>
193 | 				handleTokenExchangeCallback(
194 | 					options,
195 | 					env.CLOUDFLARE_CLIENT_ID,
196 | 					env.CLOUDFLARE_CLIENT_SECRET
197 | 				),
198 | 			// Cloudflare access token TTL
199 | 			accessTokenTTL: 3600,
200 | 			clientRegistrationEndpoint: '/register',
201 | 		}).fetch(req, env, ctx)
202 | 	},
203 | }
204 | 
```
Page 4/27FirstPrevNextLast