This is page 3 of 27. Use http://codebase.md/cloudflare/mcp-server-cloudflare?page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .dockerignore
├── .editorconfig
├── .eslintrc.cjs
├── .github
│ ├── actions
│ │ └── setup
│ │ └── action.yml
│ ├── ISSUE_TEMPLATE
│ │ └── bug_report.md
│ └── workflows
│ ├── branches.yml
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.cjs
├── .syncpackrc.cjs
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── apps
│ ├── ai-gateway
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── ai-gateway.app.ts
│ │ │ ├── ai-gateway.context.ts
│ │ │ ├── tools
│ │ │ │ └── ai-gateway.tools.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── auditlogs
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── auditlogs.app.ts
│ │ │ ├── auditlogs.context.ts
│ │ │ └── tools
│ │ │ └── auditlogs.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── autorag
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── autorag.app.ts
│ │ │ ├── autorag.context.ts
│ │ │ ├── tools
│ │ │ │ └── autorag.tools.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── browser-rendering
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── browser.app.ts
│ │ │ ├── browser.context.ts
│ │ │ └── tools
│ │ │ └── browser.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── cloudflare-one-casb
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── cf1-casb.app.ts
│ │ │ ├── cf1-casb.context.ts
│ │ │ └── tools
│ │ │ └── integrations.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── demo-day
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── frontend
│ │ │ ├── index.html
│ │ │ ├── public
│ │ │ │ ├── anthropic.svg
│ │ │ │ ├── asana.svg
│ │ │ │ ├── atlassian.svg
│ │ │ │ ├── canva.svg
│ │ │ │ ├── cloudflare_logo.svg
│ │ │ │ ├── cloudflare.svg
│ │ │ │ ├── dina.jpg
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── favicon.png
│ │ │ │ ├── intercom.svg
│ │ │ │ ├── linear.svg
│ │ │ │ ├── matt.jpg
│ │ │ │ ├── mcp_demo_day.svg
│ │ │ │ ├── mcpog.png
│ │ │ │ ├── more.svg
│ │ │ │ ├── paypal.svg
│ │ │ │ ├── pete.jpeg
│ │ │ │ ├── sentry.svg
│ │ │ │ ├── special_guest.png
│ │ │ │ ├── square.svg
│ │ │ │ ├── stripe.svg
│ │ │ │ ├── sunil.jpg
│ │ │ │ └── webflow.svg
│ │ │ ├── script.js
│ │ │ └── styles.css
│ │ ├── package.json
│ │ ├── src
│ │ │ └── demo-day.app.ts
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.json
│ ├── dex-analysis
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── dex-analysis.app.ts
│ │ │ ├── dex-analysis.context.ts
│ │ │ ├── tools
│ │ │ │ └── dex-analysis.tools.ts
│ │ │ └── warp_diag_reader.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── dns-analytics
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── dns-analytics.app.ts
│ │ │ ├── dns-analytics.context.ts
│ │ │ └── tools
│ │ │ └── dex-analytics.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-ai-search
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-ai-search.app.ts
│ │ │ └── docs-ai-search.context.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-autorag
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-autorag.app.ts
│ │ │ ├── docs-autorag.context.ts
│ │ │ └── tools
│ │ │ └── docs-autorag.tools.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-vectorize
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-vectorize.app.ts
│ │ │ └── docs-vectorize.context.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── graphql
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── graphql.app.ts
│ │ │ ├── graphql.context.ts
│ │ │ └── tools
│ │ │ └── graphql.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── logpush
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── logpush.app.ts
│ │ │ ├── logpush.context.ts
│ │ │ └── tools
│ │ │ └── logpush.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── radar
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── radar.app.ts
│ │ │ ├── radar.context.ts
│ │ │ ├── tools
│ │ │ │ ├── radar.tools.ts
│ │ │ │ └── url-scanner.tools.ts
│ │ │ ├── types
│ │ │ │ ├── radar.ts
│ │ │ │ └── url-scanner.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── sandbox-container
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── container
│ │ │ ├── fileUtils.spec.ts
│ │ │ ├── fileUtils.ts
│ │ │ ├── sandbox.container.app.ts
│ │ │ └── tsconfig.json
│ │ ├── CONTRIBUTING.md
│ │ ├── Dockerfile
│ │ ├── evals
│ │ │ ├── exec.eval.ts
│ │ │ ├── files.eval.ts
│ │ │ ├── initialize.eval.ts
│ │ │ └── utils.ts
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── server
│ │ │ ├── containerHelpers.ts
│ │ │ ├── containerManager.ts
│ │ │ ├── containerMcp.ts
│ │ │ ├── metrics.ts
│ │ │ ├── prompts.ts
│ │ │ ├── sandbox.server.app.ts
│ │ │ ├── sandbox.server.context.ts
│ │ │ ├── userContainer.ts
│ │ │ ├── utils.spec.ts
│ │ │ └── utils.ts
│ │ ├── shared
│ │ │ ├── consts.ts
│ │ │ └── schema.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.evals.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── workers-bindings
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── evals
│ │ │ ├── accounts.eval.ts
│ │ │ ├── hyperdrive.eval.ts
│ │ │ ├── kv_namespaces.eval.ts
│ │ │ ├── types.d.ts
│ │ │ └── utils.ts
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── bindings.app.ts
│ │ │ └── bindings.context.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.evals.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── workers-builds
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── tools
│ │ │ │ └── workers-builds.tools.ts
│ │ │ ├── workers-builds.app.ts
│ │ │ └── workers-builds.context.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vite.config.mts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ └── workers-observability
│ ├── .dev.vars.example
│ ├── .eslintrc.cjs
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING.md
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── tools
│ │ │ └── workers-observability.tools.ts
│ │ ├── workers-observability.app.ts
│ │ └── workers-observability.context.ts
│ ├── tsconfig.json
│ ├── types.d.ts
│ ├── vitest.config.ts
│ ├── worker-configuration.d.ts
│ └── wrangler.jsonc
├── CONTRIBUTING.md
├── implementation-guides
│ ├── evals.md
│ ├── tools.md
│ └── type-validators.md
├── LICENSE
├── package.json
├── packages
│ ├── eslint-config
│ │ ├── CHANGELOG.md
│ │ ├── default.cjs
│ │ ├── package.json
│ │ └── README.md
│ ├── eval-tools
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── runTask.ts
│ │ │ ├── scorers.ts
│ │ │ └── test-models.ts
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.json
│ ├── mcp-common
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── api
│ │ │ │ ├── account.api.ts
│ │ │ │ ├── cf1-integration.api.ts
│ │ │ │ ├── workers-builds.api.ts
│ │ │ │ ├── workers-observability.api.ts
│ │ │ │ ├── workers.api.ts
│ │ │ │ └── zone.api.ts
│ │ │ ├── api-handler.ts
│ │ │ ├── api-token-mode.ts
│ │ │ ├── cloudflare-api.ts
│ │ │ ├── cloudflare-auth.ts
│ │ │ ├── cloudflare-oauth-handler.ts
│ │ │ ├── config.ts
│ │ │ ├── constants.ts
│ │ │ ├── durable-kv-store.ts
│ │ │ ├── durable-objects
│ │ │ │ └── user_details.do.ts
│ │ │ ├── env.ts
│ │ │ ├── format.spec.ts
│ │ │ ├── format.ts
│ │ │ ├── get-props.ts
│ │ │ ├── mcp-error.ts
│ │ │ ├── poll.ts
│ │ │ ├── prompts
│ │ │ │ ├── docs-ai-search.prompts.ts
│ │ │ │ └── docs-vectorize.prompts.ts
│ │ │ ├── scopes.ts
│ │ │ ├── sentry.ts
│ │ │ ├── server.ts
│ │ │ ├── tools
│ │ │ │ ├── account.tools.ts
│ │ │ │ ├── d1.tools.ts
│ │ │ │ ├── docs-ai-search.tools.ts
│ │ │ │ ├── docs-vectorize.tools.ts
│ │ │ │ ├── hyperdrive.tools.ts
│ │ │ │ ├── kv_namespace.tools.ts
│ │ │ │ ├── r2_bucket.tools.ts
│ │ │ │ ├── worker.tools.ts
│ │ │ │ └── zone.tools.ts
│ │ │ ├── types
│ │ │ │ ├── cf1-integrations.types.ts
│ │ │ │ ├── cloudflare-mcp-agent.types.ts
│ │ │ │ ├── d1.types.ts
│ │ │ │ ├── hyperdrive.types.ts
│ │ │ │ ├── kv_namespace.types.ts
│ │ │ │ ├── r2_bucket.types.ts
│ │ │ │ ├── shared.types.ts
│ │ │ │ ├── tools.types.ts
│ │ │ │ ├── workers-builds.types.ts
│ │ │ │ ├── workers-logs.types.ts
│ │ │ │ └── workers.types.ts
│ │ │ ├── utils.spec.ts
│ │ │ ├── utils.ts
│ │ │ ├── v4-api.ts
│ │ │ └── workers-oauth-utils.ts
│ │ ├── tests
│ │ │ └── utils
│ │ │ └── cloudflare-mock.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ └── worker-configuration.d.ts
│ ├── mcp-observability
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── analytics-engine.ts
│ │ │ ├── index.ts
│ │ │ └── metrics.ts
│ │ ├── tsconfig.json
│ │ └── worker-configuration.d.ts
│ ├── tools
│ │ ├── .eslintrc.cjs
│ │ ├── bin
│ │ │ ├── run-changeset-new
│ │ │ ├── run-eslint-workers
│ │ │ ├── run-fix-deps
│ │ │ ├── run-tsc
│ │ │ ├── run-turbo
│ │ │ ├── run-vitest
│ │ │ ├── run-vitest-ci
│ │ │ ├── run-wrangler-deploy
│ │ │ ├── run-wrangler-types
│ │ │ └── runx
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── bin
│ │ │ │ └── runx.ts
│ │ │ ├── changesets.spec.ts
│ │ │ ├── changesets.ts
│ │ │ ├── cmd
│ │ │ │ └── deploy-published-packages.ts
│ │ │ ├── proc.ts
│ │ │ ├── test
│ │ │ │ ├── fixtures
│ │ │ │ │ └── changesets
│ │ │ │ │ ├── empty
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ ├── invalid-json
│ │ │ │ │ │ └── published-packages.json
│ │ │ │ │ ├── invalid-schema
│ │ │ │ │ │ └── published-packages.json
│ │ │ │ │ └── valid
│ │ │ │ │ └── published-packages.json
│ │ │ │ └── setup.ts
│ │ │ └── tsconfig.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── typescript-config
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── tools.json
│ ├── workers-lib.json
│ └── workers.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── server.json
├── tsconfig.json
├── turbo.json
└── vitest.workspace.ts
```
# Files
--------------------------------------------------------------------------------
/apps/workers-observability/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# workers-observability
## 0.4.4
### Patch Changes
- 99e2282: Move docs MCP server to use AI Search
- Updated dependencies [99e2282]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.3
### Patch Changes
- Updated dependencies [7fc3f18]
- @repo/[email protected]
## 0.4.2
### Patch Changes
- 847fc1f: Update cloudflare-oauth-handler
- Updated dependencies [f9f0bb6]
- Updated dependencies [847fc1f]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.1
### Patch Changes
- 43f493d: Update agent + modelcontextprotocol deps
- Updated dependencies [43f493d]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.0
### Minor Changes
- dee0a7b: Updated the model for docs search to embeddinggemma-300m
## 0.3.4
### Patch Changes
- 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools
- Updated dependencies [24dd872]
- @repo/[email protected]
## 0.3.3
### Patch Changes
- dffbd36: Use proper wrangler deploy in all servers so we get the name and version
## 0.3.2
### Patch Changes
- 7422e71: Update MCP sdk
- Updated dependencies [7422e71]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.1
### Patch Changes
- cc6d41f: Update agents deps & modelcontextprotocol
- Updated dependencies [1833c6d]
- Updated dependencies [cc6d41f]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.0
### Minor Changes
- f885d07: Add search docs tool to bindings and obs servers
### Patch Changes
- Updated dependencies [f885d07]
- @repo/[email protected]
## 0.2.0
### Minor Changes
- 2621557: Use new workers:read scope instead of workers:write, as these mcp servers don't require workers write permissions
### Patch Changes
- Updated dependencies [83e2d19]
- @repo/[email protected]
## 0.1.0
### Minor Changes
- 6cf52a6: Support AOT tokens
### Patch Changes
- 0fc4439: Update agents and modelcontext dependencies
- Updated dependencies [6cf52a6]
- Updated dependencies [0fc4439]
- @repo/[email protected]
- @repo/[email protected]
## 0.0.4
### Patch Changes
- 3677a18: Remove extraneous log
- Updated dependencies [3677a18]
- @repo/[email protected]
## 0.0.3
### Patch Changes
- 86c2e4f: Add API token passthrough auth
- Updated dependencies [86c2e4f]
- @repo/[email protected]
## 0.0.2
### Patch Changes
- b190e97: fix: set correct entrypoint in wrangler.jsonc
- cf3771b: chore: add suffixes to common files in apps and packages
It can be confusing switching between 16 files named 'index.ts', or 3 files named workers.ts. This change renames common files to have suffixes such as .types.ts, .api.ts, etc. to make it easier to work across files in the monorepo.
- Updated dependencies [cf3771b]
- @repo/[email protected]
- @repo/[email protected]
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/account.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import { handleAccountsList } from '../api/account.api'
import { getCloudflareClient } from '../cloudflare-api'
import { getProps } from '../get-props'
import type { CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types'
export function registerAccountTools(agent: CloudflareMcpAgent) {
// Tool to list all accounts
agent.server.tool(
'accounts_list',
'List all accounts in your Cloudflare account',
{},
{
title: 'List accounts',
annotations: {
readOnlyHint: true,
},
},
async () => {
try {
const props = getProps(agent)
const results = await handleAccountsList({
client: getCloudflareClient(props.accessToken),
})
// Sort accounts by created_on date (newest first)
const accounts = results
// order by created_on desc ( newest first )
.sort((a, b) => {
if (!a.created_on) return 1
if (!b.created_on) return -1
return new Date(b.created_on).getTime() - new Date(a.created_on).getTime()
})
// Remove fields not needed by the LLM
.map((account) => {
return {
id: account.id,
name: account.name,
created_on: account.created_on,
}
})
return {
content: [
{
type: 'text',
text: JSON.stringify({
accounts,
count: accounts.length,
}),
},
],
}
} catch (e) {
agent.server.recordError(e)
return {
content: [
{
type: 'text',
text: `Error listing accounts: ${e instanceof Error && e.message}`,
},
],
}
}
}
)
// Only register set_active_account tool when user token is provided, as it doesn't make sense to expose
// this tool for account scoped tokens, given that they're scoped to a single account
if (getProps(agent).type === 'user_token') {
const activeAccountIdParam = z
.string()
.describe(
'The accountId present in the users Cloudflare account, that should be the active accountId.'
)
agent.server.tool(
'set_active_account',
'Set active account to be used for tool calls that require accountId',
{
activeAccountIdParam,
},
{
title: 'Set active account',
annotations: {
readOnlyHint: false,
destructiveHint: false,
},
},
async (params) => {
try {
const { activeAccountIdParam: activeAccountId } = params
await agent.setActiveAccountId(activeAccountId)
return {
content: [
{
type: 'text',
text: JSON.stringify({
activeAccountId,
}),
},
],
}
} catch (e) {
agent.server.recordError(e)
return {
content: [
{
type: 'text',
text: `Error setting activeAccountID: ${e instanceof Error && e.message}`,
},
],
}
}
}
)
}
}
```
--------------------------------------------------------------------------------
/apps/sandbox-container/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# containers-mcp
## 0.2.10
### Patch Changes
- 99e2282: Move docs MCP server to use AI Search
- Updated dependencies [99e2282]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.2.9
### Patch Changes
- e659dcf: fix: use blob field for binary resources to prevent context overflow (#252)
## 0.2.8
### Patch Changes
- Updated dependencies [7fc3f18]
- @repo/[email protected]
## 0.2.7
### Patch Changes
- 847fc1f: Update cloudflare-oauth-handler
- Updated dependencies [f9f0bb6]
- Updated dependencies [847fc1f]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.2.6
### Patch Changes
- 43f493d: Update agent + modelcontextprotocol deps
- Updated dependencies [43f493d]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.2.5
### Patch Changes
- 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools
- Updated dependencies [24dd872]
- @repo/[email protected]
- @repo/[email protected]
## 0.2.4
### Patch Changes
- dffbd36: Use proper wrangler deploy in all servers so we get the name and version
## 0.2.3
### Patch Changes
- 7422e71: Update MCP sdk
- Updated dependencies [7422e71]
- @repo/[email protected]
- @repo/[email protected]
## 0.2.2
### Patch Changes
- cc6d41f: Update agents deps & modelcontextprotocol
- Updated dependencies [1833c6d]
- Updated dependencies [cc6d41f]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.2.1
### Patch Changes
- Updated dependencies [f885d07]
- @repo/[email protected]
## 0.2.0
### Minor Changes
- 2621557: Use new workers:read scope instead of workers:write, as these mcp servers don't require workers write permissions
### Patch Changes
- Updated dependencies [83e2d19]
- @repo/[email protected]
## 0.1.0
### Minor Changes
- 6cf52a6: Support AOT tokens
### Patch Changes
- 0fc4439: Update agents and modelcontext dependencies
- Updated dependencies [6cf52a6]
- Updated dependencies [0fc4439]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.0.4
### Patch Changes
- 3677a18: Remove extraneous log
- Updated dependencies [3677a18]
- @repo/[email protected]
## 0.0.3
### Patch Changes
- 86c2e4f: Add API token passthrough auth
- Updated dependencies [86c2e4f]
- @repo/[email protected]
## 0.0.2
### Patch Changes
- cf3771b: chore: add suffixes to common files in apps and packages
It can be confusing switching between 16 files named 'index.ts', or 3 files named workers.ts. This change renames common files to have suffixes such as .types.ts, .api.ts, etc. to make it easier to work across files in the monorepo.
- Updated dependencies [cf3771b]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
```
--------------------------------------------------------------------------------
/apps/sandbox-container/server/prompts.ts:
--------------------------------------------------------------------------------
```typescript
export const BASE_INSTRUCTIONS = /* markdown */ `
# Container MCP Agent
The Container MCP Agent provides access to a sandboxed container environment. This is an ephemeral container and has access to the internet.
The container is an Ubuntu 20.04 base image with the following packages installed:
- curl
- git
- net-tools
- build-essential
- nodejs
- npm
- python3
- python3-pip
If necessary, you may install additional packages.
You are given a working directory in which you can create or delete files and execute commands as described below.
If you're using python, ALWAYS use \`python3\` instead of \`python\`. ALWAYS make sure to install dependencies, as they won't be installed ahead of time.
## Resources
The primary resource in this image is the \`container_files\` resource.
This is a dynamic resource, which provides a list of files defined by \`file://{filepath}\`, where filepath is relative to the root working directory you are in.
The \`container_files_list\` allows you to list all file resources in your working directory. Content is omitted from the response of this tool.
You can read files in the container using the \`container_file_read\` tool. The contents are returned as a text blob with their associated mime type if it is text,
or a base64 encoded blob for binary files.
Directories have the special mime type \`inode/directory\`. If \`container_file_read\` is called on a directory, it returns the contents of the directory as a list of resource URIs.
AVOID manually reading or writing files using the \`container_exec\` tool. You should prefer the dedicated file resources and tools to interact with the filesystem as it is less error prone.
## Tools
To manage container lifecycle, use the \`container_initialize\` tool. If you run into errors where you can't connect to the container, attempt to restart the container with the same \`container_initialize\` tool. If that doesn't work, the system is probably overloaded.
You can execute actions in the container using the \`container_exec\` tool. By default, stdout is returned back as a string.
To write a file, use the \`container_file_write\` tool. To delete a file, use the \`container_file_delete\` tool.
The \`container_files_list\` allows you to list file resources. Content is omitted from the response of this tool and all mimeTypes are \`text/plain\` even if the file ending suggests otherwise.
If you want to get the file contents of a file resource, use \`container_file_read\`, which will return the file contents.
If after calling a tool, you receive an error that a cloudchamber instance cannot be provided, just stop attempting to answer and request that the user attempt to try again later.
If you run into issues, do not attempt to retry after 3 tries unless the user prompts you to. Instead direct the user to report an issue at: https://github.com/cloudflare/mcp-server-cloudflare
`
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/types/workers-builds.types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export type BuildDetails = z.infer<typeof BuildDetails>
export const BuildDetails = z.object({
// TODO: Maybe remove fields we don't need to reduce surface area of things we need to update
build_uuid: z.string(),
status: z.string(),
build_outcome: z.string().nullable(),
created_on: z.coerce.date(),
modified_on: z.coerce.date(),
initializing_on: z.coerce.date().nullable(),
running_on: z.coerce.date().nullable(),
stopped_on: z.coerce.date().nullable(),
trigger: z.object({
trigger_uuid: z.string(),
external_script_id: z.string(),
trigger_name: z.string(),
build_command: z.string(),
deploy_command: z.string(),
root_directory: z.string(),
branch_includes: z.array(z.string()),
branch_excludes: z.array(z.string()),
path_includes: z.array(z.string()),
path_excludes: z.array(z.string()),
build_caching_enabled: z.boolean(),
created_on: z.coerce.date(),
modified_on: z.coerce.date(),
deleted_on: z.coerce.date().nullable(),
repo_connection: z.object({
repo_connection_uuid: z.string(),
repo_id: z.string(),
repo_name: z.string(),
provider_type: z.string(),
provider_account_id: z.string(),
provider_account_name: z.string(),
created_on: z.coerce.date(),
modified_on: z.coerce.date(),
deleted_on: z.coerce.date().nullable(),
}),
}),
build_trigger_metadata: z.object({
build_trigger_source: z.string(),
branch: z.string(),
commit_hash: z.string(),
commit_message: z.string(),
author: z.string(),
build_command: z.string(),
deploy_command: z.string(),
root_directory: z.string(),
build_token_uuid: z.string(),
environment_variables: z.record(
z.string(),
z.object({
is_secret: z.boolean(),
created_on: z.coerce.date(),
value: z.string().nullable(),
})
),
repo_name: z.string(),
provider_account_name: z.string(),
provider_type: z.string(),
}),
pull_request: z.unknown(),
})
/**
* GET /builds/workers/:external_script_id/builds
*/
export type ListBuildsByScriptResult = z.infer<typeof ListBuildsByScriptResult>
export const ListBuildsByScriptResult = z.array(BuildDetails)
export type ListBuildsByScriptResultInfo = z.infer<typeof ListBuildsByScriptResultInfo>
export const ListBuildsByScriptResultInfo = z.object({
next_page: z.boolean(),
page: z.number(),
per_page: z.number(),
count: z.number(),
total_count: z.number(),
total_pages: z.number(),
})
export type GetBuildResult = z.infer<typeof GetBuildResult>
export const GetBuildResult = BuildDetails
export type LogLine = z.infer<typeof LogLine>
export const LogLine = z.tuple([
z.coerce.date().describe('line timestamp'),
z.string().describe('line message'),
])
export type GetBuildLogsResult = z.infer<typeof GetBuildLogsResult>
export const GetBuildLogsResult = z.object({
cursor: z.string().optional().describe('pagination cursor'),
truncated: z.boolean(),
lines: z.array(LogLine),
})
```
--------------------------------------------------------------------------------
/apps/demo-day/frontend/public/stripe.svg:
--------------------------------------------------------------------------------
```
<svg width="498" height="498" viewBox="0 0 498 498" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_1171_29)">
<rect x="5" y="1" width="488" height="488" rx="77" stroke="white" stroke-width="2" shape-rendering="crispEdges"/>
</g>
<g filter="url(#filter1_d_1171_29)">
<path d="M254.68 96.082C281.995 96.0821 308.964 100.336 336.251 111.109V187.169C311.044 173.904 279.661 166.483 254.68 166.482C245.797 166.482 238.504 167.735 233.402 170.75C228.213 173.817 225.379 178.659 225.379 185.499C225.379 190.498 227.517 194.558 231.143 197.993C234.74 201.401 239.824 204.219 245.808 206.81C251.8 209.404 258.78 211.804 266.211 214.338C273.655 216.876 281.566 219.553 289.477 222.711C305.304 229.03 321.028 237.241 332.817 249.986C344.395 262.503 352.235 279.455 352.556 303.463L352.565 304.611C352.565 334.175 340.793 357.4 320.487 373.259C300.152 389.14 271.174 397.695 236.7 397.695C208.383 397.695 177.442 392.12 146.805 379.016V301.951C174.624 316.941 209.332 327.962 236.7 327.962C246.077 327.962 254.276 326.71 260.162 323.414C263.121 321.757 265.51 319.575 267.153 316.771C268.797 313.966 269.663 310.595 269.663 306.61C269.663 296.03 261.196 289.259 249.324 283.608C243.345 280.763 236.384 278.146 228.973 275.449C221.547 272.748 213.664 269.965 205.778 266.772C190.001 260.386 174.342 252.41 162.628 240.368C150.947 228.36 143.143 212.261 143.143 189.502C143.143 160.251 154.263 136.938 173.701 120.919C193.162 104.881 221.05 96.082 254.68 96.082Z" stroke="white" stroke-width="2" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_1171_29" x="0" y="0" width="498" height="498" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1171_29"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1171_29" result="shape"/>
</filter>
<filter id="filter1_d_1171_29" x="138.143" y="95.082" width="219.422" height="311.613" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1171_29"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1171_29" result="shape"/>
</filter>
</defs>
</svg>
```
--------------------------------------------------------------------------------
/packages/eval-tools/src/test-models.ts:
--------------------------------------------------------------------------------
```typescript
import { createAnthropic } from '@ai-sdk/anthropic'
import { AnthropicMessagesModelId } from '@ai-sdk/anthropic/internal'
import { createGoogleGenerativeAI } from '@ai-sdk/google'
import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal'
import { createOpenAI } from '@ai-sdk/openai'
import { OpenAIChatModelId } from '@ai-sdk/openai/internal'
import { createAiGateway } from 'ai-gateway-provider'
import { env } from 'cloudflare:test'
import { describe } from 'vitest'
import { createWorkersAI } from 'workers-ai-provider'
export const factualityModel = getOpenAiModel('gpt-4o')
type value2key<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T]
type AiTextGenerationModels = Exclude<
value2key<AiModels, BaseAiTextGeneration>,
value2key<AiModels, BaseAiTextToImage>
>
function getOpenAiModel(modelName: OpenAIChatModelId) {
if (!env.CLOUDFLARE_ACCOUNT_ID || !env.AI_GATEWAY_ID || !env.AI_GATEWAY_TOKEN) {
throw new Error('No AI gateway credentials set!')
}
const aigateway = createAiGateway({
accountId: env.CLOUDFLARE_ACCOUNT_ID,
gateway: env.AI_GATEWAY_ID,
apiKey: env.AI_GATEWAY_TOKEN,
})
const ai = createOpenAI({
apiKey: '',
})
const model = aigateway([ai(modelName)])
return { modelName, model, ai }
}
function getAnthropicModel(modelName: AnthropicMessagesModelId) {
const aigateway = createAiGateway({
accountId: env.CLOUDFLARE_ACCOUNT_ID,
gateway: env.AI_GATEWAY_ID,
apiKey: env.AI_GATEWAY_TOKEN,
})
const ai = createAnthropic({
apiKey: '',
})
const model = aigateway([ai(modelName)])
return { modelName, model, ai }
}
function getGeminiModel(modelName: GoogleGenerativeAILanguageModel['modelId']) {
if (!env.CLOUDFLARE_ACCOUNT_ID || !env.AI_GATEWAY_ID || !env.AI_GATEWAY_TOKEN) {
throw new Error('No AI gateway credentials set!')
}
const aigateway = createAiGateway({
accountId: env.CLOUDFLARE_ACCOUNT_ID,
gateway: env.AI_GATEWAY_ID,
apiKey: env.AI_GATEWAY_TOKEN,
})
const ai = createGoogleGenerativeAI({ apiKey: '' })
const model = aigateway([ai(modelName)])
return { modelName, model, ai }
}
function getWorkersAiModel(modelName: AiTextGenerationModels) {
if (!env.AI) {
throw new Error('No AI binding provided!')
}
const ai = createWorkersAI({ binding: env.AI })
const model = ai(modelName)
return { modelName, model, ai }
}
export const eachModel = describe.each([
getOpenAiModel('gpt-4o'),
getOpenAiModel('gpt-4o-mini'),
// getAnthropicModel('claude-3-5-sonnet-20241022'), TODO: The evals pass with anthropic, but our rate limit is so low with AI wholesaling that we can't use it in CI because it's impossible to get a complete run with the current limits
getGeminiModel('gemini-2.0-flash'),
// llama 3 is somewhat inconsistent
//getWorkersAiModel("@cf/meta/llama-3.3-70b-instruct-fp8-fast")
// Currently llama 4 is having issues with tool calling
//getWorkersAiModel("@cf/meta/llama-4-scout-17b-16e-instruct")
])
```
--------------------------------------------------------------------------------
/apps/docs-ai-search/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# docs-ai-search
## 0.4.4
### Patch Changes
- 99e2282: Move docs MCP server to use AI Search
- Updated dependencies [99e2282]
- @repo/[email protected]
- @repo/[email protected]
## 0.5.0
### Minor Changes
- Changed backend from Vectorize to AI Search for documentation search
- Now uses Cloudflare AI Search (AutoRAG) for contextual search of the Cloudflare Developer Documentation
- Maintains full backward compatibility - same XML response format and tool interface
- Package renamed from `docs-vectorize` to `docs-ai-search` to reflect the new backend
## 0.4.3
### Patch Changes
- Updated dependencies [7fc3f18]
- @repo/[email protected]
## 0.4.2
### Patch Changes
- 847fc1f: Update cloudflare-oauth-handler
- Updated dependencies [f9f0bb6]
- Updated dependencies [847fc1f]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.1
### Patch Changes
- 43f493d: Update agent + modelcontextprotocol deps
- Updated dependencies [43f493d]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.0
### Minor Changes
- dee0a7b: Updated the model for docs search to embeddinggemma-300m
## 0.3.3
### Patch Changes
- 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools
- Updated dependencies [24dd872]
- @repo/[email protected]
## 0.3.2
### Patch Changes
- 7422e71: Update MCP sdk
- Updated dependencies [7422e71]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.1
### Patch Changes
- cc6d41f: Update agents deps & modelcontextprotocol
- Updated dependencies [1833c6d]
- Updated dependencies [cc6d41f]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.0
### Minor Changes
- f885d07: Add search docs tool to bindings and obs servers
### Patch Changes
- Updated dependencies [f885d07]
- @repo/[email protected]
## 0.2.1
### Patch Changes
- Updated dependencies [83e2d19]
- @repo/[email protected]
## 0.2.0
### Minor Changes
- 89bfaf4: feat: add Pages to Workers migration guide to docs-vectorize MCP server
## 0.1.0
### Minor Changes
- 6cf52a6: Support AOT tokens
### Patch Changes
- 0fc4439: Update agents and modelcontext dependencies
- Updated dependencies [6cf52a6]
- Updated dependencies [0fc4439]
- @repo/[email protected]
- @repo/[email protected]
## 0.0.4
### Patch Changes
- 3677a18: Remove extraneous log
- Updated dependencies [3677a18]
- @repo/[email protected]
## 0.0.3
### Patch Changes
- Updated dependencies [86c2e4f]
- @repo/[email protected]
## 0.0.2
### Patch Changes
- cf3771b: chore: add suffixes to common files in apps and packages
It can be confusing switching between 16 files named 'index.ts', or 3 files named workers.ts. This change renames common files to have suffixes such as .types.ts, .api.ts, etc. to make it easier to work across files in the monorepo.
- Updated dependencies [cf3771b]
- @repo/[email protected]
- @repo/[email protected]
```
--------------------------------------------------------------------------------
/apps/workers-bindings/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
# workers-bindings
## 0.4.4
### Patch Changes
- 99e2282: Move docs MCP server to use AI Search
- Updated dependencies [99e2282]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.3
### Patch Changes
- Updated dependencies [7fc3f18]
- @repo/[email protected]
## 0.4.2
### Patch Changes
- 847fc1f: Update cloudflare-oauth-handler
- Updated dependencies [f9f0bb6]
- Updated dependencies [847fc1f]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.1
### Patch Changes
- 43f493d: Update agent + modelcontextprotocol deps
- Updated dependencies [43f493d]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.4.0
### Minor Changes
- dee0a7b: Updated the model for docs search to embeddinggemma-300m
## 0.3.4
### Patch Changes
- 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools
- Updated dependencies [24dd872]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.3
### Patch Changes
- dffbd36: Use proper wrangler deploy in all servers so we get the name and version
## 0.3.2
### Patch Changes
- 7422e71: Update MCP sdk
- Updated dependencies [7422e71]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.1
### Patch Changes
- cc6d41f: Update agents deps & modelcontextprotocol
- Updated dependencies [1833c6d]
- Updated dependencies [cc6d41f]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.3.0
### Minor Changes
- f885d07: Add search docs tool to bindings and obs servers
### Patch Changes
- Updated dependencies [f885d07]
- @repo/[email protected]
## 0.2.0
### Minor Changes
- 2621557: Use new workers:read scope instead of workers:write, as these mcp servers don't require workers write permissions
### Patch Changes
- Updated dependencies [83e2d19]
- @repo/[email protected]
## 0.1.0
### Minor Changes
- 6cf52a6: Support AOT tokens
### Patch Changes
- 0fc4439: Update agents and modelcontext dependencies
- Updated dependencies [6cf52a6]
- Updated dependencies [0fc4439]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
## 0.0.3
### Patch Changes
- 3677a18: Remove extraneous log
- Updated dependencies [3677a18]
- @repo/[email protected]
## 0.0.2
### Patch Changes
- 86c2e4f: Add API token passthrough auth
- Updated dependencies [86c2e4f]
- @repo/[email protected]
## 0.0.1
### Patch Changes
- cf3771b: chore: add suffixes to common files in apps and packages
It can be confusing switching between 16 files named 'index.ts', or 3 files named workers.ts. This change renames common files to have suffixes such as .types.ts, .api.ts, etc. to make it easier to work across files in the monorepo.
- Updated dependencies [cf3771b]
- @repo/[email protected]
- @repo/[email protected]
- @repo/[email protected]
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/types/cf1-integrations.types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
const Vendor = z.object({
id: z.string(),
name: z.string(),
display_name: z.string(),
description: z.string().nullable(),
logo: z.string().nullable(),
static_logo: z.string().nullable(),
})
const Policy = z.object({
id: z.string(),
name: z.string(),
permissions: z.array(z.string()),
link: z.string().nullable(),
dlp_enabled: z.boolean(),
})
// Base Integration schema
export const Integration = z.object({
id: z.string(),
name: z.string(),
status: z.enum(['Healthy', 'Unhealthy', 'Initializing', 'Paused']),
upgradable: z.boolean(),
permissions: z.array(z.string()),
vendor: Vendor,
policy: Policy,
created: z.string(),
updated: z.string(),
credentials_expiry: z.string().nullable(),
last_hydrated: z.string().nullable(),
})
// Schema for output: a single integration
export const IntegrationResponse = Integration
export type zReturnedIntegrationResult = z.infer<typeof IntegrationResponse>
// Schema for output: multiple integrations
export const IntegrationsResponse = z.array(Integration)
export type zReturnedIntegrationsResult = z.infer<typeof IntegrationsResponse>
export const AssetCategory = z.object({
id: z.string().uuid(),
type: z.string(),
vendor: z.string(),
service: z.string().nullable(),
})
export const AssetDetail = z.object({
id: z.string().uuid(),
external_id: z.string(),
name: z.string(),
link: z.string().nullable(),
fields: z.array(
z.object({
link: z.string().nullable(),
name: z.string(),
value: z.any(),
})
),
category: AssetCategory,
integration: Integration,
})
export type zReturnedAssetResult = z.infer<typeof AssetDetail>
export const AssetsResponse = z.array(AssetDetail)
export type zReturnedAssetsResult = z.infer<typeof AssetsResponse>
export const AssetCategoriesResponse = z.array(AssetCategory)
export type zReturnedAssetCategoriesResult = z.infer<typeof AssetCategoriesResponse>
export const assetCategoryTypeParam = z
.enum([
'Account',
'Alert',
'App',
'Authentication Method',
'Bucket',
'Bucket Iam Permission',
'Bucket Permission',
'Calendar',
'Certificate',
'Channel',
'Commit',
'Content',
'Credential',
'Domain',
'Drive',
'Environment',
'Factor',
'File',
'File Permission',
'Folder',
'Group',
'Incident',
'Instance',
'Issue',
'Label',
'Meeting',
'Message',
'Message Rule',
'Namespace',
'Organization',
'Package',
'Pipeline',
'Project',
'Report',
'Repository',
'Risky User',
'Role',
'Server',
'Site',
'Space',
'Submodule',
'Third Party User',
'User',
'User No Mfa',
'Variable',
'Webhook',
'Workspace',
])
.optional()
.describe('Type of cloud resource or service category')
export const assetCategoryVendorParam = z
.enum([
'AWS',
'Bitbucket',
'Box',
'Confluence',
'Dropbox',
'GitHub',
'Google Cloud Platform',
'Google Workspace',
'Jira',
'Microsoft',
'Microsoft Azure',
'Okta',
'Salesforce',
'ServiceNow',
'Slack',
'Workday',
'Zoom',
])
.describe('Vendor of the cloud service or resource')
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { isPromise } from 'node:util/types'
import { type ServerOptions } from '@modelcontextprotocol/sdk/server/index.js'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { type ZodRawShape } from 'zod'
import { MetricsTracker, SessionStart, ToolCall } from '../../mcp-observability/src'
import { McpError } from './mcp-error'
import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'
import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js'
import type { SentryClient } from './sentry'
export class CloudflareMCPServer extends McpServer {
private metrics
private sentry?: SentryClient
constructor({
userId,
wae,
serverInfo,
options,
sentry,
}: {
userId?: string
wae: AnalyticsEngineDataset
serverInfo: {
[x: string]: unknown
name: string
version: string
}
options?: ServerOptions
sentry?: SentryClient
}) {
super(serverInfo, options)
this.metrics = new MetricsTracker(wae, serverInfo)
this.sentry = sentry
this.server.oninitialized = () => {
const clientInfo = this.server.getClientVersion()
const clientCapabilities = this.server.getClientCapabilities()
this.metrics.logEvent(
new SessionStart({
userId,
clientInfo,
clientCapabilities,
})
)
}
this.server.onerror = (e) => {
this.recordError(e)
}
const _tool = this.tool.bind(this)
this.tool = (name: string, ...rest: unknown[]): ReturnType<typeof this.tool> => {
const toolCb = rest[rest.length - 1] as ToolCallback<ZodRawShape | undefined>
const replacementToolCb: ToolCallback<ZodRawShape | undefined> = (arg1, arg2) => {
const toolCall = toolCb(
arg1 as { [x: string]: any } & RequestHandlerExtra<ServerRequest, ServerNotification>,
arg2
)
// There are 4 cases to track:
try {
if (isPromise(toolCall)) {
return toolCall
.then((r: any) => {
// promise succeeds
this.metrics.logEvent(
new ToolCall({
toolName: name,
userId,
})
)
return r
})
.catch((e: unknown) => {
// promise throws
this.trackToolCallError(e, name, userId)
throw e
})
} else {
// non-promise succeeds
this.metrics.logEvent(
new ToolCall({
toolName: name,
userId,
})
)
return toolCall
}
} catch (e: unknown) {
// non-promise throws
this.trackToolCallError(e, name, userId)
throw e
}
}
rest[rest.length - 1] = replacementToolCb
// @ts-ignore
return _tool(name, ...rest)
}
}
private trackToolCallError(e: unknown, toolName: string, userId?: string) {
// placeholder error code
let errorCode = -1
if (e instanceof McpError) {
errorCode = e.code
}
this.metrics.logEvent(
new ToolCall({
toolName,
userId: userId,
errorCode: errorCode,
})
)
}
public recordError(e: unknown) {
this.sentry?.recordError(e)
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/durable-kv-store.ts:
--------------------------------------------------------------------------------
```typescript
import type { ZodSchema } from 'zod'
export type DurableKVStorageKeys = { [key: string]: ZodSchema }
/**
* DurableKVStore is a type-safe key/value store backed by Durable Object storage.
*
* @example
*
* ```ts
* export class MyDurableObject extends DurableObject<Bindings> {
* readonly kv
* constructor(
* readonly state: DurableObjectState,
* env: Bindings
* ) {
* super(state, env)
* this.kv = new DurableKVStore({
* state,
* prefix: 'meta',
* keys: {
* // Each key has a matching Zod schema enforcing what's stored
* date_key: z.coerce.date(),
* // While empty keys will always return null, adding
* // `nullable()` allows us to explicitly set it to null
* string_key: z.string().nullable(),
* number_key: z.number(),
* } as const satisfies StorageKeys,
* })
* }
*
* async example(): Promise<void> {
* await this.kv.get('number_key') // -> null
* this.kv.put('number_key', 5)
* await this.kv.get('number_key') // -> 5
* }
* }
* ```
*/
export class DurableKVStore<T extends DurableKVStorageKeys> {
private readonly prefix: string
private readonly keys: T
private readonly state: DurableObjectState
constructor({ state, prefix, keys }: { state: DurableObjectState; prefix: string; keys: T }) {
this.state = state
this.prefix = prefix
this.keys = keys
}
/** Add the prefix to a key (used for get/put operations) */
private addPrefix<K extends keyof T>(key: K): string {
if (this.prefix.length > 0) {
return `${this.prefix}/${key.toString()}`
}
return key.toString()
}
/**
* Get a value from KV storage. Returns `null` if the value
* is not set (or if it's explicitly set to `null`)
*/
async get<K extends keyof T>(key: K): Promise<T[K]['_output'] | null>
/**
* Get a value from KV storage or return the provided
* default if they value in storage is unset (undefined).
* The default value must match the schema for the given key.
*
* If defaultValue is explicitly set to undefined, it will still return null (avoid this).
*
* If the value in storage is null then this will return null instead of the default.
*/
async get<K extends keyof T>(key: K, defaultValue: T[K]['_output']): Promise<T[K]['_output']>
async get<K extends keyof T>(
key: K,
defaultValue?: T[K]['_output']
): Promise<T[K]['_output'] | null> {
const schema = this.keys[key]
if (schema === undefined) {
throw new TypeError(`key ${key.toString()} has no matching schema`)
}
const res = await this.state.storage.get(this.addPrefix(key))
if (res === undefined) {
if (defaultValue !== undefined) {
return schema.parse(defaultValue)
}
return null
}
return schema.parse(res)
}
/** Write value to KV storage */
put<K extends keyof T>(key: K, value: T[K]['_input']): void {
const schema = this.keys[key]
if (schema === undefined) {
throw new TypeError(`key ${key.toString()} has no matching schema`)
}
const parsedValue = schema.parse(value)
void this.state.storage.put(this.addPrefix(key), parsedValue)
}
/**
* Delete value in KV storage. **Does not need to be awaited**
*
* @returns `true` if a value was deleted, or `false` if it did not.
*/
async delete<K extends keyof T>(key: K): Promise<boolean> {
return this.state.storage.delete(this.addPrefix(key))
}
}
```
--------------------------------------------------------------------------------
/apps/dns-analytics/src/tools/dex-analytics.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
import { getProps } from '@repo/mcp-common/src/get-props'
import type { AccountGetParams } from 'cloudflare/resources/accounts/accounts.mjs'
import type { ReportGetParams } from 'cloudflare/resources/dns/analytics.mjs'
import type { ZoneGetParams } from 'cloudflare/resources/dns/settings.mjs'
import type { DNSAnalyticsMCP } from '../dns-analytics.app'
function getStartDate(days: number) {
const today = new Date()
const start_date = new Date(today.setDate(today.getDate() - days))
return start_date.toISOString()
}
export function registerAnalyticTools(agent: DNSAnalyticsMCP) {
// Register DNS Report tool
agent.server.tool(
'dns_report',
'Fetch the DNS Report for a given zone since a date',
{
zone: z.string(),
days: z.number(),
},
async ({ zone, days }) => {
try {
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const start_date = getStartDate(days)
const params: ReportGetParams = {
zone_id: zone,
metrics: 'responseTimeAvg,queryCount,uncachedCount,staleCount',
dimensions: 'responseCode,responseCached',
since: start_date,
}
const result = await client.dns.analytics.reports.get(params)
return {
content: [
{
type: 'text',
text: JSON.stringify({
result,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error fetching DNS report: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
// Register Account DNS Settings display tool
agent.server.tool(
'show_account_dns_settings',
'Show DNS settings for current account',
async () => {
try {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const params: AccountGetParams = {
account_id: accountId,
}
const result = await client.dns.settings.account.get(params)
return {
content: [
{
type: 'text',
text: JSON.stringify({
result,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error fetching DNS report: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
// Register Zone DNS Settings display tool
agent.server.tool(
'show_zone_dns_settings',
'Show DNS settings for a zone',
{
zone: z.string(),
},
async ({ zone }) => {
try {
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const params: ZoneGetParams = {
zone_id: zone,
}
const result = await client.dns.settings.zone.get(params)
return {
content: [
{
type: 'text',
text: JSON.stringify({
result,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error fetching DNS report: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/zone.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import { handleZonesList } from '../api/zone.api'
import { getCloudflareClient } from '../cloudflare-api'
import { getProps } from '../get-props'
import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types'
export function registerZoneTools(agent: CloudflareMcpAgent) {
// Tool to list all zones under an account
agent.server.tool(
'zones_list',
'List all zones under a Cloudflare account',
{
name: z.string().optional().describe('Filter zones by name'),
status: z
.string()
.optional()
.describe(
'Filter zones by status (active, pending, initializing, moved, deleted, deactivated, read only)'
),
page: z.number().min(1).default(1).describe('Page number for pagination'),
perPage: z.number().min(5).max(1000).default(50).describe('Number of zones per page'),
order: z
.string()
.default('name')
.describe('Field to order results by (name, status, account_name)'),
direction: z
.enum(['asc', 'desc'])
.default('desc')
.describe('Direction to order results (asc, desc)'),
},
{
title: 'List zones',
annotations: {
readOnlyHint: true,
destructiveHint: false,
},
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const { page = 1, perPage = 50 } = params
const zones = await handleZonesList({
client: getCloudflareClient(props.accessToken),
accountId,
...params,
})
return {
content: [
{
type: 'text',
text: JSON.stringify({
zones,
count: zones.length,
page,
perPage,
accountId,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error listing zones: ${error instanceof Error ? error.message : String(error)}`,
},
],
}
}
}
)
// Tool to get zone details by ID
agent.server.tool(
'zone_details',
'Get details for a specific Cloudflare zone',
{
zoneId: z.string().describe('The ID of the zone to get details for'),
},
{
title: 'Get zone details',
annotations: {
readOnlyHint: true,
destructiveHint: false,
},
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const { zoneId } = params
const client = getCloudflareClient(props.accessToken)
// Use the zones.get method to fetch a specific zone
const response = await client.zones.get({ zone_id: zoneId })
return {
content: [
{
type: 'text',
text: JSON.stringify({
zone: response,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error fetching zone details: ${error instanceof Error ? error.message : String(error)}`,
},
],
}
}
}
)
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/types/hyperdrive.types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import type { ConfigCreateParams } from 'cloudflare/resources/hyperdrive/configs.mjs'
// --- Base Field Schemas ---
/** Zod schema for a Hyperdrive config ID. */
export const HyperdriveConfigIdSchema = z
.string()
.describe('The ID of the Hyperdrive configuration')
/** Zod schema for a Hyperdrive config name. */
export const HyperdriveConfigNameSchema: z.ZodType<ConfigCreateParams['name']> = z
.string()
.min(1)
.max(64)
.regex(/^[a-zA-Z0-9_-]+$/)
.describe('The name of the Hyperdrive configuration (alphanumeric, underscore, hyphen)')
// --- Origin Field Schemas ---
/** Zod schema for the origin database name. */
export const HyperdriveOriginDatabaseSchema: z.ZodType<
ConfigCreateParams.PublicDatabase['database']
> = z.string().describe('The database name')
/** Zod schema for the origin database host. */
export const HyperdriveOriginHostSchema: z.ZodType<ConfigCreateParams.PublicDatabase['host']> = z
.string()
.describe('The database host address')
/** Zod schema for the origin database port. */
export const HyperdriveOriginPortSchema: z.ZodType<ConfigCreateParams.PublicDatabase['port']> = z
.number()
.int()
.min(1)
.max(65535)
.describe('The database port')
/** Zod schema for the origin database scheme. */
export const HyperdriveOriginSchemeSchema: z.ZodType<ConfigCreateParams.PublicDatabase['scheme']> =
z.enum(['postgresql']).describe('The database protocol')
/** Zod schema for the origin database user. */
export const HyperdriveOriginUserSchema: z.ZodType<ConfigCreateParams.PublicDatabase['user']> = z
.string()
.describe('The database user')
/** Zod schema for the origin database password. */
export const HyperdriveOriginPasswordSchema: z.ZodType<
ConfigCreateParams.PublicDatabase['password']
> = z.string().describe('The database password')
// --- Caching Field Schemas (Referencing ConfigCreateParams.HyperdriveHyperdriveCachingEnabled) ---
/** Zod schema for disabling caching. */
export const HyperdriveCachingDisabledSchema: z.ZodType<
ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['disabled']
> = z.boolean().optional().describe('Whether caching is disabled')
/** Zod schema for the maximum cache age. */
export const HyperdriveCachingMaxAgeSchema: z.ZodType<
ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['max_age']
> = z.number().int().min(1).optional().describe('Maximum cache age in seconds')
/** Zod schema for the stale while revalidate duration. */
export const HyperdriveCachingStaleWhileRevalidateSchema: z.ZodType<
ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['stale_while_revalidate']
> = z.number().int().min(1).optional().describe('Stale while revalidate duration in seconds')
// --- List Parameter Schemas (Cannot directly type against SDK ConfigListParams which only has account_id) ---
/** Zod schema for the list page number. */
export const HyperdriveListParamPageSchema = z
.number()
.int()
.positive()
.optional()
.describe('Page number of results')
/** Zod schema for the list results per page. */
export const HyperdriveListParamPerPageSchema = z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe('Number of results per page')
/** Zod schema for the list order field. */
export const HyperdriveListParamOrderSchema = z
.enum(['id', 'name'])
.optional()
.describe('Field to order by')
/** Zod schema for the list order direction. */
export const HyperdriveListParamDirectionSchema = z
.enum(['asc', 'desc'])
.optional()
.describe('Direction to order')
// --- Tool Parameter Schemas ---
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/format.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from 'vitest'
import { fmt } from './format'
describe('fmt', () => {
describe('trim()', () => {
it('should return an empty string for an empty input', () => {
expect(fmt.trim('')).toBe('')
})
it('should trim leading and trailing spaces', () => {
expect(fmt.trim(' hello ')).toBe('hello')
})
it('should trim leading and trailing newlines', () => {
expect(fmt.trim('\n\nhello\n\n')).toBe('hello')
})
it('should trim leading/trailing spaces and newlines from each line but not remove empty lines', () => {
const input = `
line1
line2
line3
`
const expected = `line1
line2
line3`
expect(fmt.trim(input)).toBe(expected)
})
it('should handle a string that is already trimmed', () => {
expect(fmt.trim('hello\nworld')).toBe('hello\nworld')
})
it('should handle a string with only spaces', () => {
expect(fmt.trim(' ')).toBe('')
})
it('should handle a string with only newlines', () => {
expect(fmt.trim('\n\n\n')).toBe('')
})
it('should preserve empty lines from the middle', () => {
expect(fmt.trim('hello\n\nworld')).toBe('hello\n\nworld')
})
})
describe('oneLine()', () => {
it('should return an empty string for an empty input', () => {
expect(fmt.oneLine('')).toBe('')
})
it('should convert a multi-line string to a single line', () => {
expect(fmt.oneLine('hello\nworld')).toBe('hello world')
})
it('should trim leading/trailing spaces and newlines before joining', () => {
expect(fmt.oneLine(' hello \n world \n')).toBe('hello world')
})
it('should remove empty lines before joining', () => {
expect(fmt.oneLine('hello\n\nworld')).toBe('hello world')
})
it('should handle a string that is already a single line', () => {
expect(fmt.oneLine('hello world')).toBe('hello world')
})
it('should handle a string with only spaces and newlines', () => {
expect(fmt.oneLine(' \n \n ')).toBe('')
})
})
describe('asTSV()', () => {
it('should convert an empty array to an empty string', async () => {
expect(await fmt.asTSV([])).toBe('')
})
it('should convert an array of one object to a TSV string', async () => {
const data = [{ a: 1, b: 'hello' }]
expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello')
})
it('should convert an array of multiple objects to a TSV string', async () => {
const data = [
{ a: 1, b: 'hello' },
{ a: 2, b: 'world' },
]
expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\tworld')
})
it('should handle objects with different keys (using keys from the first object as headers)', async () => {
const data = [
{ a: 1, b: 'hello' },
{ a: 2, c: 'world' },
]
expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\t')
expect(await fmt.asTSV(data)).toMatchInlineSnapshot(`
"a b
1 hello
2 "
`)
})
it('should handle values with tabs and newlines (fast-csv should quote them)', async () => {
const data = [{ name: 'John\tDoe', description: 'Line1\nLine2' }]
expect(await fmt.asTSV(data)).toBe('name\tdescription\n"John\tDoe"\t"Line1\nLine2"')
expect(await fmt.asTSV(data)).toMatchInlineSnapshot(`
"name description
"John Doe" "Line1
Line2""
`)
})
it('should handle values with quotes (fast-csv should escape them)', async () => {
const data = [{ name: 'James "Jim" Raynor' }]
expect(await fmt.asTSV(data)).toBe('name\n"James ""Jim"" Raynor"')
expect(await fmt.asTSV(data)).toMatchInlineSnapshot(`
"name
"James ""Jim"" Raynor""
`)
})
})
})
```
--------------------------------------------------------------------------------
/apps/logpush/src/tools/logpush.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api'
import { getProps } from '@repo/mcp-common/src/get-props'
import type { LogsMCP } from '../logpush.app'
const zJobIdentifier = z.number().int().min(1).optional().describe('Unique id of the job.')
const zEnabled = z.boolean().optional().describe('Flag that indicates if the job is enabled.')
const zName = z
.string()
.regex(/^[a-zA-Z0-9\-.]*$/)
.max(512)
.nullable()
.optional()
.describe('Optional human readable job name. Not unique.')
const zDataset = z
.string()
.regex(/^[a-zA-Z0-9_-]*$/)
.max(256)
.nullable()
.optional()
.describe('Name of the dataset.')
const zLastComplete = z
.string()
.datetime()
.nullable()
.optional()
.describe('Records the last time for which logs have been successfully pushed.')
const zLastError = z
.string()
.datetime()
.nullable()
.optional()
.describe('Records the last time the job failed.')
const zErrorMessage = z
.string()
.nullable()
.optional()
.describe('If not null, the job is currently failing.')
export const zLogpushJob = z
.object({
id: zJobIdentifier,
enabled: zEnabled,
name: zName,
dataset: zDataset,
last_complete: zLastComplete,
last_error: zLastError,
error_message: zErrorMessage,
})
.nullable()
.optional()
const zApiResponseCommon = z.object({
success: z.literal(true),
errors: z.array(z.object({ message: z.string() })).optional(),
})
const zLogPushJobResults = z.array(zLogpushJob).optional()
// The complete schema for zone_logpush_job_response_collection
export const zLogpushJobResponseCollection = zApiResponseCommon.extend({
result: zLogPushJobResults,
})
/**
* Fetches available telemetry keys for a specified Cloudflare Worker
* @param accountId Cloudflare account ID
* @param apiToken Cloudflare API token
* @returns List of telemetry keys available for the worker
*/
export async function handleGetAccountLogPushJobs(
accountId: string,
apiToken: string
): Promise<z.infer<typeof zLogPushJobResults>> {
// Call the Public API
const data = await fetchCloudflareApi({
endpoint: `/logpush/jobs`,
accountId,
apiToken,
responseSchema: zLogpushJobResponseCollection,
options: {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'portal-version': '2',
},
},
})
const res = data as z.infer<typeof zLogpushJobResponseCollection>
return (res.result ?? []).slice(0, 100)
}
/**
* Registers the logs analysis tool with the MCP server
* @param server The MCP server instance
* @param accountId Cloudflare account ID
* @param apiToken Cloudflare API token
*/
export function registerLogsTools(agent: LogsMCP) {
// Register the worker logs analysis tool by worker name
agent.server.tool(
'logpush_jobs_by_account_id',
`All Logpush jobs by Account ID.
You should use this tool when:
- You have questions or wish to request information about their Cloudflare Logpush jobs by account
- You want a condensed version for the output results of your account's Cloudflare Logpush job
This tool returns at most the first 100 jobs.
`,
{},
async () => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const result = await handleGetAccountLogPushJobs(accountId, props.accessToken)
return {
content: [
{
type: 'text',
text: JSON.stringify({
result,
}),
},
],
}
} catch (e) {
agent.server.recordError(e)
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Error analyzing logpush jobs: ${e instanceof Error && e.message}`,
}),
},
],
}
}
}
)
}
```
--------------------------------------------------------------------------------
/apps/browser-rendering/src/tools/browser.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
import { getProps } from '@repo/mcp-common/src/get-props'
import type { BrowserMCP } from '../browser.app'
export function registerBrowserTools(agent: BrowserMCP) {
agent.server.tool(
'get_url_html_content',
'Get page HTML content',
{
url: z.string().url(),
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const r = await client.browserRendering.content.create({
account_id: accountId,
url: params.url,
})
return {
content: [
{
type: 'text',
text: JSON.stringify({
result: r,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error getting page html: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
agent.server.tool(
'get_url_markdown',
'Get page converted into Markdown',
{
url: z.string().url(),
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const r = (await client.post(`/accounts/${accountId}/browser-rendering/markdown`, {
body: {
url: params.url,
},
})) as { result: string }
return {
content: [
{
type: 'text',
text: JSON.stringify({
result: r.result,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error getting page in markdown: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
agent.server.tool(
'get_url_screenshot',
'Get page screenshot',
{
url: z.string().url(),
viewport: z
.object({
height: z.number().default(600),
width: z.number().default(800),
})
.optional(),
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
// Cf client appears to be broken, so we use the raw API instead.
// const client = getCloudflareClient(props.accessToken)
// const r = await client.browserRendering.screenshot.create({
// account_id: accountId,
// url: params.url,
// viewport: params.viewport,
// })
const r = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/screenshot`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${props.accessToken}`,
},
body: JSON.stringify({
url: params.url,
viewport: params.viewport,
}),
}
)
const arrayBuffer = await r.arrayBuffer()
const base64Image = Buffer.from(arrayBuffer).toString('base64')
return {
content: [
{
type: 'image',
mimeType: 'image/png',
data: base64Image,
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error getting page in markdown: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
}
```
--------------------------------------------------------------------------------
/apps/cloudflare-one-casb/src/cf1-casb.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerIntegrationsTools } from './tools/integrations.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './cf1-casb.context'
export { UserDetails }
const env = getEnv<Env>()
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class CASBMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
registerIntegrationsTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const CloudflareOneCasbScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'teams:read': 'See Cloudflare One Resources',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(CASBMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': CASBMCP.serve('/mcp'),
'/sse': CASBMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/autorag/src/autorag.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerAutoRAGTools } from './tools/autorag.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './autorag.context'
const env = getEnv<Env>()
export { UserDetails }
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class AutoRAGMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
// Register Cloudflare Log Push tools
registerAutoRAGTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const LogPushScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'rag:write': 'Grants write level access to AutoRag.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(AutoRAGMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': AutoRAGMCP.serve('/mcp'),
'/sse': AutoRAGMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/browser-rendering/src/browser.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerBrowserTools } from './tools/browser.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './browser.context'
const env = getEnv<Env>()
export { UserDetails }
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class BrowserMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
// Register Cloudflare Log Push tools
registerBrowserTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const BrowserScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'browser:write': 'Grants write level access to Browser Rendering.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(BrowserMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': BrowserMCP.serve('/mcp'),
'/sse': BrowserMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: BrowserScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/ai-gateway/src/ai-gateway.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerAIGatewayTools } from './tools/ai-gateway.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './ai-gateway.context'
const env = getEnv<Env>()
export { UserDetails }
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class AIGatewayMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
// Register Cloudflare Log Push tools
registerAIGatewayTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const AIGatewayScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'aig:read': 'Grants read level access to AI Gateway.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(AIGatewayMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': AIGatewayMCP.serve('/mcp'),
'/sse': AIGatewayMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: AIGatewayScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/auditlogs/src/auditlogs.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerAuditLogTools } from './tools/auditlogs.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './auditlogs.context'
const env = getEnv<Env>()
export { UserDetails }
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
export type State = { activeAccountId: string | null }
export class AuditlogMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
// Register Cloudflare Audit Log tools
registerAuditLogTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const AuditlogScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'auditlogs:read': 'See your resource configuration changes.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(AuditlogMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': AuditlogMCP.serve('/mcp'),
'/sse': AuditlogMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: AuditlogScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/docs-vectorize.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
interface RequiredEnv {
AI: Ai
VECTORIZE: VectorizeIndex
}
// Always return 10 results for simplicity, don't make it configurable
const TOP_K = 10
/**
* Registers the docs search tool with the MCP server
* @param server The MCP server instance
*/
export function registerDocsTools(server: McpServer, env: RequiredEnv) {
server.tool(
'search_cloudflare_documentation',
`Search the Cloudflare documentation.
This tool should be used to answer any question about Cloudflare products or features, including:
- Workers, Pages, R2, Images, Stream, D1, Durable Objects, KV, Workflows, Hyperdrive, Queues
- AI Search, Workers AI, Vectorize, AI Gateway, Browser Rendering
- Zero Trust, Access, Tunnel, Gateway, Browser Isolation, WARP, DDOS, Magic Transit, Magic WAN
- CDN, Cache, DNS, Zaraz, Argo, Rulesets, Terraform, Account and Billing
Results are returned as semantically similar chunks to the query.
`,
{
query: z.string(),
},
{
title: 'Search Cloudflare docs',
annotations: {
readOnlyHint: true,
},
},
async ({ query }) => {
const results = await queryVectorize(env.AI, env.VECTORIZE, query, TOP_K)
const resultsAsXml = results
.map((result) => {
return `<result>
<url>${result.url}</url>
<title>${result.title}</title>
<text>
${result.text}
</text>
</result>`
})
.join('\n')
return {
content: [{ type: 'text', text: resultsAsXml }],
}
}
)
// Note: this is a tool instead of a prompt because
// prompt support is much less common than tools.
server.tool(
'migrate_pages_to_workers_guide',
`ALWAYS read this guide before migrating Pages projects to Workers.`,
{},
{
title: 'Get Pages migration guide',
annotations: {
readOnlyHint: true,
},
},
async () => {
const res = await fetch(
'https://developers.cloudflare.com/workers/prompts/pages-to-workers.txt',
{
cf: { cacheEverything: true, cacheTtl: 3600 },
}
)
if (!res.ok) {
return {
content: [{ type: 'text', text: 'Error: Failed to fetch guide. Please try again.' }],
}
}
return {
content: [
{
type: 'text',
text: await res.text(),
},
],
}
}
)
}
async function queryVectorize(ai: Ai, vectorizeIndex: VectorizeIndex, query: string, topK: number) {
// Recommendation from: https://ai.google.dev/gemma/docs/embeddinggemma/model_card#prompt_instructions
const [queryEmbedding] = await getEmbeddings(ai, ['task: search result | query: ' + query])
const { matches } = await vectorizeIndex.query(queryEmbedding, {
topK,
returnMetadata: 'all',
returnValues: false,
})
return matches.map((match, _i) => ({
similarity: Math.min(match.score, 1),
id: match.id,
url: sourceToUrl(String(match.metadata?.filePath ?? '')),
title: String(match.metadata?.title ?? ''),
text: String(match.metadata?.text ?? ''),
}))
}
const TOP_DIR = 'src/content/docs'
function sourceToUrl(path: string) {
const prefix = `${TOP_DIR}/`
return (
'https://developers.cloudflare.com/' +
(path.startsWith(prefix) ? path.slice(prefix.length) : path)
.replace(/index\.mdx$/, '')
.replace(/\.mdx$/, '')
)
}
async function getEmbeddings(ai: Ai, strings: string[]): Promise<number[][]> {
const response = await doWithRetries(() =>
// @ts-expect-error embeddinggemma not in types yet
ai.run('@cf/google/embeddinggemma-300m', {
text: strings,
})
)
// @ts-expect-error embeddinggemma not in types yet
return response.data
}
/**
* @template T
* @param {() => Promise<T>} action
*/
async function doWithRetries<T>(action: () => Promise<T>) {
const NUM_RETRIES = 10
const INIT_RETRY_MS = 50
for (let i = 0; i <= NUM_RETRIES; i++) {
try {
return await action()
} catch (e) {
// TODO: distinguish between user errors (4xx) and system errors (5xx)
console.error(e)
if (i === NUM_RETRIES) {
throw e
}
// Exponential backoff with full jitter
await scheduler.wait(Math.random() * INIT_RETRY_MS * Math.pow(2, i))
}
}
// Should never reach here – last loop iteration should return
throw new Error('An unknown error occurred')
}
```
--------------------------------------------------------------------------------
/apps/dex-analysis/src/dex-analysis.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '@repo/mcp-observability'
import { registerDEXTools } from './tools/dex-analysis.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './dex-analysis.context'
export { UserDetails }
export { WarpDiagReader } from './warp_diag_reader'
const env = getEnv<Env>()
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class CloudflareDEXMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
registerDEXTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const DexScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'dex:write':
'Grants write level access to DEX resources like tests, fleet status, and remote captures.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(CloudflareDEXMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': CloudflareDEXMCP.serve('/mcp'),
'/sse': CloudflareDEXMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: DexScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/sentry.ts:
--------------------------------------------------------------------------------
```typescript
import { APIError } from 'cloudflare'
import { Toucan, zodErrorsIntegration } from 'toucan-js'
import { McpError } from './mcp-error'
import type { BaseTransportOptions, Client, ClientOptions, Event, EventHint } from '@sentry/types'
import type { Context, Next } from 'hono'
import type { Context as SentryContext } from 'toucan-js/dist/types'
import type { MCPEnvironment } from './config'
function is5xxError(status: number): boolean {
return status >= 500 && status <= 599
}
export class SentryClient {
private sentry: Toucan
constructor(sentry: Toucan) {
this.sentry = sentry
}
public recordError(e: unknown) {
if (this.sentry) {
// ignore errors from McpError and APIError (cloudflare) that have reportToSentry = false, or aren't 5xx errors
if (e instanceof McpError) {
if (e.reportToSentry === false) {
return
}
} else if (e instanceof APIError) {
if (!is5xxError(e.status)) {
return
}
}
this.sentry.captureException(e)
}
}
public setUser(userId: string) {
this.sentry.setUser({ ...this.sentry.getUser(), user_id: userId })
}
}
interface BaseBindings {
ENVIRONMENT: MCPEnvironment
GIT_HASH: string
SENTRY_DSN: string
SENTRY_ACCESS_CLIENT_ID: string
SENTRY_ACCESS_CLIENT_SECRET: string
}
export interface BaseHonoContext {
Bindings: BaseBindings
Variables: {
sentry?: SentryClient
}
}
export function initSentry<T extends BaseBindings>(
env: T,
ctx: SentryContext,
req?: Request<unknown, CfProperties>
): SentryClient {
const sentry = new Toucan({
dsn: env.SENTRY_DSN,
request: req,
environment: env.ENVIRONMENT,
context: ctx,
release: env.GIT_HASH,
requestDataOptions: {
allowedHeaders: [
'user-agent',
'cf-challenge',
'accept-encoding',
'accept-language',
'cf-ray',
'content-length',
'content-type',
'host',
],
// Allow ONLY the “scope” param in order to avoid recording jwt, code, state and any other callback params
allowedSearchParams: /^scope$/,
},
integrations: [
zodErrorsIntegration({ saveAttachments: true }),
{
name: 'mcp-api-errors',
processEvent(
event: Event,
_hint: EventHint,
_client: Client<ClientOptions<BaseTransportOptions>>
): Event {
const processedEvent = applyMcpErrorsToEvent(event)
return processedEvent
},
},
],
transportOptions: {
headers: {
'CF-Access-Client-ID': env.SENTRY_ACCESS_CLIENT_ID,
'CF-Access-Client-Secret': env.SENTRY_ACCESS_CLIENT_SECRET,
},
},
})
return new SentryClient(sentry)
}
export function initSentryWithUser<T extends BaseBindings>(
env: T,
ctx: SentryContext,
userId: string,
req?: Request<unknown, CfProperties>
): SentryClient {
const sentryClient = initSentry(env, ctx, req)
sentryClient.setUser(userId)
return sentryClient
}
export async function useSentry<T extends BaseHonoContext>(
c: Context<T>,
next: Next
): Promise<void> {
c.set('sentry', initSentry(c.env, c.executionCtx, c.req.raw))
await next()
}
export function setSentryRequestHeaders(sentry: Toucan, req: Request<unknown, CfProperties>) {
const colo: string = req.cf && typeof req.cf.colo === 'string' ? req.cf.colo : 'UNKNOWN'
sentry.setTag('colo', colo)
const ip_address = req.headers.get('cf-connecting-ip') ?? ''
const userAgent = req.headers.get('user-agent') ?? ''
sentry.setUser({
...sentry.getUser(),
ip_address,
userAgent,
colo,
})
}
function applyMcpErrorsToEvent(event: Event): Event {
if (event.exception === undefined || event.exception.values === undefined) {
return event
}
if (event.exception instanceof McpError) {
try {
return {
...event,
extra: {
...event.extra,
statusCode: event.exception.code,
internalMessage: event.exception.internalMessage,
},
}
} catch (e) {
// Hopefully we never throw errors here, but record it
// with the event just in case.
return {
...event,
extra: {
...event.extra,
'McpError sentry integration parse error': {
message: `an exception was thrown while processing McpError within applyMcpErrorsToEvent()`,
error: e instanceof Error ? `${e.name}: ${e.cause}\n${e.stack}` : 'unknown',
},
},
}
}
}
return event
}
```
--------------------------------------------------------------------------------
/apps/logpush/src/logpush.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerLogsTools } from './tools/logpush.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './logpush.context'
const env = getEnv<Env>()
export { UserDetails }
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class LogsMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
// Register Cloudflare Log Push tools
registerLogsTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const LogPushScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'logpush:write':
'Grants read and write access to Logpull and Logpush, and read access to Instant Logs. Note that all Logpush API operations require Logs: Write permission because Logpush jobs contain sensitive information.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(LogsMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': LogsMCP.serve('/mcp'),
'/sse': LogsMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/workers-bindings/evals/kv_namespaces.eval.ts:
--------------------------------------------------------------------------------
```typescript
import { expect } from 'vitest'
import { describeEval } from 'vitest-evals'
import { runTask } from '@repo/eval-tools/src/runTask'
import { checkFactuality } from '@repo/eval-tools/src/scorers'
import { eachModel } from '@repo/eval-tools/src/test-models'
import { KV_NAMESPACE_TOOLS } from '@repo/mcp-common/src/tools/kv_namespace.tools'
import { initializeClient } from './utils' // Assuming utils.ts will exist here
eachModel('$modelName', ({ model }) => {
describeEval('Create Cloudflare KV Namespace', {
data: async () => [
{
input: 'Create a new Cloudflare KV Namespace called "my-test-namespace".',
expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_create} tool should be called to create a new kv namespace.`,
},
],
task: async (input: string) => {
const client = await initializeClient(/* Pass necessary mocks/config */)
const { promptOutput, toolCalls } = await runTask(client, model, input)
const toolCall = toolCalls.find(
(call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_create
)
expect(toolCall, 'Tool kv_namespace_create was not called').toBeDefined()
return promptOutput
},
scorers: [checkFactuality],
threshold: 1,
timeout: 60000, // 60 seconds
})
describeEval('List Cloudflare KV Namespaces', {
data: async () => [
{
input: 'List all my Cloudflare KV Namespaces.',
expected: `The ${KV_NAMESPACE_TOOLS.kv_namespaces_list} tool should be called to retrieve the list of kv namespaces. There should be at least one kv namespace in the list.`,
},
],
task: async (input: string) => {
const client = await initializeClient(/* Pass necessary mocks/config */)
const { promptOutput, toolCalls } = await runTask(client, model, input)
const toolCall = toolCalls.find(
(call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespaces_list
)
expect(toolCall, 'Tool kv_namespaces_list was not called').toBeDefined()
return promptOutput
},
scorers: [checkFactuality],
threshold: 1,
timeout: 60000, // 60 seconds
})
describeEval('Rename Cloudflare KV Namespace', {
data: async () => [
{
input: 'Rename my Cloudflare KV Namespace with ID 1234 to "my-new-test-namespace".',
expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_update} tool should be called to rename the kv namespace.`,
},
],
task: async (input: string) => {
const client = await initializeClient(/* Pass necessary mocks/config */)
const { promptOutput, toolCalls } = await runTask(client, model, input)
const toolCall = toolCalls.find(
(call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_update
)
expect(toolCall, 'Tool kv_namespace_update was not called').toBeDefined()
return promptOutput
},
scorers: [checkFactuality],
threshold: 1,
timeout: 60000, // 60 seconds
})
describeEval('Get Cloudflare KV Namespace Details', {
data: async () => [
{
input: 'Get details of my Cloudflare KV Namespace with ID 1234.',
expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_get} tool should be called to retrieve the details of the kv namespace.`,
},
],
task: async (input: string) => {
const client = await initializeClient(/* Pass necessary mocks/config */)
const { promptOutput, toolCalls } = await runTask(client, model, input)
const toolCall = toolCalls.find(
(call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_get
)
expect(toolCall, 'Tool kv_namespace_get was not called').toBeDefined()
return promptOutput
},
scorers: [checkFactuality],
threshold: 1,
timeout: 60000, // 60 seconds
})
describeEval('Delete Cloudflare KV Namespace', {
data: async () => [
{
input: 'Delete the kv namespace with ID 1234.',
expected: `The ${KV_NAMESPACE_TOOLS.kv_namespace_delete} tool should be called to delete the kv namespace.`,
},
],
task: async (input: string) => {
const client = await initializeClient(/* Pass necessary mocks/config */)
const { promptOutput, toolCalls } = await runTask(client, model, input)
const toolCall = toolCalls.find(
(call) => call.toolName === KV_NAMESPACE_TOOLS.kv_namespace_delete
)
expect(toolCall, 'Tool kv_namespace_delete was not called').toBeDefined()
return promptOutput
},
scorers: [checkFactuality],
threshold: 1,
timeout: 60000, // 60 seconds
})
})
```
--------------------------------------------------------------------------------
/apps/radar/src/radar.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { MetricsTracker } from '@repo/mcp-observability'
import { BASE_INSTRUCTIONS } from './radar.context'
import { registerRadarTools } from './tools/radar.tools'
import { registerUrlScannerTools } from './tools/url-scanner.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './radar.context'
const env = getEnv<Env>()
export { UserDetails }
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class RadarMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
options: { instructions: BASE_INSTRUCTIONS },
})
registerAccountTools(this)
registerRadarTools(this)
registerUrlScannerTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const RadarScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'radar:read': 'Grants access to read Cloudflare Radar data.',
'url_scanner:write': 'Grants write level access to URL Scanner',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(RadarMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': RadarMCP.serve('/mcp'),
'/sse': RadarMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: RadarScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/packages/mcp-observability/src/analytics-engine.ts:
--------------------------------------------------------------------------------
```typescript
export type MetricsBindings = {
MCP_METRICS: AnalyticsEngineDataset
}
/**
* Generic metrics event utilities
* @description Wrapper for RA binding
*/
export class MetricsTracker {
constructor(
private wae: AnalyticsEngineDataset,
private mcpServerInfo: {
name: string
version: string
}
) {}
logEvent(event: MetricsEvent): void {
try {
event.serverInfo = this.mcpServerInfo
let dataPoint = event.toDataPoint()
this.wae.writeDataPoint(dataPoint)
} catch (e) {
console.error(`Failed to log metrics event, ${e}`)
}
}
}
/**
* MetricsEvent
*
* Each event type is stored with a different indexId and has an associated class which
* maps a more ergonomic event object to a ReadyAnalyticsEvent
*/
export abstract class MetricsEvent {
public _serverInfo: { name: string; version: string } | undefined
set serverInfo(serverInfo: { name: string; version: string }) {
this._serverInfo = serverInfo
}
get serverInfo(): { name: string; version: string } {
if (!this._serverInfo) {
throw new Error('Server info not set')
}
return this._serverInfo
}
/**
* Output a valid AnalyticsEngineDataPoint. Use `mapBlobs` and `mapDoubles` to write well defined
* analytics engine datapoints. The first and second blob entries are reserved for the MCP server name and
* MCP server version.
*/
abstract toDataPoint(): AnalyticsEngineDataPoint
mapBlobs(blobs: Blobs): Array<string | null> {
if (blobs.blob1 || blobs.blob2) {
throw new MetricsError(
'Failed to map blobs, blob1 and blob2 are reserved for MCP server info'
)
}
// add placeholder blobs, filled in by the MetricsTracker later
blobs.blob1 = this.serverInfo.name
blobs.blob2 = this.serverInfo.version
const blobsArray = new Array(Object.keys(blobs).length)
for (const [key, value] of Object.entries(blobs)) {
const match = key.match(/^blob(\d+)$/)
if (match === null || match.length < 2) {
// we should never hit this because of the typedefinitions above,
// but this error is for safety
throw new MetricsError('Failed to map blobs, invalid key')
}
const index = parseInt(match[1], 10)
if (isNaN(index)) {
// we should never hit this because of the typedefinitions above,
// but this esrror is for safety
throw new MetricsError('Failed to map blobs, invalid index')
}
if (index - 1 >= blobsArray.length) {
throw new MetricsError('Failed to map blobs, missing blob')
}
blobsArray[index - 1] = value
}
return blobsArray
}
mapDoubles(doubles: Doubles): number[] {
const doublesArray = new Array(Object.keys(doubles).length)
for (const [key, value] of Object.entries(doubles)) {
const match = key.match(/^double(\d+)$/)
if (match === null || match.length < 2) {
// we should never hit this because of the typedefinitions above,
// but this error is for safety
throw new MetricsError(': Failed to map doubles, invalid key')
}
const index = parseInt(match[1], 10)
if (isNaN(index)) {
// we should never hit this because of the typedefinitions above,
// but this error is for safety
throw new MetricsError('Failed to map doubles, invalid index')
}
if (index - 1 >= doublesArray.length) {
throw new MetricsError('Failed to map doubles, missing blob')
}
doublesArray[index - 1] = value
}
return doublesArray
}
}
export enum MetricsEventIndexIds {
AUTH_USER = 'auth_user',
SESSION_START = 'session_start',
TOOL_CALL = 'tool_call',
CONTAINER_MANAGER = 'container_manager',
}
/**
* Utility functions to map named blob/double objects to an array
* We do this so we don't have to annotate `blob1`, `blob2`, etc in comments.
*
* I prefer this to just writing it in an array because it'll be easier to reference
* later when we are writing ready analytics queries.
*
* IMO named tuples and raw arrays aren't as ergonomic to work with, but they require less of this code below
*/
type Range1To20 =
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18
| 19
| 20
// blob1 and blob2 are reserved for server name and version
type Blobs = {
[key in `blob${Range1To20}`]?: string | null
}
type Doubles = {
[key in `double${Range1To20}`]?: number
}
export class MetricsError extends Error {
constructor(message: string) {
super(message)
this.name = 'MetricsError'
}
}
```
--------------------------------------------------------------------------------
/apps/dns-analytics/src/dns-analytics.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerAnalyticTools } from './tools/dex-analytics.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './dns-analytics.context'
export { UserDetails }
const env = getEnv<Env>()
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
export type Props = AuthProps
export type State = { activeAccountId: string | null }
export class DNSAnalyticsMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
// Register Cloudflare DNS Analytics tools
registerAnalyticTools(this)
registerZoneTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const AnalyticsScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'zone:read': 'See your zones',
'dns_settings:read': 'See your DNS settings',
'dns_analytics:read': 'See your DNS analytics',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(DNSAnalyticsMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': DNSAnalyticsMCP.serve('/mcp'),
'/sse': DNSAnalyticsMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: AnalyticsScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/sandbox-container/container/sandbox.container.app.ts:
--------------------------------------------------------------------------------
```typescript
import { exec } from 'node:child_process'
import * as fs from 'node:fs/promises'
import path from 'node:path'
import { serve } from '@hono/node-server'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { streamText } from 'hono/streaming'
import mime from 'mime'
import { ExecParams, FileWrite } from '../shared/schema.ts'
import {
DIRECTORY_CONTENT_TYPE,
get_file_name_from_path,
get_mime_type,
list_files_in_directory,
} from './fileUtils.ts'
import type { FileList } from '../shared/schema.ts'
process.chdir('workdir')
const app = new Hono()
app.get('/ping', (c) => c.text('pong!'))
/**
* GET /files/ls
*
* Gets all files in a directory
*/
app.get('/files/ls', async (c) => {
const directoriesToRead = ['.']
const files: FileList = { resources: [] }
while (directoriesToRead.length > 0) {
const curr = directoriesToRead.pop()
if (!curr) {
throw new Error('Popped empty stack, error while listing directories')
}
const fullPath = path.join(process.cwd(), curr)
const dir = await fs.readdir(fullPath, { withFileTypes: true })
for (const dirent of dir) {
const relPath = path.relative(process.cwd(), `${fullPath}/${dirent.name}`)
if (dirent.isDirectory()) {
directoriesToRead.push(dirent.name)
files.resources.push({
uri: `file:///${relPath}`,
name: dirent.name,
mimeType: 'inode/directory',
})
} else {
const mimeType = mime.getType(dirent.name)
files.resources.push({
uri: `file:///${relPath}`,
name: dirent.name,
mimeType: mimeType ?? undefined,
})
}
}
}
return c.json(files)
})
/**
* GET /files/contents/{filepath}
*
* Get the contents of a file or directory
*/
app.get('/files/contents/*', async (c) => {
const reqPath = await get_file_name_from_path(c.req.path)
try {
const mimeType = await get_mime_type(reqPath)
const headers = mimeType ? { 'Content-Type': mimeType } : undefined
const contents = await fs.readFile(path.join(process.cwd(), reqPath))
return c.newResponse(contents, 200, headers)
} catch (e: any) {
if (e.code) {
if (e.code === 'EISDIR') {
const files = await list_files_in_directory(reqPath)
return c.newResponse(files.join('\n'), 200, {
'Content-Type': DIRECTORY_CONTENT_TYPE,
})
}
if (e.code === 'ENOENT') {
return c.notFound()
}
}
throw e
}
})
/**
* POST /files/contents
*
* Create or update file contents
*/
app.post('/files/contents', zValidator('json', FileWrite), async (c) => {
const file = c.req.valid('json')
const reqPath = await get_file_name_from_path(file.path)
try {
await fs.writeFile(reqPath, file.text)
return c.newResponse(null, 200)
} catch (e) {
return c.newResponse(`Error: ${e}`, 400)
}
})
/**
* DELETE /files/contents/{filepath}
*
* Delete a file or directory
*/
app.delete('/files/contents/*', async (c) => {
const reqPath = await get_file_name_from_path(c.req.path)
try {
await fs.rm(path.join(process.cwd(), reqPath), { recursive: true })
return c.newResponse('ok', 200)
} catch (e: any) {
if (e.code) {
if (e.code === 'ENOENT') {
return c.notFound()
}
}
throw e
}
})
/**
* POST /exec
*
* Execute a command in a shell
*/
app.post('/exec', zValidator('json', ExecParams), (c) => {
const execParams = c.req.valid('json')
const proc = exec(execParams.args)
return streamText(c, async (stream) => {
return new Promise((resolve, reject) => {
if (proc.stdout) {
// Stream data from stdout
proc.stdout.on('data', async (data) => {
await stream.write(data.toString())
})
} else {
void stream.write('WARNING: no stdout stream for process')
}
if (execParams.streamStderr) {
if (proc.stderr) {
proc.stderr.on('data', async (data) => {
await stream.write(data.toString())
})
} else {
void stream.write('WARNING: no stderr stream for process')
}
}
// Handle process exit
proc.on('exit', async (code) => {
await stream.write(`Process exited with code: ${code}`)
if (code === 0) {
await stream.close()
resolve()
} else {
console.error(`Process exited with code ${code}`)
reject(new Error(`Process failed with code ${code}`))
}
})
proc.on('error', (err) => {
console.error('Error with process: ', err)
reject(err)
})
})
})
})
serve({
fetch: app.fetch,
port: 8080,
})
```
--------------------------------------------------------------------------------
/apps/graphql/src/graphql.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { initSentryWithUser } from '@repo/mcp-common/src/sentry'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools'
import { MetricsTracker } from '@repo/mcp-observability'
import { registerGraphQLTools } from './tools/graphql.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './graphql.context'
export { UserDetails }
const env = getEnv<Env>()
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class GraphQLMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
const sentry =
props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
sentry,
})
// Register account tools
registerAccountTools(this)
// Register zone tools
registerZoneTools(this)
// Register GraphQL tools
registerGraphQLTools(this)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const GraphQLScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'zone:read': 'See zone data such as settings, analytics, and DNS records.',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(GraphQLMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': GraphQLMCP.serve('/mcp'),
'/sse': GraphQLMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: GraphQLScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/autorag/src/tools/autorag.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { V4PagePaginationArray } from 'cloudflare/src/pagination.js'
import { z } from 'zod'
import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
import { getProps } from '@repo/mcp-common/src/get-props'
import { pageParam, perPageParam } from '../types'
import type { AutoRAGMCP } from '../autorag.app'
export function registerAutoRAGTools(agent: AutoRAGMCP) {
agent.server.tool(
'list_rags',
'List AutoRAGs (vector stores)',
{
page: pageParam,
per_page: perPageParam,
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const r = (await client.getAPIList(
`/accounts/${accountId}/autorag/rags`,
// @ts-ignore
V4PagePaginationArray,
{ query: { page: params.page, per_page: params.per_page } }
)) as unknown as {
result: Array<{ id: string; source: string; paused: boolean }>
result_info: { total_count: number }
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
autorags: r.result.map((obj) => {
return {
id: obj.id,
source: obj.source,
paused: obj.paused,
}
}),
total_count: r.result_info.total_count,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error listing rags: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
agent.server.tool(
'search',
'Search Documents using AutoRAG (vector store)',
{
rag_id: z.string().describe('ID of the AutoRAG to search'),
query: z.string().describe('Query to search for. Can be a URL, a title, or a snippet.'),
},
async (params) => {
try {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const r = (await client.post(
`/accounts/${accountId}/autorag/rags/${params.rag_id}/search`,
{
body: {
query: params.query,
max_num_results: 5,
},
}
)) as { result: { data: Array<{ filename: string; content: Array<{ text: string }> }> } }
const chunks = r.result.data
.map((item) => {
const data = item.content
.map((content) => {
return content.text
})
.join('\n\n')
return `<file name="${item.filename}">${data}</file>`
})
.join('\n\n')
return {
content: [
{
type: 'text',
text: chunks,
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error searching rag: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
agent.server.tool(
'ai_search',
'AI Search Documents using AutoRAG (vector store)',
{
rag_id: z.string().describe('ID of the AutoRAG to search'),
query: z.string().describe('Query to search for. Can be a URL, a title, or a snippet.'),
},
async (params) => {
try {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
const props = getProps(agent)
const client = getCloudflareClient(props.accessToken)
const r = (await client.post(
`/accounts/${accountId}/autorag/rags/${params.rag_id}/ai-search`,
{
body: {
query: params.query,
max_num_results: 10, // Limit can be bigger here, since llm is only getting the end response and not individual chunks
},
}
)) as { result: { response: string } }
return {
content: [
{
type: 'text',
text: r.result.response,
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error searching rag: ${error instanceof Error && error.message}`,
},
],
}
}
}
)
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/cloudflare-auth.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import { McpError } from './mcp-error'
import type { AuthRequest } from '@cloudflare/workers-oauth-provider'
// Constants
const PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
const RECOMMENDED_CODE_VERIFIER_LENGTH = 96
function base64urlEncode(value: string): string {
let base64 = btoa(value)
base64 = base64.replace(/\+/g, '-')
base64 = base64.replace(/\//g, '_')
base64 = base64.replace(/=/g, '')
return base64
}
interface PKCECodes {
codeChallenge: string
codeVerifier: string
}
export async function generatePKCECodes(): Promise<PKCECodes> {
const output = new Uint32Array(RECOMMENDED_CODE_VERIFIER_LENGTH)
crypto.getRandomValues(output)
const codeVerifier = base64urlEncode(
Array.from(output)
.map((num: number) => PKCE_CHARSET[num % PKCE_CHARSET.length])
.join('')
)
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
const hash = new Uint8Array(buffer)
let binary = ''
const hashLength = hash.byteLength
for (let i = 0; i < hashLength; i++) {
binary += String.fromCharCode(hash[i])
}
const codeChallenge = base64urlEncode(binary) //btoa(binary);
return { codeChallenge, codeVerifier }
}
function generateAuthUrl({
client_id,
redirect_uri,
state,
code_challenge,
scopes,
}: {
client_id: string
redirect_uri: string
code_challenge: string
state: string
scopes: Record<string, string>
}) {
const params = new URLSearchParams({
response_type: 'code',
client_id,
redirect_uri,
state,
code_challenge,
code_challenge_method: 'S256',
scope: Object.keys(scopes).join(' '),
})
const upstream = new URL(`https://dash.cloudflare.com/oauth2/auth?${params.toString()}`)
return upstream.href
}
/**
* Constructs an authorization URL for Cloudflare.
*
* @param {Object} options
* @param {string} options.client_id - The client ID of the application.
* @param {string} options.redirect_uri - The redirect URI of the application.
* @param {string} [options.state] - The state parameter.
*
* @returns {string} The authorization URL.
*/
export async function getAuthorizationURL({
client_id,
redirect_uri,
state,
scopes,
codeChallenge,
}: {
client_id: string
redirect_uri: string
state: AuthRequest
scopes: Record<string, string>
codeChallenge: string
}): Promise<{ authUrl: string }> {
return {
authUrl: generateAuthUrl({
client_id,
redirect_uri,
state: btoa(JSON.stringify(state)),
code_challenge: codeChallenge,
scopes,
}),
}
}
type AuthorizationToken = z.infer<typeof AuthorizationToken>
const AuthorizationToken = z.object({
access_token: z.string(),
expires_in: z.number(),
refresh_token: z.string(),
scope: z.string(),
token_type: z.string(),
})
/**
* Fetches an authorization token from Cloudflare.
*
* @param {Object} options
* @param {string} options.client_id - The client ID of the application.
* @param {string} options.client_secret - The client secret of the application.
* @param {string} options.code - The authorization code.
* @param {string} options.redirect_uri - The redirect URI of the application.
*
* @returns {Promise<[string, null] | [null, Response]>} A promise that resolves to an array containing the access token or an error response.
*/
export async function getAuthToken({
client_id,
client_secret,
redirect_uri,
code_verifier,
code,
}: {
client_id: string
client_secret: string
redirect_uri: string
code_verifier: string
code: string
}): Promise<AuthorizationToken> {
if (!code) {
throw new McpError('Missing code', 400)
}
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id,
redirect_uri,
code,
code_verifier,
}).toString()
const resp = await fetch('https://dash.cloudflare.com/oauth2/token', {
method: 'POST',
headers: {
Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
})
if (!resp.ok) {
console.log(await resp.text())
throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true })
}
return AuthorizationToken.parse(await resp.json())
}
export async function refreshAuthToken({
client_id,
client_secret,
refresh_token,
}: {
client_id: string
client_secret: string
refresh_token: string
}): Promise<AuthorizationToken> {
const params = new URLSearchParams({
grant_type: 'refresh_token',
client_id,
refresh_token,
})
const resp = await fetch('https://dash.cloudflare.com/oauth2/token', {
method: 'POST',
body: params.toString(),
headers: {
Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
})
if (!resp.ok) {
console.log(await resp.text())
throw new McpError('Failed to get OAuth token', 500, { reportToSentry: true })
}
return AuthorizationToken.parse(await resp.json())
}
```
--------------------------------------------------------------------------------
/apps/sandbox-container/server/userContainer.ts:
--------------------------------------------------------------------------------
```typescript
import { DurableObject } from 'cloudflare:workers'
import { OPEN_CONTAINER_PORT } from '../shared/consts'
import { MAX_CONTAINERS, proxyFetch, startAndWaitForPort } from './containerHelpers'
import { getContainerManager } from './containerManager'
import { fileToBase64 } from './utils'
import type { ExecParams, FileList, FileWrite } from '../shared/schema'
import type { Env } from './sandbox.server.context'
export class UserContainer extends DurableObject<Env> {
constructor(
public ctx: DurableObjectState,
public env: Env
) {
console.log('creating user container DO')
super(ctx, env)
}
async destroyContainer(): Promise<void> {
await this.ctx.container?.destroy()
}
async killContainer(): Promise<void> {
console.log('Reaping container')
const containerManager = getContainerManager(this.env)
const active = await containerManager.listActive()
if (this.ctx.id.toString() in active) {
console.log('killing container')
await this.destroyContainer()
await containerManager.killContainer(this.ctx.id.toString())
}
}
async container_initialize(): Promise<string> {
// kill container
await this.killContainer()
// try to cleanup cleanup old containers
const containerManager = getContainerManager(this.env)
// if more than half of our containers are being used, let's try reaping
if ((await containerManager.listActive()).length >= MAX_CONTAINERS / 2) {
await containerManager.tryKillOldContainers()
if ((await containerManager.listActive()).length >= MAX_CONTAINERS) {
throw new Error(
`Unable to reap enough containers. There are ${MAX_CONTAINERS} active container sandboxes, please wait`
)
}
}
// start container
let startedContainer = false
await this.ctx.blockConcurrencyWhile(async () => {
startedContainer = await startAndWaitForPort(
this.env.ENVIRONMENT,
this.ctx.container,
OPEN_CONTAINER_PORT
)
})
if (!startedContainer) {
throw new Error('Failed to start container')
}
// track and manage lifecycle
await containerManager.trackContainer(this.ctx.id.toString())
return `Created new container`
}
async container_ping(): Promise<string> {
const res = await proxyFetch(
this.env.ENVIRONMENT,
this.ctx.container,
new Request(`http://host:${OPEN_CONTAINER_PORT}/ping`),
OPEN_CONTAINER_PORT
)
if (!res || !res.ok) {
throw new Error(`Request to container failed: ${await res.text()}`)
}
return await res.text()
}
async container_exec(params: ExecParams): Promise<string> {
const res = await proxyFetch(
this.env.ENVIRONMENT,
this.ctx.container,
new Request(`http://host:${OPEN_CONTAINER_PORT}/exec`, {
method: 'POST',
body: JSON.stringify(params),
headers: {
'content-type': 'application/json',
},
}),
OPEN_CONTAINER_PORT
)
if (!res || !res.ok) {
throw new Error(`Request to container failed: ${await res.text()}`)
}
const txt = await res.text()
return txt
}
async container_ls(): Promise<FileList> {
const res = await proxyFetch(
this.env.ENVIRONMENT,
this.ctx.container,
new Request(`http://host:${OPEN_CONTAINER_PORT}/files/ls`),
OPEN_CONTAINER_PORT
)
if (!res || !res.ok) {
throw new Error(`Request to container failed: ${await res.text()}`)
}
const json = (await res.json()) as FileList
return json
}
async container_file_delete(filePath: string): Promise<boolean> {
const res = await proxyFetch(
this.env.ENVIRONMENT,
this.ctx.container,
new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`, {
method: 'DELETE',
}),
OPEN_CONTAINER_PORT
)
return res.ok
}
async container_file_read(
filePath: string
): Promise<
| { type: 'text'; textOutput: string; mimeType: string | undefined }
| { type: 'base64'; base64Output: string; mimeType: string | undefined }
> {
const res = await proxyFetch(
this.env.ENVIRONMENT,
this.ctx.container,
new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents/${filePath}`),
OPEN_CONTAINER_PORT
)
if (!res || !res.ok) {
throw new Error(`Request to container failed: ${await res.text()}`)
}
const mimeType = res.headers.get('Content-Type') ?? undefined
const blob = await res.blob()
if (mimeType && mimeType.startsWith('text')) {
return {
type: 'text',
textOutput: await blob.text(),
mimeType,
}
} else {
return {
type: 'base64',
base64Output: await fileToBase64(blob),
mimeType,
}
}
}
async container_file_write(file: FileWrite): Promise<string> {
const res = await proxyFetch(
this.env.ENVIRONMENT,
this.ctx.container,
new Request(`http://host:${OPEN_CONTAINER_PORT}/files/contents`, {
method: 'POST',
body: JSON.stringify(file),
headers: {
'content-type': 'application/json',
},
}),
OPEN_CONTAINER_PORT
)
if (!res || !res.ok) {
throw new Error(`Request to container failed: ${await res.text()}`)
}
return `Wrote file: ${file.path}`
}
}
```
--------------------------------------------------------------------------------
/apps/workers-bindings/src/bindings.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-ai-search.prompts'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { registerD1Tools } from '@repo/mcp-common/src/tools/d1.tools'
import { registerDocsTools } from '@repo/mcp-common/src/tools/docs-ai-search.tools'
import { registerHyperdriveTools } from '@repo/mcp-common/src/tools/hyperdrive.tools'
import { registerKVTools } from '@repo/mcp-common/src/tools/kv_namespace.tools'
import { registerR2BucketTools } from '@repo/mcp-common/src/tools/r2_bucket.tools'
import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools'
import { MetricsTracker } from '@repo/mcp-observability'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './bindings.context'
export { UserDetails }
const env = getEnv<Env>()
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
export type WorkersBindingsMCPState = { activeAccountId: string | null }
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
export class WorkersBindingsMCP extends McpAgent<Env, WorkersBindingsMCPState, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
initialState: WorkersBindingsMCPState = {
activeAccountId: null,
}
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})
registerAccountTools(this)
registerKVTools(this)
registerWorkersTools(this)
registerR2BucketTools(this)
registerD1Tools(this)
registerHyperdriveTools(this)
// Add docs tools
registerDocsTools(this.server, this.env)
registerPrompts(this.server)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const BindingsScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'workers:write':
'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
'd1:write': 'Create, read, and write to D1 databases',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
console.log('is token mode')
return await handleApiTokenMode(WorkersBindingsMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': WorkersBindingsMCP.serve('/mcp'),
'/sse': WorkersBindingsMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: BindingsScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/tools/worker.tools.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
import {
handleGetWorkersService,
handleWorkerScriptDownload,
handleWorkersList,
} from '../api/workers.api'
import { getCloudflareClient } from '../cloudflare-api'
import { fmt } from '../format'
import { getProps } from '../get-props'
import type { CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types'
/**
* Registers the workers tools with the MCP server
* @param server The MCP server instance
* @param accountId Cloudflare account ID
* @param apiToken Cloudflare API token
*/
// Define the scriptName parameter schema
const workerNameParam = z.string().describe('The name of the worker script to retrieve')
export function registerWorkersTools(agent: CloudflareMcpAgent) {
// Tool to list all workers
agent.server.tool(
'workers_list',
fmt.trim(`
List all Workers in your Cloudflare account.
If you only need details of a single Worker, use workers_get_worker.
`),
{},
{
title: 'List Workers',
annotations: {
readOnlyHint: true,
destructiveHint: false,
},
},
async () => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const results = await handleWorkersList({
client: getCloudflareClient(props.accessToken),
accountId,
})
// Extract worker details and sort by created_on date (newest first)
const workers = results
.map((worker) => ({
name: worker.id,
// The API client doesn't know tag exists. The tag is needed in other places such as Workers Builds
id: z.object({ tag: z.string() }).parse(worker),
modified_on: worker.modified_on || null,
created_on: worker.created_on || null,
}))
// order by created_on desc ( newest first )
.sort((a, b) => {
if (!a.created_on) return 1
if (!b.created_on) return -1
return new Date(b.created_on).getTime() - new Date(a.created_on).getTime()
})
return {
content: [
{
type: 'text',
text: JSON.stringify({
workers,
count: workers.length,
}),
},
],
}
} catch (e) {
agent.server.recordError(e)
return {
content: [
{
type: 'text',
text: `Error listing workers: ${e instanceof Error && e.message}`,
},
],
}
}
}
)
// Tool to get a specific worker's script details
agent.server.tool(
'workers_get_worker',
'Get the details of the Cloudflare Worker.',
{
scriptName: workerNameParam,
},
{
title: 'Get Worker details',
annotations: {
readOnlyHint: true,
destructiveHint: false,
},
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const { scriptName } = params
const res = await handleGetWorkersService({
apiToken: props.accessToken,
scriptName,
accountId,
})
if (!res.result) {
return {
content: [
{
type: 'text',
text: 'Worker not found',
},
],
}
}
return {
content: [
{
type: 'text',
text: await fmt.asTSV([
{
name: res.result.id,
id: res.result.default_environment.script_tag,
},
]),
},
],
}
} catch (e) {
agent.server.recordError(e)
return {
content: [
{
type: 'text',
text: `Error retrieving worker script: ${e instanceof Error && e.message}`,
},
],
}
}
}
)
// Tool to get a specific worker's script content
agent.server.tool(
'workers_get_worker_code',
'Get the source code of a Cloudflare Worker. Note: This may be a bundled version of the worker.',
{ scriptName: workerNameParam },
{
title: 'Get Worker code',
annotations: {
readOnlyHint: true,
destructiveHint: false,
},
},
async (params) => {
const accountId = await agent.getActiveAccountId()
if (!accountId) {
return {
content: [
{
type: 'text',
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
},
],
}
}
try {
const props = getProps(agent)
const { scriptName } = params
const scriptContent = await handleWorkerScriptDownload({
client: getCloudflareClient(props.accessToken),
scriptName,
accountId,
})
return {
content: [
{
type: 'text',
text: scriptContent,
},
],
}
} catch (e) {
agent.server.recordError(e)
return {
content: [
{
type: 'text',
text: `Error retrieving worker script: ${e instanceof Error && e.message}`,
},
],
}
}
}
)
}
```
--------------------------------------------------------------------------------
/apps/workers-observability/src/workers-observability.app.ts:
--------------------------------------------------------------------------------
```typescript
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'
import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-ai-search.prompts'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { initSentryWithUser } from '@repo/mcp-common/src/sentry'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { registerDocsTools } from '@repo/mcp-common/src/tools/docs-ai-search.tools'
import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker.tools'
import { MetricsTracker } from '../../../packages/mcp-observability/src'
import { registerObservabilityTools } from './tools/workers-observability.tools'
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './workers-observability.context'
export { UserDetails }
const env = getEnv<Env>()
const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = AuthProps
type State = { activeAccountId: string | null }
export class ObservabilityMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
async init() {
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined
const sentry =
props.type === 'user_token' ? initSentryWithUser(env, this.ctx, props.user.id) : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
sentry,
options: {
instructions: `# Cloudflare Workers Observability Tool
* A cloudflare worker is a serverless function
* Workers Observability is the tool to inspect the logs for your cloudflare Worker
* Each log is a structured JSON payload with keys and values
This server allows you to analyze your Cloudflare Workers logs and metrics.
`,
},
})
registerAccountTools(this)
// Register Cloudflare Workers tools
registerWorkersTools(this)
// Register Cloudflare Workers logs tools
registerObservabilityTools(this)
// Add docs tools
registerDocsTools(this.server, this.env)
registerPrompts(this.server)
}
async getActiveAccountId() {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return props.account.id
}
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
// we do this so we can persist activeAccountId across sessions
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}
async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
// account tokens are scoped to one account
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}
const ObservabilityScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'workers:read':
'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
'workers_observability:read': 'See observability logs for your account',
} as const
export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(ObservabilityMCP, req, env, ctx)
}
return new OAuthProvider({
apiHandlers: {
'/mcp': ObservabilityMCP.serve('/mcp'),
'/sse': ObservabilityMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: ObservabilityScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
// Cloudflare access token TTL
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
```
--------------------------------------------------------------------------------
/apps/sandbox-container/server/containerMcp.ts:
--------------------------------------------------------------------------------
```typescript
import { McpAgent } from 'agents/mcp'
import { getProps } from '@repo/mcp-common/src/get-props'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { ExecParams, FilePathParam, FileWrite } from '../shared/schema'
import { BASE_INSTRUCTIONS } from './prompts'
import { stripProtocolFromFilePath } from './utils'
import type { Props, UserContainer } from './sandbox.server.app'
import type { Env } from './sandbox.server.context'
export class ContainerMcpAgent extends McpAgent<Env, never, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}
get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}
return this._server
}
get userContainer(): DurableObjectStub<UserContainer> {
const props = getProps(this)
// TODO: Support account scoped tokens?
if (props.type === 'account_token') {
throw new Error('Container server does not currently support account scoped tokens')
}
const userContainer = this.env.USER_CONTAINER.idFromName(props.user.id)
return this.env.USER_CONTAINER.get(userContainer)
}
constructor(
public ctx: DurableObjectState,
public env: Env
) {
console.log('creating container DO')
super(ctx, env)
}
async init() {
const props = getProps(this)
// TODO: Probably we'll want to track account tokens usage through an account identifier at some point
const userId = props.type === 'user_token' ? props.user.id : undefined
this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
options: { instructions: BASE_INSTRUCTIONS },
})
this.server.tool(
'container_initialize',
`Start or restart the container.
Use this tool to initialize a container before running any python or node.js code that the user requests ro run.`,
async () => {
const props = getProps(this)
if (props.type === 'account_token') {
return {
// TODO: Support account scoped tokens?
// we'll need to add support for an account blocklist in that case
content: [
{
type: 'text',
text: 'Container server does not currently support account scoped tokens.',
},
],
}
}
const userInBlocklist = await this.env.USER_BLOCKLIST.get(props.user.id)
if (userInBlocklist) {
return {
content: [{ type: 'text', text: 'Blocked from intializing container.' }],
}
}
return {
content: [{ type: 'text', text: await this.userContainer.container_initialize() }],
}
}
)
this.server.tool(
'container_ping',
`Ping the container for liveliness. Use this tool to check if the container is running.`,
async () => {
return {
content: [{ type: 'text', text: await this.userContainer.container_ping() }],
}
}
)
this.server.tool(
'container_exec',
`Run a command in a container and return the results from stdout.
If necessary, set a timeout. To debug, stream back standard error.
If you're using python, ALWAYS use python3 alongside pip3`,
{ args: ExecParams },
async ({ args }) => {
return {
content: [{ type: 'text', text: await this.userContainer.container_exec(args) }],
}
}
)
this.server.tool(
'container_file_delete',
'Delete file in the working directory',
{ args: FilePathParam },
async ({ args }) => {
const path = await stripProtocolFromFilePath(args.path)
const deleted = await this.userContainer.container_file_delete(path)
return {
content: [{ type: 'text', text: `File deleted: ${deleted}.` }],
}
}
)
this.server.tool(
'container_file_write',
'Create a new file with the provided contents in the working direcotry, overwriting the file if it already exists',
{ args: FileWrite },
async ({ args }) => {
args.path = await stripProtocolFromFilePath(args.path)
return {
content: [{ type: 'text', text: await this.userContainer.container_file_write(args) }],
}
}
)
this.server.tool(
'container_files_list',
'List working directory file tree. This just reads the contents of the current working directory',
async () => {
// Begin workaround using container read rather than ls:
const readFile = await this.userContainer.container_file_read('.')
return {
content: [
{
type: 'resource',
resource: {
text: readFile.type === 'text' ? readFile.textOutput : readFile.base64Output,
uri: `file://`,
mimeType: readFile.mimeType,
},
},
],
}
}
)
this.server.tool(
'container_file_read',
'Read a specific file or directory. Use this tool if you would like to read files or display them to the user. This allow you to get a displayable image for the user if there is an image file.',
{ args: FilePathParam },
async ({ args }) => {
const path = await stripProtocolFromFilePath(args.path)
const readFile = await this.userContainer.container_file_read(path)
return {
content: [
{
type: 'resource',
resource: {
...(readFile.type === 'text'
? { text: readFile.textOutput }
: { blob: readFile.base64Output }),
uri: `file://${path}`,
mimeType: readFile.mimeType,
},
},
],
}
}
)
}
}
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/api/cf1-integration.api.ts:
--------------------------------------------------------------------------------
```typescript
import { fetchCloudflareApi } from '../cloudflare-api'
import {
AssetCategoriesResponse,
AssetDetail,
AssetsResponse,
IntegrationResponse,
IntegrationsResponse,
} from '../types/cf1-integrations.types'
import { V4Schema } from '../v4-api'
import type { z } from 'zod'
import type {
zReturnedAssetCategoriesResult,
zReturnedAssetsResult,
zReturnedIntegrationResult,
zReturnedIntegrationsResult,
} from '../types/cf1-integrations.types'
interface BaseParams {
accountId: string
apiToken: string
}
interface PaginationParams {
page?: number
pageSize?: number
}
type IntegrationParams = BaseParams & { integrationIdParam: string }
type AssetCategoryParams = BaseParams & { type?: string; vendor?: string }
type AssetSearchParams = BaseParams & { searchTerm: string } & PaginationParams
type AssetByIdParams = BaseParams & { assetId: string }
type AssetByCategoryParams = BaseParams & { categoryId: string } & PaginationParams
type AssetByIntegrationParams = BaseParams & { integrationId: string } & PaginationParams
const buildParams = (baseParams: Record<string, string>, pagination?: PaginationParams) => {
const params = new URLSearchParams(baseParams)
if (pagination?.page) params.append('page', String(pagination.page))
if (pagination?.pageSize) params.append('page_size', String(pagination.pageSize))
return params
}
const buildIntegrationEndpoint = (integrationId: string) => `/casb/integrations/${integrationId}`
const buildAssetEndpoint = (assetId?: string) =>
assetId ? `/casb/assets/${assetId}` : '/casb/assets'
const buildAssetCategoryEndpoint = () => '/casb/asset_categories'
const makeApiCall = async <T>({
endpoint,
accountId,
apiToken,
responseSchema,
params,
}: {
endpoint: string
accountId: string
apiToken: string
responseSchema: z.ZodType<any>
params?: URLSearchParams
}): Promise<T> => {
try {
const fullEndpoint = params ? `${endpoint}?${params.toString()}` : endpoint
const data = await fetchCloudflareApi({
endpoint: fullEndpoint,
accountId,
apiToken,
responseSchema,
options: {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
})
return data.result as T
} catch (error) {
console.error(`API call failed for ${endpoint}:`, error)
throw error
}
}
// Resource-specific API call handlers
const makeIntegrationCall = <T>(params: IntegrationParams, responseSchema: z.ZodType<any>) =>
makeApiCall<T>({
endpoint: buildIntegrationEndpoint(params.integrationIdParam),
accountId: params.accountId,
apiToken: params.apiToken,
responseSchema,
})
const makeAssetCall = <T>(
params: BaseParams & PaginationParams,
responseSchema: z.ZodType<any>,
assetId?: string,
additionalParams?: Record<string, string>
) =>
makeApiCall<T>({
endpoint: buildAssetEndpoint(assetId),
accountId: params.accountId,
apiToken: params.apiToken,
responseSchema,
params: buildParams(additionalParams || {}, params),
})
const makeAssetCategoryCall = <T>(params: AssetCategoryParams, responseSchema: z.ZodType<any>) =>
makeApiCall<T>({
endpoint: buildAssetCategoryEndpoint(),
accountId: params.accountId,
apiToken: params.apiToken,
responseSchema,
params: buildParams({
...(params.vendor && { vendor: params.vendor }),
...(params.type && { type: params.type }),
}),
})
// Integration handlers
export async function handleIntegrationById(
params: IntegrationParams
): Promise<{ integration: zReturnedIntegrationResult | null }> {
const integration = await makeIntegrationCall<zReturnedIntegrationResult>(
params,
V4Schema(IntegrationResponse)
)
return { integration }
}
export async function handleIntegrations(
params: BaseParams
): Promise<{ integrations: zReturnedIntegrationsResult | null }> {
const integrations = await makeApiCall<zReturnedIntegrationsResult>({
endpoint: '/casb/integrations',
accountId: params.accountId,
apiToken: params.apiToken,
responseSchema: V4Schema(IntegrationsResponse),
})
return { integrations }
}
// Asset category handlers
export async function handleAssetCategories(
params: AssetCategoryParams
): Promise<{ categories: zReturnedAssetCategoriesResult | null }> {
const categories = await makeAssetCategoryCall<zReturnedAssetCategoriesResult>(
params,
V4Schema(AssetCategoriesResponse)
)
return { categories }
}
// Asset handlers
export async function handleAssets(params: BaseParams & PaginationParams) {
const assets = await makeAssetCall<zReturnedAssetsResult>(params, V4Schema(AssetsResponse))
return { assets }
}
export async function handleAssetsByIntegrationId(params: AssetByIntegrationParams) {
const assets = await makeAssetCall<zReturnedAssetsResult>(
params,
V4Schema(AssetsResponse),
undefined,
{ integration_id: params.integrationId }
)
return { assets }
}
export async function handleAssetById(params: AssetByIdParams) {
const asset = await makeAssetCall<zReturnedAssetsResult>(
params,
V4Schema(AssetDetail),
params.assetId
)
return { asset }
}
export async function handleAssetsByAssetCategoryId(params: AssetByCategoryParams) {
const assets = await makeAssetCall<zReturnedAssetsResult>(
params,
V4Schema(AssetsResponse),
undefined,
{ category_id: params.categoryId }
)
return { assets }
}
export async function handleAssetsSearch(params: AssetSearchParams) {
const assets = await makeAssetCall<zReturnedAssetsResult>(
params,
V4Schema(AssetsResponse),
undefined,
{ search: params.searchTerm }
)
return { assets }
}
```