This is page 3 of 27. Use http://codebase.md/cloudflare/mcp-server-cloudflare?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .dockerignore ├── .editorconfig ├── .eslintrc.cjs ├── .github │ ├── actions │ │ └── setup │ │ └── action.yml │ ├── ISSUE_TEMPLATE │ │ └── bug_report.md │ └── workflows │ ├── branches.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .syncpackrc.cjs ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── apps │ ├── ai-gateway │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── ai-gateway.app.ts │ │ │ ├── ai-gateway.context.ts │ │ │ ├── tools │ │ │ │ └── ai-gateway.tools.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── auditlogs │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── auditlogs.app.ts │ │ │ ├── auditlogs.context.ts │ │ │ └── tools │ │ │ └── auditlogs.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── autorag │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── autorag.app.ts │ │ │ ├── autorag.context.ts │ │ │ ├── tools │ │ │ │ └── autorag.tools.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── browser-rendering │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── browser.app.ts │ │ │ ├── browser.context.ts │ │ │ └── tools │ │ │ └── browser.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── cloudflare-one-casb │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── cf1-casb.app.ts │ │ │ ├── cf1-casb.context.ts │ │ │ └── tools │ │ │ └── integrations.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── demo-day │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── frontend │ │ │ ├── index.html │ │ │ ├── public │ │ │ │ ├── anthropic.svg │ │ │ │ ├── asana.svg │ │ │ │ ├── atlassian.svg │ │ │ │ ├── canva.svg │ │ │ │ ├── cloudflare_logo.svg │ │ │ │ ├── cloudflare.svg │ │ │ │ ├── dina.jpg │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── favicon.png │ │ │ │ ├── intercom.svg │ │ │ │ ├── linear.svg │ │ │ │ ├── matt.jpg │ │ │ │ ├── mcp_demo_day.svg │ │ │ │ ├── mcpog.png │ │ │ │ ├── more.svg │ │ │ │ ├── paypal.svg │ │ │ │ ├── pete.jpeg │ │ │ │ ├── sentry.svg │ │ │ │ ├── special_guest.png │ │ │ │ ├── square.svg │ │ │ │ ├── stripe.svg │ │ │ │ ├── sunil.jpg │ │ │ │ └── webflow.svg │ │ │ ├── script.js │ │ │ └── styles.css │ │ ├── package.json │ │ ├── src │ │ │ └── demo-day.app.ts │ │ ├── tsconfig.json │ │ ├── worker-configuration.d.ts │ │ └── wrangler.json │ ├── dex-analysis │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── dex-analysis.app.ts │ │ │ ├── dex-analysis.context.ts │ │ │ ├── tools │ │ │ │ └── dex-analysis.tools.ts │ │ │ └── warp_diag_reader.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── dns-analytics │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── dns-analytics.app.ts │ │ │ ├── dns-analytics.context.ts │ │ │ └── tools │ │ │ └── dex-analytics.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── docs-autorag │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── docs-autorag.app.ts │ │ │ ├── docs-autorag.context.ts │ │ │ └── tools │ │ │ └── docs-autorag.tools.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── docs-vectorize │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── docs-vectorize.app.ts │ │ │ └── docs-vectorize.context.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── graphql │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── graphql.app.ts │ │ │ ├── graphql.context.ts │ │ │ └── tools │ │ │ └── graphql.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── logpush │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── logpush.app.ts │ │ │ ├── logpush.context.ts │ │ │ └── tools │ │ │ └── logpush.tools.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── radar │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── radar.app.ts │ │ │ ├── radar.context.ts │ │ │ ├── tools │ │ │ │ ├── radar.tools.ts │ │ │ │ └── url-scanner.tools.ts │ │ │ ├── types │ │ │ │ ├── radar.ts │ │ │ │ └── url-scanner.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── sandbox-container │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── container │ │ │ ├── fileUtils.spec.ts │ │ │ ├── fileUtils.ts │ │ │ ├── sandbox.container.app.ts │ │ │ └── tsconfig.json │ │ ├── CONTRIBUTING.md │ │ ├── Dockerfile │ │ ├── evals │ │ │ ├── exec.eval.ts │ │ │ ├── files.eval.ts │ │ │ ├── initialize.eval.ts │ │ │ └── utils.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── server │ │ │ ├── containerHelpers.ts │ │ │ ├── containerManager.ts │ │ │ ├── containerMcp.ts │ │ │ ├── metrics.ts │ │ │ ├── prompts.ts │ │ │ ├── sandbox.server.app.ts │ │ │ ├── sandbox.server.context.ts │ │ │ ├── userContainer.ts │ │ │ ├── utils.spec.ts │ │ │ └── utils.ts │ │ ├── shared │ │ │ ├── consts.ts │ │ │ └── schema.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.evals.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── workers-bindings │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── evals │ │ │ ├── accounts.eval.ts │ │ │ ├── hyperdrive.eval.ts │ │ │ ├── kv_namespaces.eval.ts │ │ │ ├── types.d.ts │ │ │ └── utils.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── bindings.app.ts │ │ │ └── bindings.context.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.evals.ts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ ├── workers-builds │ │ ├── .dev.vars.example │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── tools │ │ │ │ └── workers-builds.tools.ts │ │ │ ├── workers-builds.app.ts │ │ │ └── workers-builds.context.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vite.config.mts │ │ ├── vitest.config.ts │ │ ├── worker-configuration.d.ts │ │ └── wrangler.jsonc │ └── workers-observability │ ├── .dev.vars.example │ ├── .eslintrc.cjs │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── package.json │ ├── README.md │ ├── src │ │ ├── tools │ │ │ └── workers-observability.tools.ts │ │ ├── workers-observability.app.ts │ │ └── workers-observability.context.ts │ ├── tsconfig.json │ ├── types.d.ts │ ├── vitest.config.ts │ ├── worker-configuration.d.ts │ └── wrangler.jsonc ├── CONTRIBUTING.md ├── implementation-guides │ ├── evals.md │ ├── tools.md │ └── type-validators.md ├── LICENSE ├── package.json ├── packages │ ├── eslint-config │ │ ├── CHANGELOG.md │ │ ├── default.cjs │ │ ├── package.json │ │ └── README.md │ ├── eval-tools │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── runTask.ts │ │ │ ├── scorers.ts │ │ │ └── test-models.ts │ │ ├── tsconfig.json │ │ ├── worker-configuration.d.ts │ │ └── wrangler.json │ ├── mcp-common │ │ ├── .eslintrc.cjs │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── api │ │ │ │ ├── account.api.ts │ │ │ │ ├── cf1-integration.api.ts │ │ │ │ ├── workers-builds.api.ts │ │ │ │ ├── workers-observability.api.ts │ │ │ │ ├── workers.api.ts │ │ │ │ └── zone.api.ts │ │ │ ├── api-handler.ts │ │ │ ├── api-token-mode.ts │ │ │ ├── cloudflare-api.ts │ │ │ ├── cloudflare-auth.ts │ │ │ ├── cloudflare-oauth-handler.ts │ │ │ ├── config.ts │ │ │ ├── constants.ts │ │ │ ├── durable-kv-store.ts │ │ │ ├── durable-objects │ │ │ │ └── user_details.do.ts │ │ │ ├── env.ts │ │ │ ├── format.spec.ts │ │ │ ├── format.ts │ │ │ ├── get-props.ts │ │ │ ├── mcp-error.ts │ │ │ ├── poll.ts │ │ │ ├── prompts │ │ │ │ └── docs-vectorize.prompts.ts │ │ │ ├── scopes.ts │ │ │ ├── sentry.ts │ │ │ ├── server.ts │ │ │ ├── tools │ │ │ │ ├── account.tools.ts │ │ │ │ ├── d1.tools.ts │ │ │ │ ├── docs-vectorize.tools.ts │ │ │ │ ├── hyperdrive.tools.ts │ │ │ │ ├── kv_namespace.tools.ts │ │ │ │ ├── r2_bucket.tools.ts │ │ │ │ ├── worker.tools.ts │ │ │ │ └── zone.tools.ts │ │ │ ├── types │ │ │ │ ├── cf1-integrations.types.ts │ │ │ │ ├── cloudflare-mcp-agent.types.ts │ │ │ │ ├── d1.types.ts │ │ │ │ ├── hyperdrive.types.ts │ │ │ │ ├── kv_namespace.types.ts │ │ │ │ ├── r2_bucket.types.ts │ │ │ │ ├── shared.types.ts │ │ │ │ ├── tools.types.ts │ │ │ │ ├── workers-builds.types.ts │ │ │ │ ├── workers-logs.types.ts │ │ │ │ └── workers.types.ts │ │ │ ├── utils.spec.ts │ │ │ ├── utils.ts │ │ │ └── v4-api.ts │ │ ├── tests │ │ │ └── utils │ │ │ └── cloudflare-mock.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ ├── vitest.config.ts │ │ └── worker-configuration.d.ts │ ├── mcp-observability │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── analytics-engine.ts │ │ │ ├── index.ts │ │ │ └── metrics.ts │ │ ├── tsconfig.json │ │ └── worker-configuration.d.ts │ ├── tools │ │ ├── .eslintrc.cjs │ │ ├── bin │ │ │ ├── run-changeset-new │ │ │ ├── run-eslint-workers │ │ │ ├── run-fix-deps │ │ │ ├── run-tsc │ │ │ ├── run-turbo │ │ │ ├── run-vitest │ │ │ ├── run-vitest-ci │ │ │ ├── run-wrangler-deploy │ │ │ ├── run-wrangler-types │ │ │ └── runx │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── bin │ │ │ │ └── runx.ts │ │ │ ├── changesets.spec.ts │ │ │ ├── changesets.ts │ │ │ ├── cmd │ │ │ │ └── deploy-published-packages.ts │ │ │ ├── proc.ts │ │ │ ├── test │ │ │ │ ├── fixtures │ │ │ │ │ └── changesets │ │ │ │ │ ├── empty │ │ │ │ │ │ └── .gitkeep │ │ │ │ │ ├── invalid-json │ │ │ │ │ │ └── published-packages.json │ │ │ │ │ ├── invalid-schema │ │ │ │ │ │ └── published-packages.json │ │ │ │ │ └── valid │ │ │ │ │ └── published-packages.json │ │ │ │ └── setup.ts │ │ │ └── tsconfig.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── typescript-config │ ├── CHANGELOG.md │ ├── package.json │ ├── tools.json │ ├── workers-lib.json │ └── workers.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── server.json ├── tsconfig.json ├── turbo.json └── vitest.workspace.ts ``` # Files -------------------------------------------------------------------------------- /apps/sandbox-container/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "containers-mcp", 3 | "version": "0.2.6", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "check:types": "run-tsc", 8 | "check:lint": "run-eslint-workers", 9 | "deploy": "run-wrangler-deploy", 10 | "dev": "concurrently \"tsx container/sandbox.container.app.ts\" \"wrangler dev --var \"ENVIRONMENT:dev\"\"", 11 | "build:container": "docker build --platform linux/amd64 --tag sandbox-container:$(git rev-parse --short HEAD) -f Dockerfile ../../ && wrangler containers push sandbox-container:$(git rev-parse --short HEAD)", 12 | "start": "wrangler dev", 13 | "start:container": "tsx container/sandbox.container.app.ts", 14 | "postinstall": "mkdir -p workdir", 15 | "test": "vitest", 16 | "types": "wrangler types --include-env=false", 17 | "eval:dev": "start-server-and-test --expect 404 eval:server http://localhost:8976 'vitest --testTimeout=60000 --config vitest.config.evals.ts'", 18 | "eval:server": "concurrently \"tsx container/sandbox.container.app.ts\" \"wrangler dev --var ENVIRONMENT:test --var DEV_DISABLE_OAUTH:true --var DEV_CLOUDFLARE_EMAIL:[email protected]\"", 19 | "eval:ci": "start-server-and-test --expect 404 eval:server http://localhost:8976 'vitest run --testTimeout=60000 --config vitest.config.evals.ts'" 20 | }, 21 | "dependencies": { 22 | "@cloudflare/workers-oauth-provider": "0.0.5", 23 | "@hono/node-server": "1.13.8", 24 | "@hono/zod-validator": "0.4.3", 25 | "@modelcontextprotocol/sdk": "1.18.2", 26 | "@n8n/json-schema-to-zod": "1.1.0", 27 | "@repo/eval-tools": "workspace:*", 28 | "@repo/mcp-common": "workspace:*", 29 | "@repo/mcp-observability": "workspace:*", 30 | "agents": "0.2.7", 31 | "cron-schedule": "5.0.4", 32 | "esbuild": "0.25.1", 33 | "hono": "4.7.6", 34 | "mime": "4.0.6", 35 | "simple-git-hooks": "2.12.1", 36 | "tsx": "4.19.3", 37 | "vitest-evals": "0.1.4", 38 | "zod": "3.24.2" 39 | }, 40 | "devDependencies": { 41 | "@cloudflare/vitest-pool-workers": "0.8.14", 42 | "@types/mock-fs": "4.13.4", 43 | "@types/node": "22.14.1", 44 | "ai": "4.3.10", 45 | "concurrently": "9.1.2", 46 | "mock-fs": "5.5.0", 47 | "start-server-and-test": "2.0.11", 48 | "wrangler": "4.10.0" 49 | } 50 | } 51 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | 7 | env: 8 | FORCE_COLOR: 1 9 | 10 | jobs: 11 | create-release-pr: 12 | name: Create Release PR 13 | runs-on: ubuntu-24.04 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | timeout-minutes: 5 18 | concurrency: ${{ github.workflow }}-create-release-pr 19 | outputs: 20 | published: ${{ steps.create-release-pr.outputs.published }} 21 | steps: 22 | - name: Checkout Repo 23 | uses: actions/checkout@v4 24 | - uses: ./.github/actions/setup 25 | - name: Create Release PR 26 | id: create-release-pr 27 | uses: changesets/action@v1 28 | with: 29 | publish: pnpm changeset publish 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | - name: Save Published Packages 33 | if: steps.create-release-pr.outputs.published == 'true' 34 | run: | 35 | echo '${{steps.create-release-pr.outputs.publishedPackages}}' \ 36 | > ${{ github.workspace }}/published-packages.json 37 | - name: Upload Published Packages 38 | if: steps.create-release-pr.outputs.published == 'true' 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: published-packages 42 | path: ${{ github.workspace }}/published-packages.json 43 | 44 | deploy-production: 45 | name: Deploy (production) 46 | needs: create-release-pr 47 | if: needs.create-release-pr.outputs.published == 'true' 48 | runs-on: ubuntu-24.04 49 | timeout-minutes: 10 50 | concurrency: ${{ github.workflow }}-deploy-production 51 | permissions: 52 | contents: read 53 | steps: 54 | - name: Checkout Repo 55 | uses: actions/checkout@v4 56 | - name: Download published packages 57 | uses: actions/download-artifact@v4 58 | with: 59 | name: published-packages 60 | path: ${{ runner.temp }} 61 | - uses: ./.github/actions/setup 62 | - name: Deploy Published Workers (production) 63 | run: pnpm runx deploy-published-workers --env production 64 | env: 65 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 66 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 67 | ``` -------------------------------------------------------------------------------- /apps/sandbox-container/server/sandbox.server.app.ts: -------------------------------------------------------------------------------- ```typescript 1 | import OAuthProvider from '@cloudflare/workers-oauth-provider' 2 | 3 | import { createApiHandler } from '@repo/mcp-common/src/api-handler' 4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' 5 | import { 6 | createAuthHandlers, 7 | handleTokenExchangeCallback, 8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler' 9 | import { getEnv } from '@repo/mcp-common/src/env' 10 | import { RequiredScopes } from '@repo/mcp-common/src/scopes' 11 | import { MetricsTracker } from '@repo/mcp-observability' 12 | 13 | import { ContainerManager } from './containerManager' 14 | import { ContainerMcpAgent } from './containerMcp' 15 | import { UserContainer } from './userContainer' 16 | 17 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' 18 | import type { Env } from './sandbox.server.context' 19 | 20 | export { ContainerManager, ContainerMcpAgent, UserContainer } 21 | 22 | const env = getEnv<Env>() 23 | 24 | const metrics = new MetricsTracker(env.MCP_METRICS, { 25 | name: env.MCP_SERVER_NAME, 26 | version: env.MCP_SERVER_VERSION, 27 | }) 28 | 29 | // Context from the auth process, encrypted & stored in the auth token 30 | // and provided to the DurableMCP as this.props 31 | export type Props = AuthProps 32 | 33 | const ContainerScopes = { 34 | ...RequiredScopes, 35 | 'account:read': 'See your account info such as account details, analytics, and memberships.', 36 | } as const 37 | 38 | export default { 39 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { 40 | if (await isApiTokenRequest(req, env)) { 41 | return await handleApiTokenMode(ContainerMcpAgent, req, env, ctx) 42 | } 43 | 44 | return new OAuthProvider({ 45 | apiRoute: ['/mcp', '/sse'], 46 | apiHandler: createApiHandler(ContainerMcpAgent), 47 | // @ts-ignore 48 | defaultHandler: createAuthHandlers({ scopes: ContainerScopes, metrics }), 49 | authorizeEndpoint: '/oauth/authorize', 50 | tokenEndpoint: '/token', 51 | tokenExchangeCallback: (options) => 52 | handleTokenExchangeCallback( 53 | options, 54 | env.CLOUDFLARE_CLIENT_ID, 55 | env.CLOUDFLARE_CLIENT_SECRET 56 | ), 57 | // Cloudflare access token TTL 58 | accessTokenTTL: 3600, 59 | clientRegistrationEndpoint: '/register', 60 | }).fetch(req, env, ctx) 61 | }, 62 | } 63 | ``` -------------------------------------------------------------------------------- /apps/docs-vectorize/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # docs-vectorize 2 | 3 | ## 0.4.1 4 | 5 | ### Patch Changes 6 | 7 | - 43f493d: Update agent + modelcontextprotocol deps 8 | - Updated dependencies [43f493d] 9 | - @repo/[email protected] 10 | - @repo/[email protected] 11 | 12 | ## 0.4.0 13 | 14 | ### Minor Changes 15 | 16 | - dee0a7b: Updated the model for docs search to embeddinggemma-300m 17 | 18 | ## 0.3.3 19 | 20 | ### Patch Changes 21 | 22 | - 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools 23 | - Updated dependencies [24dd872] 24 | - @repo/[email protected] 25 | 26 | ## 0.3.2 27 | 28 | ### Patch Changes 29 | 30 | - 7422e71: Update MCP sdk 31 | - Updated dependencies [7422e71] 32 | - @repo/[email protected] 33 | - @repo/[email protected] 34 | 35 | ## 0.3.1 36 | 37 | ### Patch Changes 38 | 39 | - cc6d41f: Update agents deps & modelcontextprotocol 40 | - Updated dependencies [1833c6d] 41 | - Updated dependencies [cc6d41f] 42 | - @repo/[email protected] 43 | - @repo/[email protected] 44 | 45 | ## 0.3.0 46 | 47 | ### Minor Changes 48 | 49 | - f885d07: Add search docs tool to bindings and obs servers 50 | 51 | ### Patch Changes 52 | 53 | - Updated dependencies [f885d07] 54 | - @repo/[email protected] 55 | 56 | ## 0.2.1 57 | 58 | ### Patch Changes 59 | 60 | - Updated dependencies [83e2d19] 61 | - @repo/[email protected] 62 | 63 | ## 0.2.0 64 | 65 | ### Minor Changes 66 | 67 | - 89bfaf4: feat: add Pages to Workers migration guide to docs-vectorize MCP server 68 | 69 | ## 0.1.0 70 | 71 | ### Minor Changes 72 | 73 | - 6cf52a6: Support AOT tokens 74 | 75 | ### Patch Changes 76 | 77 | - 0fc4439: Update agents and modelcontext dependencies 78 | - Updated dependencies [6cf52a6] 79 | - Updated dependencies [0fc4439] 80 | - @repo/[email protected] 81 | - @repo/[email protected] 82 | 83 | ## 0.0.4 84 | 85 | ### Patch Changes 86 | 87 | - 3677a18: Remove extraneous log 88 | - Updated dependencies [3677a18] 89 | - @repo/[email protected] 90 | 91 | ## 0.0.3 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [86c2e4f] 96 | - @repo/[email protected] 97 | 98 | ## 0.0.2 99 | 100 | ### Patch Changes 101 | 102 | - cf3771b: chore: add suffixes to common files in apps and packages 103 | 104 | 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. 105 | 106 | - Updated dependencies [cf3771b] 107 | - @repo/[email protected] 108 | - @repo/[email protected] 109 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/cloudflare-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Cloudflare } from 'cloudflare' 2 | import { env } from 'cloudflare:workers' 3 | 4 | import type { z } from 'zod' 5 | 6 | export function getCloudflareClient(apiToken: string) { 7 | // @ts-expect-error We don't have actual env in this package 8 | if (env.DEV_DISABLE_OAUTH) { 9 | return new Cloudflare({ 10 | // @ts-expect-error We don't have actual env in this package, but we know this is defined because the initial Oauth handshake will fail without it 11 | apiEmail: env.DEV_CLOUDFLARE_EMAIL, 12 | // @ts-expect-error We don't have actual env in this package, but we know this is defined because the initial Oauth handshake will fail without it 13 | apiKey: env.DEV_CLOUDFLARE_API_TOKEN, 14 | }) 15 | } 16 | 17 | return new Cloudflare({ apiToken }) 18 | } 19 | 20 | /** 21 | * Makes a request to the Cloudflare API 22 | * @param endpoint API endpoint path (without the base URL) 23 | * @param accountId Cloudflare account ID 24 | * @param apiToken Cloudflare API token 25 | * @param options Additional fetch options 26 | * @returns The API response 27 | */ 28 | export async function fetchCloudflareApi<T>({ 29 | endpoint, 30 | accountId, 31 | apiToken, 32 | responseSchema, 33 | options = {}, 34 | }: { 35 | endpoint: string 36 | accountId: string 37 | apiToken: string 38 | responseSchema?: z.ZodType<T> 39 | options?: RequestInit 40 | }): Promise<T> { 41 | const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}${endpoint}` 42 | 43 | // @ts-expect-error We don't have actual env in this package 44 | if (env.DEV_DISABLE_OAUTH) { 45 | options.headers = { 46 | ...options.headers, 47 | // @ts-expect-error We don't have actual env in this package 48 | 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, 49 | // @ts-expect-error We don't have actual env in this package 50 | 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, 51 | } 52 | } 53 | const response = await fetch(url, { 54 | ...options, 55 | headers: { 56 | Authorization: `Bearer ${apiToken}`, 57 | ...(options.headers || {}), 58 | }, 59 | }) 60 | 61 | if (!response.ok) { 62 | const error = await response.text() 63 | throw new Error(`Cloudflare API request failed: ${error}`) 64 | } 65 | 66 | const data = await response.json() 67 | 68 | // If a schema is provided, validate the response 69 | if (responseSchema) { 70 | return responseSchema.parse(data) 71 | } 72 | 73 | return data as T 74 | } 75 | ``` -------------------------------------------------------------------------------- /apps/sandbox-container/server/containerHelpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const MAX_CONTAINERS = 50 2 | export async function startAndWaitForPort( 3 | environment: 'dev' | 'prod' | 'test', 4 | container: Container | undefined, 5 | portToAwait: number, 6 | maxTries = 10 7 | ): Promise<boolean> { 8 | if (environment === 'dev' || environment === 'test') { 9 | console.log('Running in dev, assuming locally running container') 10 | return true 11 | } 12 | 13 | if (!container) { 14 | throw new Error('Error: ctx.container is undefined. Does this DO support containers?') 15 | } 16 | 17 | const port = container.getTcpPort(portToAwait) 18 | // promise to make sure the container does not exit 19 | let monitor 20 | 21 | for (let i = 0; i < maxTries; i++) { 22 | try { 23 | if (!container.running) { 24 | console.log('starting container') 25 | container.start({ 26 | enableInternet: true, 27 | }) 28 | 29 | // force DO to keep track of running state 30 | monitor = container.monitor() 31 | void monitor.then(() => console.log('Container exited')) 32 | } 33 | 34 | const conn = await port.connect(`10.0.0.1:${portToAwait}`) 35 | await conn.close() 36 | console.log('Connected') 37 | return true 38 | } catch (err: any) { 39 | if (!(err instanceof Error)) { 40 | throw err 41 | } 42 | 43 | console.error('Error connecting to the container on', i, 'try', err) 44 | 45 | if (err.message.includes('listening')) { 46 | await new Promise((res) => setTimeout(res, 300)) 47 | continue 48 | } 49 | 50 | // no container yet 51 | if (err.message.includes('there is no container instance that can be provided')) { 52 | await new Promise((res) => setTimeout(res, 300)) 53 | continue 54 | } 55 | 56 | console.log(err) 57 | return false 58 | } 59 | } 60 | 61 | return false 62 | } 63 | 64 | export async function proxyFetch( 65 | environment: 'dev' | 'prod' | 'test', 66 | container: Container | undefined, 67 | request: Request, 68 | portNumber: number 69 | ): Promise<Response> { 70 | if (environment === 'dev' || environment === 'test') { 71 | const url = request.url 72 | .replace('https://', 'http://') 73 | .replace('http://host', 'http://localhost') 74 | return fetch(url, request.clone() as Request) 75 | } 76 | 77 | if (!container) { 78 | throw new Error('Error: ctx.container is undefined. Does this DO support containers?') 79 | } 80 | 81 | return await container 82 | .getTcpPort(portNumber) 83 | .fetch(request.url.replace('https://', 'http://'), request.clone() as Request) 84 | } 85 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/api-token-mode.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getUserAndAccounts } from './cloudflare-oauth-handler' 2 | 3 | import type { McpAgent } from 'agents/mcp' 4 | import type { AuthProps } from './cloudflare-oauth-handler' 5 | 6 | interface RequiredEnv { 7 | DEV_CLOUDFLARE_API_TOKEN: string 8 | DEV_CLOUDFLARE_EMAIL: string 9 | DEV_DISABLE_OAUTH: string 10 | } 11 | 12 | export async function isApiTokenRequest(req: Request, env: RequiredEnv) { 13 | // shortcircuit for dev 14 | if (env.DEV_CLOUDFLARE_API_TOKEN && env.DEV_DISABLE_OAUTH === 'true') { 15 | return true 16 | } 17 | 18 | const authHeader = req.headers.get('Authorization') 19 | if (!authHeader) return false 20 | 21 | const [type, token] = authHeader.split(' ') 22 | if (type !== 'Bearer') return false 23 | 24 | // Return true only if the token was issued by the OAuthProvider. 25 | // A token provisioned by the OAuthProvider has 3 parts, split by colons. 26 | const codeParts = token.split(':') 27 | return codeParts.length !== 3 28 | } 29 | 30 | export async function handleApiTokenMode< 31 | T extends typeof McpAgent<unknown, unknown, Record<string, unknown>>, 32 | >(agent: T, req: Request, env: RequiredEnv, ctx: ExecutionContext) { 33 | // Handle global API token case 34 | let opts, token 35 | // dev mode 36 | if ( 37 | env.DEV_CLOUDFLARE_API_TOKEN && 38 | env.DEV_CLOUDFLARE_EMAIL && 39 | env.DEV_DISABLE_OAUTH === 'true' 40 | ) { 41 | opts = { 42 | 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, 43 | 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, 44 | } 45 | token = env.DEV_CLOUDFLARE_API_TOKEN 46 | // header mode 47 | } else { 48 | const authHeader = req.headers.get('Authorization') 49 | if (!authHeader) { 50 | throw new Error('Authorization header is required') 51 | } 52 | 53 | const [type, tokenStr] = authHeader.split(' ') 54 | if (type !== 'Bearer') { 55 | throw new Error('Invalid authorization type, must be Bearer') 56 | } 57 | token = tokenStr 58 | } 59 | 60 | const { user, accounts } = await getUserAndAccounts(token, opts) 61 | 62 | // If user is null, handle API token mode 63 | if (user === null) { 64 | ctx.props = { 65 | type: 'account_token', 66 | accessToken: token, 67 | // we always select the first account from the response, 68 | // this assumes that account owned tokens can only access one account 69 | account: accounts[0], 70 | } satisfies AuthProps 71 | } else { 72 | ctx.props = { 73 | type: 'user_token', 74 | accessToken: token, 75 | user, 76 | accounts, 77 | } satisfies AuthProps 78 | } 79 | return agent.mount('/sse').fetch(req, env, ctx) 80 | } 81 | ``` -------------------------------------------------------------------------------- /apps/dex-analysis/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # dex-analysis 2 | 3 | ## 0.2.2 4 | 5 | ### Patch Changes 6 | 7 | - 43f493d: Update agent + modelcontextprotocol deps 8 | - Updated dependencies [43f493d] 9 | - @repo/[email protected] 10 | - @repo/[email protected] 11 | 12 | ## 0.2.1 13 | 14 | ### Patch Changes 15 | 16 | - 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools 17 | - Updated dependencies [24dd872] 18 | - @repo/[email protected] 19 | 20 | ## 0.2.0 21 | 22 | ### Minor Changes 23 | 24 | - 9496e21: Add tool for analyzing WARP-diags for common issues via bonobo 25 | 26 | ## 0.1.7 27 | 28 | ### Patch Changes 29 | 30 | - dffbd36: Improve DEX remote captures tools, separate by type for clarity 31 | 32 | ## 0.1.6 33 | 34 | ### Patch Changes 35 | 36 | - d672471: dex-analysis: add WARP diag analysis tools and reader D.O. 37 | 38 | ## 0.1.5 39 | 40 | ### Patch Changes 41 | 42 | - 016cb73: Add more DEX tools, including remote captures 43 | 44 | ## 0.1.4 45 | 46 | ### Patch Changes 47 | 48 | - 7422e71: Update MCP sdk 49 | - Updated dependencies [7422e71] 50 | - @repo/[email protected] 51 | - @repo/[email protected] 52 | 53 | ## 0.1.3 54 | 55 | ### Patch Changes 56 | 57 | - cc6d41f: Update agents deps & modelcontextprotocol 58 | - Updated dependencies [1833c6d] 59 | - Updated dependencies [cc6d41f] 60 | - @repo/[email protected] 61 | - @repo/[email protected] 62 | 63 | ## 0.1.2 64 | 65 | ### Patch Changes 66 | 67 | - Updated dependencies [f885d07] 68 | - @repo/[email protected] 69 | 70 | ## 0.1.1 71 | 72 | ### Patch Changes 73 | 74 | - Updated dependencies [83e2d19] 75 | - @repo/[email protected] 76 | 77 | ## 0.1.0 78 | 79 | ### Minor Changes 80 | 81 | - 6cf52a6: Support AOT tokens 82 | 83 | ### Patch Changes 84 | 85 | - 0fc4439: Update agents and modelcontext dependencies 86 | - Updated dependencies [6cf52a6] 87 | - Updated dependencies [0fc4439] 88 | - @repo/[email protected] 89 | - @repo/[email protected] 90 | 91 | ## 0.0.4 92 | 93 | ### Patch Changes 94 | 95 | - 3677a18: Remove extraneous log 96 | - Updated dependencies [3677a18] 97 | - @repo/[email protected] 98 | 99 | ## 0.0.3 100 | 101 | ### Patch Changes 102 | 103 | - 86c2e4f: Add API token passthrough auth 104 | - Updated dependencies [86c2e4f] 105 | - @repo/[email protected] 106 | 107 | ## 0.0.2 108 | 109 | ### Patch Changes 110 | 111 | - cf3771b: chore: add suffixes to common files in apps and packages 112 | 113 | 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. 114 | 115 | - Updated dependencies [cf3771b] 116 | - @repo/[email protected] 117 | - @repo/[email protected] 118 | ``` -------------------------------------------------------------------------------- /apps/sandbox-container/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # containers-mcp 2 | 3 | ## 0.2.6 4 | 5 | ### Patch Changes 6 | 7 | - 43f493d: Update agent + modelcontextprotocol deps 8 | - Updated dependencies [43f493d] 9 | - @repo/[email protected] 10 | - @repo/[email protected] 11 | - @repo/[email protected] 12 | 13 | ## 0.2.5 14 | 15 | ### Patch Changes 16 | 17 | - 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools 18 | - Updated dependencies [24dd872] 19 | - @repo/[email protected] 20 | - @repo/[email protected] 21 | 22 | ## 0.2.4 23 | 24 | ### Patch Changes 25 | 26 | - dffbd36: Use proper wrangler deploy in all servers so we get the name and version 27 | 28 | ## 0.2.3 29 | 30 | ### Patch Changes 31 | 32 | - 7422e71: Update MCP sdk 33 | - Updated dependencies [7422e71] 34 | - @repo/[email protected] 35 | - @repo/[email protected] 36 | 37 | ## 0.2.2 38 | 39 | ### Patch Changes 40 | 41 | - cc6d41f: Update agents deps & modelcontextprotocol 42 | - Updated dependencies [1833c6d] 43 | - Updated dependencies [cc6d41f] 44 | - @repo/[email protected] 45 | - @repo/[email protected] 46 | - @repo/[email protected] 47 | 48 | ## 0.2.1 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies [f885d07] 53 | - @repo/[email protected] 54 | 55 | ## 0.2.0 56 | 57 | ### Minor Changes 58 | 59 | - 2621557: Use new workers:read scope instead of workers:write, as these mcp servers don't require workers write permissions 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [83e2d19] 64 | - @repo/[email protected] 65 | 66 | ## 0.1.0 67 | 68 | ### Minor Changes 69 | 70 | - 6cf52a6: Support AOT tokens 71 | 72 | ### Patch Changes 73 | 74 | - 0fc4439: Update agents and modelcontext dependencies 75 | - Updated dependencies [6cf52a6] 76 | - Updated dependencies [0fc4439] 77 | - @repo/[email protected] 78 | - @repo/[email protected] 79 | - @repo/[email protected] 80 | 81 | ## 0.0.4 82 | 83 | ### Patch Changes 84 | 85 | - 3677a18: Remove extraneous log 86 | - Updated dependencies [3677a18] 87 | - @repo/[email protected] 88 | 89 | ## 0.0.3 90 | 91 | ### Patch Changes 92 | 93 | - 86c2e4f: Add API token passthrough auth 94 | - Updated dependencies [86c2e4f] 95 | - @repo/[email protected] 96 | 97 | ## 0.0.2 98 | 99 | ### Patch Changes 100 | 101 | - cf3771b: chore: add suffixes to common files in apps and packages 102 | 103 | 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. 104 | 105 | - Updated dependencies [cf3771b] 106 | - @repo/[email protected] 107 | - @repo/[email protected] 108 | - @repo/[email protected] 109 | ``` -------------------------------------------------------------------------------- /apps/sandbox-container/container/fileUtils.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import mime from 'mime' 2 | import mock from 'mock-fs' 3 | import { afterEach, describe, expect, it, vi } from 'vitest' 4 | 5 | import { get_file_name_from_path, get_mime_type, list_files_in_directory } from './fileUtils' 6 | 7 | vi.mock('mime', () => { 8 | return { 9 | default: { 10 | getType: vi.fn(), 11 | }, 12 | } 13 | }) 14 | 15 | afterEach(async () => { 16 | mock.restore() 17 | vi.restoreAllMocks() 18 | }) 19 | 20 | describe('get_file_name_from_path', () => { 21 | it('strips files/contents', async () => { 22 | const path = await get_file_name_from_path('/files/contents/cats') 23 | expect(path).toBe('/cats') 24 | }), 25 | it('works if files/contents is not present', async () => { 26 | const path = await get_file_name_from_path('/dogs') 27 | expect(path).toBe('/dogs') 28 | }), 29 | it('strips a trailing slash', async () => { 30 | const path = await get_file_name_from_path('/files/contents/birds/') 31 | expect(path).toBe('/birds') 32 | }) 33 | }), 34 | describe('list_files_in_directory', () => { 35 | it('lists the files in a directory', async () => { 36 | mock({ 37 | testDir: { 38 | cats: 'aurora, luna', 39 | dogs: 'penny', 40 | }, 41 | }) 42 | const listFiles = await list_files_in_directory('testDir') 43 | expect(listFiles).toEqual(['file:///testDir/cats', 'file:///testDir/dogs']) 44 | }), 45 | it('throws an error if path is not a directory', async () => { 46 | mock({ 47 | testDir: { 48 | cats: 'aurora, luna', 49 | dogs: 'penny', 50 | }, 51 | }) 52 | await expect(async () => await list_files_in_directory('testDir/cats')).rejects.toThrow( 53 | 'Failed to read directory' 54 | ) 55 | }), 56 | it('treats empty strings as cwd', async () => { 57 | mock({ 58 | testDir: { 59 | cats: 'aurora, luna', 60 | dogs: 'penny', 61 | }, 62 | }) 63 | 64 | const listFiles = await list_files_in_directory('') 65 | expect(listFiles).toEqual(['file:///../../../../../../testDir']) 66 | }) 67 | }), 68 | describe('get_mime_type', async () => { 69 | it("provides the natural mime type when not 'inode/directory'", async () => { 70 | vi.mocked(mime.getType).mockReturnValueOnce('theType') 71 | const mimeType = await get_mime_type('someFile') 72 | expect(mimeType).toEqual('theType') 73 | }) 74 | it("overrides mime type for 'inode/directory'", async () => { 75 | vi.mocked(mime.getType).mockReturnValueOnce('inode/directory') 76 | const mimeType = await get_mime_type('someDirectory') 77 | expect(mimeType).toEqual('text/directory') 78 | }) 79 | }) 80 | ``` -------------------------------------------------------------------------------- /apps/dex-analysis/src/warp_diag_reader.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { DurableObject } from 'cloudflare:workers' 2 | import JSZip from 'jszip' 3 | 4 | import type { Env } from './dex-analysis.context' 5 | 6 | // Helper for reading large WARP diag zip archives. 7 | // Holds the contents in memory between requests from the agent for specific files 8 | // instead of having the worker download the zip on every request. 9 | // 10 | // Each DO represents one remote capture zip 11 | export class WarpDiagReader extends DurableObject<Env> { 12 | #cache?: { files: string[]; zip: JSZip } 13 | 14 | // List the files in the zip for the agent 15 | async list(accessToken: string, url: string) { 16 | const { files } = await this.#getZip(accessToken, url) 17 | return files 18 | } 19 | 20 | // Return the contents of a file by path 21 | async read(accessToken: string, url: string, filepath: string) { 22 | const { zip } = await this.#getZip(accessToken, url) 23 | const file = zip.file(filepath) 24 | const content = await file?.async('text') 25 | return content 26 | } 27 | 28 | async #getZip(accessToken: string, url: string) { 29 | if (this.#cache) { 30 | return this.#cache 31 | } 32 | 33 | console.log(`WarpDiagReader fetching `, url) 34 | 35 | const res = await fetch(url, { 36 | headers: { 37 | Authorization: `Bearer ${accessToken}`, 38 | }, 39 | }) 40 | 41 | if (res.status !== 200) { 42 | throw new Error(`failed to download zip, non-200 status code: ${res.status}`) 43 | } 44 | 45 | const zip = await new JSZip().loadAsync(await res.arrayBuffer()) 46 | const files: string[] = [] 47 | for (const [relativePath, file] of Object.entries(zip.files)) { 48 | if (!file.dir) { 49 | files.push(relativePath) 50 | } 51 | } 52 | 53 | const cache = { files, zip } 54 | this.#cache = cache 55 | return cache 56 | } 57 | } 58 | 59 | async function hashToken(accessToken: string) { 60 | const hashArr = Array.from( 61 | new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(accessToken))) 62 | ) 63 | return hashArr.map((b) => b.toString(16).padStart(2, '0')).join('') 64 | } 65 | 66 | // Create unique name based on accessToken hash and download url. In order to read cached zip from memory 67 | // you need to have the same access token that was used to fetch it. 68 | async function readerName(accessToken: string, url: string) { 69 | return (await hashToken(accessToken)) + url 70 | } 71 | 72 | export async function getReader(env: Env, accessToken: string, download: string) { 73 | const name = await readerName(accessToken, download) 74 | const id = env.WARP_DIAG_READER.idFromName(name) 75 | return env.WARP_DIAG_READER.get(id) 76 | } 77 | ``` -------------------------------------------------------------------------------- /apps/demo-day/frontend/public/cloudflare.svg: -------------------------------------------------------------------------------- ``` 1 | <svg width="531" height="240" viewBox="0 0 531 240" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path d="M364.276 226.805L364.273 226.815L361.598 236.302C361.597 236.305 361.597 236.307 361.596 236.309C361.113 237.921 359.639 239 358.062 239H4.49414C3.17374 239 2.00552 237.981 1.76785 236.575C1.29745 232.723 1 228.886 1 225.057C1 181.893 35.2676 146.875 78.1587 145.596L79.4005 145.559L79.099 144.353C77.5739 138.256 77.146 131.677 77.7417 124.952C80.333 98.8682 101.26 77.9116 127.401 75.3232C142.053 73.966 155.679 78.2521 166.379 86.1874L167.481 87.0051L167.922 85.705C184.598 36.4752 231.241 1 286.199 1C345.788 1 395.645 42.6457 407.99 98.5134L407.993 98.5269L407.997 98.5403C408.24 99.5149 408.147 100.557 407.73 101.656C407.73 101.656 407.73 101.657 407.729 101.657L396.668 130.417L396.668 130.418C392.403 141.573 384.13 151.739 374.079 159.251C364.03 166.762 352.276 171.568 341.092 172.101L147.818 174.606L147.792 174.606L147.766 174.608C145.391 174.762 143.124 176.442 142.386 178.708L142.374 178.745L142.365 178.783C142.011 180.247 142.191 181.794 142.974 182.975C143.77 184.218 145.169 184.982 146.743 184.984C146.746 184.984 146.748 184.984 146.751 184.984L338.283 187.489C348.152 187.956 356.255 191.91 361.006 198.6L361.009 198.603C366.233 205.897 367.495 215.839 364.276 226.805Z" stroke="white" stroke-width="1"/> 3 | <path d="M420.981 105.214H421.032L421.083 105.209C422.708 105.042 424.513 105.041 426.253 105.041C483.434 105.041 530.001 151.584 530.001 208.558C530.001 218.224 528.631 227.635 526.148 236.579L526.145 236.591L526.142 236.604C525.832 237.842 524.712 238.654 523.395 238.654H377.422C376.423 238.654 375.85 237.79 376.193 236.908C376.193 236.907 376.193 236.907 376.193 236.907L379.304 228.962L379.307 228.955C383.572 217.821 391.803 207.666 401.832 200.159C411.862 192.651 423.623 187.843 434.899 187.315L434.911 187.314L477.389 184.809L477.395 184.809C479.77 184.654 482.037 182.974 482.775 180.709L482.782 180.689L482.787 180.669C483.162 179.333 482.96 177.655 482.193 176.45C481.402 175.209 480.011 174.443 478.443 174.432L437.679 171.929L437.672 171.929L437.665 171.928C427.789 171.464 419.679 167.51 414.925 160.817L414.923 160.813C409.698 153.519 408.436 143.577 411.656 132.611L411.657 132.606L419.119 106.698C419.497 105.775 420.265 105.214 420.981 105.214Z" stroke="white" stroke-width="1"/> 4 | </svg> 5 | ``` -------------------------------------------------------------------------------- /apps/workers-observability/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # workers-observability 2 | 3 | ## 0.4.1 4 | 5 | ### Patch Changes 6 | 7 | - 43f493d: Update agent + modelcontextprotocol deps 8 | - Updated dependencies [43f493d] 9 | - @repo/[email protected] 10 | - @repo/[email protected] 11 | 12 | ## 0.4.0 13 | 14 | ### Minor Changes 15 | 16 | - dee0a7b: Updated the model for docs search to embeddinggemma-300m 17 | 18 | ## 0.3.4 19 | 20 | ### Patch Changes 21 | 22 | - 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools 23 | - Updated dependencies [24dd872] 24 | - @repo/[email protected] 25 | 26 | ## 0.3.3 27 | 28 | ### Patch Changes 29 | 30 | - dffbd36: Use proper wrangler deploy in all servers so we get the name and version 31 | 32 | ## 0.3.2 33 | 34 | ### Patch Changes 35 | 36 | - 7422e71: Update MCP sdk 37 | - Updated dependencies [7422e71] 38 | - @repo/[email protected] 39 | - @repo/[email protected] 40 | 41 | ## 0.3.1 42 | 43 | ### Patch Changes 44 | 45 | - cc6d41f: Update agents deps & modelcontextprotocol 46 | - Updated dependencies [1833c6d] 47 | - Updated dependencies [cc6d41f] 48 | - @repo/[email protected] 49 | - @repo/[email protected] 50 | 51 | ## 0.3.0 52 | 53 | ### Minor Changes 54 | 55 | - f885d07: Add search docs tool to bindings and obs servers 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies [f885d07] 60 | - @repo/[email protected] 61 | 62 | ## 0.2.0 63 | 64 | ### Minor Changes 65 | 66 | - 2621557: Use new workers:read scope instead of workers:write, as these mcp servers don't require workers write permissions 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies [83e2d19] 71 | - @repo/[email protected] 72 | 73 | ## 0.1.0 74 | 75 | ### Minor Changes 76 | 77 | - 6cf52a6: Support AOT tokens 78 | 79 | ### Patch Changes 80 | 81 | - 0fc4439: Update agents and modelcontext dependencies 82 | - Updated dependencies [6cf52a6] 83 | - Updated dependencies [0fc4439] 84 | - @repo/[email protected] 85 | - @repo/[email protected] 86 | 87 | ## 0.0.4 88 | 89 | ### Patch Changes 90 | 91 | - 3677a18: Remove extraneous log 92 | - Updated dependencies [3677a18] 93 | - @repo/[email protected] 94 | 95 | ## 0.0.3 96 | 97 | ### Patch Changes 98 | 99 | - 86c2e4f: Add API token passthrough auth 100 | - Updated dependencies [86c2e4f] 101 | - @repo/[email protected] 102 | 103 | ## 0.0.2 104 | 105 | ### Patch Changes 106 | 107 | - b190e97: fix: set correct entrypoint in wrangler.jsonc 108 | - cf3771b: chore: add suffixes to common files in apps and packages 109 | 110 | 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. 111 | 112 | - Updated dependencies [cf3771b] 113 | - @repo/[email protected] 114 | - @repo/[email protected] 115 | ``` -------------------------------------------------------------------------------- /packages/eval-tools/src/runTask.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { type MCPClientManager } from 'agents/mcp/client' 2 | import { generateText, jsonSchema, tool } from 'ai' 3 | import { z } from 'zod' 4 | 5 | import type { GenerateTextResult, LanguageModelV1, ToolCallPart, ToolSet } from 'ai' 6 | 7 | export async function runTask( 8 | clientManager: MCPClientManager, 9 | model: LanguageModelV1, 10 | input: string 11 | ): Promise<{ 12 | promptOutput: string 13 | fullResult: GenerateTextResult<ToolSet, never> 14 | toolCalls: ToolCallPart[] 15 | }> { 16 | const tools = clientManager.listTools() 17 | const toolSet: ToolSet = tools.reduce((acc, v) => { 18 | if (!v.inputSchema.properties) { 19 | v.inputSchema.properties = {} 20 | } 21 | 22 | acc[v.name] = tool({ 23 | parameters: jsonSchema(v.inputSchema as any), 24 | description: v.description, 25 | execute: async (args: any, opts) => { 26 | try { 27 | const res = await clientManager.callTool( 28 | { 29 | ...v, 30 | arguments: { ...args }, 31 | }, 32 | z.any() as any, 33 | { signal: opts.abortSignal } 34 | ) 35 | return res.content 36 | } catch (e) { 37 | console.log('Error calling tool') 38 | console.log(e) 39 | return e 40 | } 41 | }, 42 | }) 43 | return acc 44 | }, {} as ToolSet) 45 | 46 | const res = await generateText({ 47 | model, 48 | system: 49 | "You are an assistant responsible for evaluating the results of calling various tools. Given the user's query, use the tools available to you to answer the question.", 50 | tools: toolSet, 51 | prompt: input, 52 | maxRetries: 1, 53 | maxSteps: 10, 54 | }) 55 | 56 | // convert into an LLM readable result so our factuality checker can validate tool calls 57 | let messagesWithTools = '' 58 | const toolCalls: ToolCallPart[] = [] 59 | const response = res.response 60 | const messages = response.messages 61 | 62 | for (const message of messages) { 63 | for (const messagePart of message.content) { 64 | if (typeof messagePart === 'string') { 65 | messagesWithTools += `<message_content type="text">${messagePart}</message_content>` 66 | } else if (messagePart.type === 'tool-call') { 67 | messagesWithTools += `<message_content type=${messagePart.type}> 68 | <tool_name>${messagePart.toolName}</tool_name> 69 | <tool_arguments>${JSON.stringify(messagePart.args)}</tool_arguments> 70 | </message_content>` 71 | toolCalls.push(messagePart) 72 | } else if (messagePart.type === 'text') { 73 | messagesWithTools += `<message_content type=${messagePart.type}>${messagePart.text}</message_content>` 74 | } 75 | } 76 | } 77 | 78 | return { promptOutput: messagesWithTools, fullResult: res, toolCalls } 79 | } 80 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/types/kv_namespace.types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import type { 4 | Namespace, 5 | NamespaceCreateParams, 6 | NamespaceDeleteParams, 7 | NamespaceGetParams, 8 | NamespaceListParams, 9 | NamespaceUpdateParams, 10 | } from 'cloudflare/resources/kv.mjs' 11 | 12 | /** 13 | * Zod schema for a KV namespace ID. 14 | */ 15 | export const KvNamespaceIdSchema: z.ZodType<Namespace['id']> = z 16 | .string() 17 | .describe('The ID of the KV namespace') 18 | 19 | /** 20 | * Zod schema for a KV namespace title. 21 | */ 22 | export const KvNamespaceTitleSchema: z.ZodType<Namespace['title']> = z 23 | .string() 24 | .describe('The human-readable name/title of the KV namespace') 25 | 26 | /** 27 | * Zod schema for the optional parameters when listing KV namespaces. 28 | */ 29 | export const KvNamespacesListParamsSchema: z.ZodType<Omit<NamespaceListParams, 'account_id'>> = z 30 | .object({ 31 | direction: z 32 | .enum(['asc', 'desc']) 33 | .optional() 34 | .describe('Direction to order namespaces (asc/desc)'), 35 | order: z.enum(['id', 'title']).optional().describe('Field to order namespaces by (id/title)'), 36 | page: z.number().int().positive().optional().describe('Page number of results (starts at 1)'), 37 | per_page: z 38 | .number() 39 | .int() 40 | .min(1) 41 | .max(100) 42 | .optional() 43 | .describe('Number of namespaces per page (1-100)'), 44 | }) 45 | .describe('Optional parameters for listing KV namespaces') 46 | 47 | /** 48 | * Zod schema for parameters needed to create a KV namespace. 49 | */ 50 | export const KvNamespaceCreateParamsSchema: z.ZodType<Omit<NamespaceCreateParams, 'account_id'>> = z 51 | .object({ 52 | title: KvNamespaceTitleSchema, 53 | }) 54 | .describe('Parameters for creating a KV namespace') 55 | 56 | /** 57 | * Zod schema for parameters needed to delete a KV namespace. 58 | */ 59 | export const KvNamespaceDeleteParamsSchema: z.ZodType<Omit<NamespaceDeleteParams, 'account_id'>> = z 60 | .object({ 61 | namespace_id: KvNamespaceIdSchema, 62 | }) 63 | .describe('Parameters for deleting a KV namespace') 64 | 65 | /** 66 | * Zod schema for parameters needed to get a KV namespace. 67 | */ 68 | export const KvNamespaceGetParamsSchema: z.ZodType<Omit<NamespaceGetParams, 'account_id'>> = z 69 | .object({ 70 | namespace_id: KvNamespaceIdSchema, 71 | }) 72 | .describe('Parameters for getting a KV namespace') 73 | 74 | /** 75 | * Zod schema for parameters needed to update a KV namespace. 76 | */ 77 | export const KvNamespaceUpdateParamsSchema: z.ZodType<Omit<NamespaceUpdateParams, 'account_id'>> = z 78 | .object({ 79 | namespace_id: KvNamespaceIdSchema, 80 | title: KvNamespaceTitleSchema, 81 | }) 82 | .describe('Parameters for updating a KV namespace') 83 | ``` -------------------------------------------------------------------------------- /packages/tools/src/changesets.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { program } from '@commander-js/extra-typings' 2 | import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' 3 | 4 | import { getPublishedPackages } from './changesets' 5 | 6 | describe('getPublishedPackages', () => { 7 | const fixturesDir = path.join(__dirname, 'test/fixtures/changesets') 8 | const fixture = (name: string) => path.join(fixturesDir, name) 9 | 10 | beforeAll(() => { 11 | // throw errors instead of calling process.exit(1) 12 | // within program.error() is called by cliError() 13 | program.exitOverride((e) => { 14 | throw e 15 | }) 16 | }) 17 | afterEach(() => { 18 | vi.unstubAllEnvs() 19 | }) 20 | 21 | it('should read and parse valid published packages', async () => { 22 | vi.stubEnv('RUNNER_TEMP', fixture('valid')) 23 | 24 | const result = await getPublishedPackages() 25 | 26 | expect(result).toStrictEqual([ 27 | { name: 'package-a', version: '1.0.0' }, 28 | { name: 'package-b', version: '2.1.3' }, 29 | ]) 30 | }) 31 | 32 | it('should throw error when RUNNER_TEMP is not set', async () => { 33 | vi.stubEnv('RUNNER_TEMP', undefined) 34 | 35 | await expect(getPublishedPackages()).rejects.toThrowErrorMatchingInlineSnapshot( 36 | `[CommanderError: error: ✖ $RUNNER_TEMP is not set]` 37 | ) 38 | }) 39 | 40 | it('should throw error when RUNNER_TEMP is empty', async () => { 41 | vi.stubEnv('RUNNER_TEMP', '') 42 | 43 | await expect(getPublishedPackages()).rejects.toThrowErrorMatchingInlineSnapshot( 44 | `[CommanderError: error: ✖ $RUNNER_TEMP is empty]` 45 | ) 46 | }) 47 | 48 | it('should throw error when published packages file is not found', async () => { 49 | vi.stubEnv('RUNNER_TEMP', fixture('empty')) 50 | 51 | await expect(getPublishedPackages()).rejects.toThrowErrorMatchingInlineSnapshot( 52 | `[CommanderError: error: No published packages file found at: ${fixture('empty/published-packages.json')}]` 53 | ) 54 | }) 55 | 56 | it('should throw error when published packages JSON is invalid', async () => { 57 | vi.stubEnv('RUNNER_TEMP', fixture('invalid-json')) 58 | 59 | await expect(getPublishedPackages()).rejects.toThrowErrorMatchingInlineSnapshot( 60 | `[Error: Failed to parse published packages: SyntaxError: Unexpected token 'h', "this is not"... is not valid JSON]` 61 | ) 62 | }) 63 | 64 | it('should throw error when published packages schema is invalid', async () => { 65 | vi.stubEnv('RUNNER_TEMP', fixture('invalid-schema')) 66 | 67 | await expect(getPublishedPackages()).rejects.toThrowErrorMatchingInlineSnapshot(` 68 | [Error: Failed to parse published packages: ✖ Invalid input: expected string, received number 69 | → at [0].version] 70 | `) 71 | }) 72 | }) 73 | ``` -------------------------------------------------------------------------------- /apps/workers-bindings/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # workers-bindings 2 | 3 | ## 0.4.1 4 | 5 | ### Patch Changes 6 | 7 | - 43f493d: Update agent + modelcontextprotocol deps 8 | - Updated dependencies [43f493d] 9 | - @repo/[email protected] 10 | - @repo/[email protected] 11 | - @repo/[email protected] 12 | 13 | ## 0.4.0 14 | 15 | ### Minor Changes 16 | 17 | - dee0a7b: Updated the model for docs search to embeddinggemma-300m 18 | 19 | ## 0.3.4 20 | 21 | ### Patch Changes 22 | 23 | - 24dd872: feat: Add MCP tool titles and hints to all Cloudflare tools 24 | - Updated dependencies [24dd872] 25 | - @repo/[email protected] 26 | - @repo/[email protected] 27 | 28 | ## 0.3.3 29 | 30 | ### Patch Changes 31 | 32 | - dffbd36: Use proper wrangler deploy in all servers so we get the name and version 33 | 34 | ## 0.3.2 35 | 36 | ### Patch Changes 37 | 38 | - 7422e71: Update MCP sdk 39 | - Updated dependencies [7422e71] 40 | - @repo/[email protected] 41 | - @repo/[email protected] 42 | 43 | ## 0.3.1 44 | 45 | ### Patch Changes 46 | 47 | - cc6d41f: Update agents deps & modelcontextprotocol 48 | - Updated dependencies [1833c6d] 49 | - Updated dependencies [cc6d41f] 50 | - @repo/[email protected] 51 | - @repo/[email protected] 52 | - @repo/[email protected] 53 | 54 | ## 0.3.0 55 | 56 | ### Minor Changes 57 | 58 | - f885d07: Add search docs tool to bindings and obs servers 59 | 60 | ### Patch Changes 61 | 62 | - Updated dependencies [f885d07] 63 | - @repo/[email protected] 64 | 65 | ## 0.2.0 66 | 67 | ### Minor Changes 68 | 69 | - 2621557: Use new workers:read scope instead of workers:write, as these mcp servers don't require workers write permissions 70 | 71 | ### Patch Changes 72 | 73 | - Updated dependencies [83e2d19] 74 | - @repo/[email protected] 75 | 76 | ## 0.1.0 77 | 78 | ### Minor Changes 79 | 80 | - 6cf52a6: Support AOT tokens 81 | 82 | ### Patch Changes 83 | 84 | - 0fc4439: Update agents and modelcontext dependencies 85 | - Updated dependencies [6cf52a6] 86 | - Updated dependencies [0fc4439] 87 | - @repo/[email protected] 88 | - @repo/[email protected] 89 | - @repo/[email protected] 90 | 91 | ## 0.0.3 92 | 93 | ### Patch Changes 94 | 95 | - 3677a18: Remove extraneous log 96 | - Updated dependencies [3677a18] 97 | - @repo/[email protected] 98 | 99 | ## 0.0.2 100 | 101 | ### Patch Changes 102 | 103 | - 86c2e4f: Add API token passthrough auth 104 | - Updated dependencies [86c2e4f] 105 | - @repo/[email protected] 106 | 107 | ## 0.0.1 108 | 109 | ### Patch Changes 110 | 111 | - cf3771b: chore: add suffixes to common files in apps and packages 112 | 113 | 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. 114 | 115 | - Updated dependencies [cf3771b] 116 | - @repo/[email protected] 117 | - @repo/[email protected] 118 | - @repo/[email protected] 119 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/api/workers-observability.api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { env } from 'cloudflare:workers' 2 | 3 | import { fetchCloudflareApi } from '../cloudflare-api' 4 | import { 5 | zKeysResponse, 6 | zReturnedQueryRunResult, 7 | zValuesResponse, 8 | } from '../types/workers-logs.types' 9 | import { V4Schema } from '../v4-api' 10 | 11 | import type { z } from 'zod' 12 | import type { zKeysRequest, zQueryRunRequest, zValuesRequest } from '../types/workers-logs.types' 13 | 14 | type QueryRunRequest = z.infer<typeof zQueryRunRequest> 15 | 16 | function fixTimeframe(timeframe: QueryRunRequest['timeframe']) { 17 | return { 18 | from: new Date(timeframe.from).getTime(), 19 | to: new Date(timeframe.to).getTime(), 20 | } 21 | } 22 | 23 | export async function queryWorkersObservability( 24 | apiToken: string, 25 | accountId: string, 26 | query: QueryRunRequest 27 | ): Promise<z.infer<typeof zReturnedQueryRunResult> | null> { 28 | // @ts-expect-error We don't have actual env in this package 29 | const environment = env.ENVIRONMENT 30 | const data = await fetchCloudflareApi({ 31 | endpoint: '/workers/observability/telemetry/query', 32 | accountId, 33 | apiToken, 34 | responseSchema: V4Schema(zReturnedQueryRunResult), 35 | options: { 36 | method: 'POST', 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | 'workers-observability-origin': `workers-observability-mcp-${environment}`, 40 | }, 41 | body: JSON.stringify({ ...query, timeframe: fixTimeframe(query.timeframe) }), 42 | }, 43 | }) 44 | 45 | return data.result 46 | } 47 | 48 | type QueryKeysRequest = z.infer<typeof zKeysRequest> 49 | export async function handleWorkerLogsKeys( 50 | apiToken: string, 51 | accountId: string, 52 | keysQuery: QueryKeysRequest 53 | ): Promise<zKeysResponse> { 54 | const data = await fetchCloudflareApi({ 55 | endpoint: '/workers/observability/telemetry/keys', 56 | accountId, 57 | apiToken, 58 | responseSchema: V4Schema(zKeysResponse), 59 | options: { 60 | method: 'POST', 61 | headers: { 62 | 'Content-Type': 'application/json', 63 | }, 64 | body: JSON.stringify({ ...keysQuery, timeframe: fixTimeframe(keysQuery.timeframe) }), 65 | }, 66 | }) 67 | 68 | return data.result || [] 69 | } 70 | 71 | export async function handleWorkerLogsValues( 72 | apiToken: string, 73 | accountId: string, 74 | valuesQuery: z.infer<typeof zValuesRequest> 75 | ): Promise<z.infer<typeof zValuesResponse> | null> { 76 | const data = await fetchCloudflareApi({ 77 | endpoint: '/workers/observability/telemetry/values', 78 | accountId, 79 | apiToken, 80 | responseSchema: V4Schema(zValuesResponse), 81 | options: { 82 | method: 'POST', 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | }, 86 | body: JSON.stringify({ ...valuesQuery, timeframe: fixTimeframe(valuesQuery.timeframe) }), 87 | }, 88 | }) 89 | 90 | return data.result 91 | } 92 | ``` -------------------------------------------------------------------------------- /packages/eval-tools/src/scorers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { generateObject } from 'ai' 2 | import { z } from 'zod' 3 | 4 | import { factualityModel } from './test-models' 5 | 6 | import type { ScoreFn } from 'vitest-evals' 7 | 8 | /** 9 | * Checks the factuality of a submission, using 10 | * OpenAI's GPT-4o model. 11 | */ 12 | export const checkFactuality: ScoreFn = async ({ input, expected, output }) => { 13 | const { model } = factualityModel 14 | const { object } = await generateObject({ 15 | model, 16 | /** 17 | * Prompt taken from autoevals: 18 | * 19 | * {@link https://github.com/braintrustdata/autoevals/blob/5aa20a0a9eb8fc9e07e9e5722ebf71c68d082f32/templates/factuality.yaml} 20 | */ 21 | prompt: ` 22 | You are comparing a submitted answer to an expert's rubric on a given question. Here is the data: 23 | [BEGIN DATA] 24 | ************ 25 | [Question]: ${input} 26 | ************ 27 | [Expert Rubric]: ${expected} 28 | ************ 29 | [Submission]: ${output} 30 | ************ 31 | [END DATA] 32 | 33 | Submissions contain message metadata inside of the <message_content> XML tags. 34 | The attribute \`type=text\` indicates text content. The attribute \`type=tool-call\` indicates a tool call. 35 | Use this metadata to determine the accuracy of the response. 36 | 37 | Compare the factual content of the submitted answer with the expert's answer rubric. Ignore any differences in style, grammar, or punctuation. 38 | The submitted answer may either be a subset or superset of the expert's expected answer, or it may conflict with it. Determine which case applies. Answer the question by selecting one of the following options: 39 | (A) The submitted answer is a subset of the answer the expert's rubric describes and is fully consistent with it. 40 | (B) The submitted answer is a superset of the answer the expert's rubric describes and is fully consistent with it. 41 | (C) The submitted answer contains all the same details of the answer the expert's rubric describes. 42 | (D) There is a disagreement between the submitted answer and the expert's rubric. 43 | (E) The answers differ, but these differences don't matter from the perspective of factuality. 44 | `, 45 | schema: z.object({ 46 | answer: z.enum(['A', 'B', 'C', 'D', 'E']).describe('Your selection.'), 47 | rationale: z.string().describe('Why you chose this answer. Be very detailed.'), 48 | }), 49 | }) 50 | 51 | /** 52 | * LLM's are well documented at being poor at generating 53 | */ 54 | const scores = { 55 | A: 0.4, 56 | B: 1, 57 | C: 1, 58 | D: 0, 59 | E: 1, 60 | } 61 | 62 | return { 63 | score: scores[object.answer], 64 | metadata: { 65 | rationale: object.rationale, 66 | }, 67 | } 68 | } 69 | ``` -------------------------------------------------------------------------------- /apps/docs-autorag/src/tools/docs-autorag.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { type EmbeddedResource } from '@modelcontextprotocol/sdk/types.js' 2 | import mime from 'mime' 3 | import { z } from 'zod' 4 | 5 | import type { CloudflareDocumentationMCP } from '../docs-autorag.app' 6 | 7 | /** 8 | * Registers the docs search tool with the MCP server 9 | * @param agent The MCP server instance 10 | */ 11 | export function registerDocsTools(agent: CloudflareDocumentationMCP) { 12 | // Register the worker logs analysis tool by worker name 13 | agent.server.tool( 14 | 'search_cloudflare_documentation', 15 | `Search the Cloudflare documentation. 16 | 17 | You should use this tool when: 18 | - A user asks questions about Cloudflare products (Workers, Developer Platform, Zero Trust, CDN, etc) 19 | - A user requests information about a Cloudflare feature 20 | - You are unsure of how to use some Cloudflare functionality 21 | - You are writing Cloudflare Workers code and need to look up Workers-specific documentation 22 | 23 | This tool returns a number of results from a vector database. These are embedded as resources in the response and are plaintext documents in a variety of formats. 24 | `, 25 | { 26 | // partially pulled from autorag query optimization example 27 | query: z.string().describe(`Search query. The query should: 28 | 1. Identify the core concepts and intent 29 | 2. Add relevant synonyms and related terms 30 | 3. Remove irrelevant filler words 31 | 4. Structure the query to emphasize key terms 32 | 5. Include technical or domain-specific terminology if applicable`), 33 | scoreThreshold: z 34 | .number() 35 | .min(0) 36 | .max(1) 37 | .optional() 38 | .describe('A score threshold (0-1) for which matches should be included.'), 39 | maxNumResults: z 40 | .number() 41 | .default(10) 42 | .optional() 43 | .describe('The maximum number of results to return.'), 44 | }, 45 | async (params) => { 46 | // we don't need "rewrite query" OR aiSearch because an LLM writes the query and formats the output for us. 47 | const result = await agent.env.AI.autorag(agent.env.AUTORAG_NAME).search({ 48 | query: params.query, 49 | ranking_options: params.scoreThreshold 50 | ? { 51 | score_threshold: params.scoreThreshold, 52 | } 53 | : undefined, 54 | max_num_results: params.maxNumResults, 55 | }) 56 | 57 | const resources: EmbeddedResource[] = result.data.map((result) => { 58 | const content = result.content.reduce((acc, contentPart) => { 59 | return acc + contentPart.text 60 | }, '') 61 | return { 62 | type: 'resource', 63 | resource: { 64 | uri: `docs://${result.filename}`, 65 | mimeType: mime.getType(result.filename) ?? 'text/plain', 66 | text: content, 67 | }, 68 | } 69 | }) 70 | 71 | return { 72 | content: resources, 73 | } 74 | } 75 | ) 76 | } 77 | ``` -------------------------------------------------------------------------------- /apps/radar/src/tools/url-scanner.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' 2 | import { getProps } from '@repo/mcp-common/src/get-props' 3 | import { pollUntilReady } from '@repo/mcp-common/src/poll' 4 | 5 | import { CreateScanResult, UrlParam } from '../types/url-scanner' 6 | 7 | import type { RadarMCP } from '../radar.app' 8 | 9 | const MAX_WAIT_SECONDS = 30 10 | const INTERVAL_SECONDS = 2 11 | 12 | export function registerUrlScannerTools(agent: RadarMCP) { 13 | agent.server.tool( 14 | 'scan_url', 15 | 'Submit a URL to scan', 16 | { 17 | url: UrlParam, 18 | }, 19 | async ({ url }) => { 20 | const accountId = await agent.getActiveAccountId() 21 | if (!accountId) { 22 | return { 23 | content: [ 24 | { 25 | type: 'text', 26 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 27 | }, 28 | ], 29 | } 30 | } 31 | 32 | try { 33 | const props = getProps(agent) 34 | const client = getCloudflareClient(props.accessToken) 35 | 36 | // Search if there are recent scans for the URL 37 | const scans = await client.urlScanner.scans.list({ 38 | account_id: accountId, 39 | q: `page.url:"${url}"`, 40 | }) 41 | 42 | let scanId = scans.results.length > 0 ? scans.results[0]._id : null 43 | 44 | if (!scanId) { 45 | // Submit scan 46 | // TODO theres an issue (reported) with this method in the cloudflare TS lib 47 | // const scan = await (client.urlScanner.scans.create({ account_id, url: "https://www.example.com" }, { headers })).withResponse() 48 | 49 | const res = await fetch( 50 | `https://api.cloudflare.com/client/v4/accounts/${accountId}/urlscanner/v2/scan`, 51 | { 52 | method: 'POST', 53 | headers: { 54 | Authorization: `Bearer ${props.accessToken}`, 55 | }, 56 | body: JSON.stringify({ url }), 57 | } 58 | ) 59 | 60 | if (!res.ok) { 61 | throw new Error('Failed to submit scan') 62 | } 63 | 64 | const scan = CreateScanResult.parse(await res.json()) 65 | scanId = scan?.uuid 66 | } 67 | 68 | const r = await pollUntilReady({ 69 | taskFn: () => client.urlScanner.scans.get(scanId, { account_id: accountId }), 70 | intervalSeconds: INTERVAL_SECONDS, 71 | maxWaitSeconds: MAX_WAIT_SECONDS, 72 | }) 73 | 74 | return { 75 | content: [ 76 | { 77 | type: 'text', 78 | text: JSON.stringify({ 79 | result: { verdicts: r.verdicts, stats: r.stats, page: r.page }, // TODO select what is more relevant, or add a param to allow the agent to select a set of metrics 80 | }), 81 | }, 82 | ], 83 | } 84 | } catch (error) { 85 | return { 86 | content: [ 87 | { 88 | type: 'text', 89 | text: `Error scanning URL: ${error instanceof Error && error.message}`, 90 | }, 91 | ], 92 | } 93 | } 94 | } 95 | ) 96 | } 97 | ``` -------------------------------------------------------------------------------- /apps/demo-day/frontend/public/cloudflare_logo.svg: -------------------------------------------------------------------------------- ``` 1 | <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 651.29 94.76"><defs><style>.cls-1{fill:#ffffff;}.cls-2{fill:#ffffff;}</style></defs><path class="cls-1" d="M143.05,93.42l1.07-3.71c1.27-4.41.8-8.48-1.34-11.48-2-2.76-5.26-4.38-9.25-4.57L58,72.7a1.47,1.47,0,0,1-1.35-2,2,2,0,0,1,1.75-1.34l76.26-1c9-.41,18.84-7.75,22.27-16.71l4.34-11.36a2.68,2.68,0,0,0,.18-1,3.31,3.31,0,0,0-.06-.54,49.67,49.67,0,0,0-95.49-5.14,22.35,22.35,0,0,0-35,23.42A31.73,31.73,0,0,0,.34,93.45a1.47,1.47,0,0,0,1.45,1.27l139.49,0h0A1.83,1.83,0,0,0,143.05,93.42Z"/><path class="cls-2" d="M168.22,41.15q-1,0-2.1.06a.88.88,0,0,0-.32.07,1.17,1.17,0,0,0-.76.8l-3,10.26c-1.28,4.41-.81,8.48,1.34,11.48a11.65,11.65,0,0,0,9.24,4.57l16.11,1a1.44,1.44,0,0,1,1.14.62,1.5,1.5,0,0,1,.17,1.37,2,2,0,0,1-1.75,1.34l-16.73,1c-9.09.42-18.88,7.75-22.31,16.7l-1.21,3.16a.9.9,0,0,0,.79,1.22h57.63A1.55,1.55,0,0,0,208,93.63a41.34,41.34,0,0,0-39.76-52.48Z"/><polygon points="273.03 59.66 282.56 59.66 282.56 85.72 299.23 85.72 299.23 94.07 273.03 94.07 273.03 59.66"/><path d="M309.11,77v-.09c0-9.88,8-17.9,18.58-17.9s18.48,7.92,18.48,17.8v.1c0,9.88-8,17.89-18.58,17.89S309.11,86.85,309.11,77m27.33,0v-.09c0-5-3.59-9.29-8.85-9.29s-8.7,4.22-8.7,9.19v.1c0,5,3.59,9.29,8.8,9.29s8.75-4.23,8.75-9.2"/><path d="M357.84,79V59.66h9.69V78.78c0,5,2.5,7.33,6.34,7.33s6.34-2.26,6.34-7.08V59.66h9.68V78.73c0,11.11-6.34,16-16.12,16s-15.93-5-15.93-15.73"/><path d="M404.49,59.66h13.27c12.29,0,19.42,7.08,19.42,17v.1c0,9.93-7.23,17.3-19.61,17.3H404.49Zm13.42,26c5.7,0,9.49-3.15,9.49-8.71v-.09c0-5.51-3.79-8.71-9.49-8.71H414V85.62Z"/><polygon points="451.04 59.66 478.56 59.66 478.56 68.02 460.58 68.02 460.58 73.87 476.85 73.87 476.85 81.78 460.58 81.78 460.58 94.07 451.04 94.07 451.04 59.66"/><polygon points="491.84 59.66 501.37 59.66 501.37 85.72 518.04 85.72 518.04 94.07 491.84 94.07 491.84 59.66"/><path d="M543,59.42h9.19L566.8,94.07H556.58l-2.51-6.14H540.79l-2.45,6.14h-10Zm8.35,21.08-3.83-9.78L543.6,80.5Z"/><path d="M579.08,59.66h16.27c5.27,0,8.9,1.38,11.21,3.74a10.64,10.64,0,0,1,3.05,8v.1a10.88,10.88,0,0,1-7.08,10.57l8.21,12h-11L592.8,83.65h-4.18V94.07h-9.54Zm15.83,16.52c3.25,0,5.12-1.58,5.12-4.08V72c0-2.71-2-4.08-5.17-4.08h-6.24v8.26Z"/><polygon points="623.37 59.66 651.05 59.66 651.05 67.77 632.81 67.77 632.81 72.98 649.33 72.98 649.33 80.5 632.81 80.5 632.81 85.96 651.29 85.96 651.29 94.07 623.37 94.07 623.37 59.66"/><path d="M252.15,81a8.44,8.44,0,0,1-7.88,5.16c-5.22,0-8.8-4.33-8.8-9.29v-.1c0-5,3.49-9.2,8.7-9.2a8.64,8.64,0,0,1,8.18,5.71h10C260.79,65.09,253.6,59,244.27,59c-10.62,0-18.58,8-18.58,17.9V77c0,9.88,7.86,17.8,18.48,17.8,9.08,0,16.18-5.88,18.05-13.76Z"/></svg> ``` -------------------------------------------------------------------------------- /apps/sandbox-container/evals/files.eval.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { assert, expect } from 'vitest' 2 | import { describeEval } from 'vitest-evals' 3 | import { z } from 'zod' 4 | 5 | import { runTask } from '@repo/eval-tools/src/runTask' 6 | import { checkFactuality } from '@repo/eval-tools/src/scorers' 7 | import { eachModel } from '@repo/eval-tools/src/test-models' 8 | 9 | import { initializeClient } from './utils' 10 | 11 | eachModel('$modelName', ({ model }) => { 12 | describeEval('Runs container file write', { 13 | data: async () => [ 14 | { 15 | input: 'write a file named test.txt containing the text "asdf"', 16 | expected: 'The container_file_write tool was called and the file\'s content is "asdf"', 17 | }, 18 | ], 19 | task: async (input) => { 20 | const client = await initializeClient() 21 | const { promptOutput } = await runTask(client, model, input) 22 | const fileRead = client.listTools().find((tool) => { 23 | if (tool.name === 'container_file_read') { 24 | return tool 25 | } 26 | }) 27 | 28 | assert(fileRead !== undefined) 29 | const result = await client.callTool( 30 | { 31 | ...fileRead, 32 | arguments: { 33 | args: { path: 'file://test.txt' }, 34 | }, 35 | }, 36 | z.any() as any, 37 | {} 38 | ) 39 | 40 | expect(result.content).toStrictEqual([ 41 | { 42 | type: 'resource', 43 | resource: { 44 | uri: 'file://test.txt', 45 | mimeType: 'text/plain', 46 | text: 'asdf', 47 | }, 48 | }, 49 | ]) 50 | 51 | return promptOutput 52 | }, 53 | scorers: [checkFactuality], 54 | threshold: 1, 55 | timeout: 60000, 56 | }) 57 | 58 | describeEval('Runs container file delete', { 59 | data: async () => [ 60 | { 61 | input: 'write a file named test.txt, then delete it', 62 | expected: 63 | 'The container_file_write tool was called and then the container_file_delete tool was called with the same parameters', 64 | }, 65 | ], 66 | task: async (input) => { 67 | const client = await initializeClient() 68 | const { promptOutput, toolCalls } = await runTask(client, model, input) 69 | 70 | const toolArgs = toolCalls.find((tool) => { 71 | return tool.toolName === 'container_file_write' ? tool : undefined 72 | })?.args as { args: { path: string } } | undefined 73 | 74 | assert(toolArgs !== undefined) 75 | expect(toolCalls).toEqual( 76 | expect.arrayContaining([ 77 | expect.objectContaining({ 78 | type: 'tool-call', 79 | toolName: 'container_file_write', 80 | args: { 81 | args: expect.objectContaining({ 82 | path: toolArgs.args.path, 83 | }), 84 | }, 85 | }), 86 | ]) 87 | ) 88 | 89 | expect(toolCalls).toEqual( 90 | expect.arrayContaining([ 91 | expect.objectContaining({ 92 | type: 'tool-call', 93 | toolName: 'container_file_delete', 94 | args: { 95 | args: expect.objectContaining({ 96 | path: toolArgs.args.path, 97 | }), 98 | }, 99 | }), 100 | ]) 101 | ) 102 | 103 | return promptOutput 104 | }, 105 | scorers: [checkFactuality], 106 | threshold: 1, 107 | timeout: 60000, 108 | }) 109 | }) 110 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/account.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import { handleAccountsList } from '../api/account.api' 4 | import { getCloudflareClient } from '../cloudflare-api' 5 | import { getProps } from '../get-props' 6 | 7 | import type { CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types' 8 | 9 | export function registerAccountTools(agent: CloudflareMcpAgent) { 10 | // Tool to list all accounts 11 | agent.server.tool( 12 | 'accounts_list', 13 | 'List all accounts in your Cloudflare account', 14 | {}, 15 | { 16 | title: 'List accounts', 17 | annotations: { 18 | readOnlyHint: true, 19 | }, 20 | }, 21 | async () => { 22 | try { 23 | const props = getProps(agent) 24 | const results = await handleAccountsList({ 25 | client: getCloudflareClient(props.accessToken), 26 | }) 27 | // Sort accounts by created_on date (newest first) 28 | const accounts = results 29 | // order by created_on desc ( newest first ) 30 | .sort((a, b) => { 31 | if (!a.created_on) return 1 32 | if (!b.created_on) return -1 33 | return new Date(b.created_on).getTime() - new Date(a.created_on).getTime() 34 | }) 35 | // Remove fields not needed by the LLM 36 | .map((account) => { 37 | return { 38 | id: account.id, 39 | name: account.name, 40 | created_on: account.created_on, 41 | } 42 | }) 43 | 44 | return { 45 | content: [ 46 | { 47 | type: 'text', 48 | text: JSON.stringify({ 49 | accounts, 50 | count: accounts.length, 51 | }), 52 | }, 53 | ], 54 | } 55 | } catch (e) { 56 | agent.server.recordError(e) 57 | return { 58 | content: [ 59 | { 60 | type: 'text', 61 | text: `Error listing accounts: ${e instanceof Error && e.message}`, 62 | }, 63 | ], 64 | } 65 | } 66 | } 67 | ) 68 | 69 | // Only register set_active_account tool when user token is provided, as it doesn't make sense to expose 70 | // this tool for account scoped tokens, given that they're scoped to a single account 71 | if (getProps(agent).type === 'user_token') { 72 | const activeAccountIdParam = z 73 | .string() 74 | .describe( 75 | 'The accountId present in the users Cloudflare account, that should be the active accountId.' 76 | ) 77 | agent.server.tool( 78 | 'set_active_account', 79 | 'Set active account to be used for tool calls that require accountId', 80 | { 81 | activeAccountIdParam, 82 | }, 83 | { 84 | title: 'Set active account', 85 | annotations: { 86 | readOnlyHint: false, 87 | destructiveHint: false, 88 | }, 89 | }, 90 | async (params) => { 91 | try { 92 | const { activeAccountIdParam: activeAccountId } = params 93 | await agent.setActiveAccountId(activeAccountId) 94 | return { 95 | content: [ 96 | { 97 | type: 'text', 98 | text: JSON.stringify({ 99 | activeAccountId, 100 | }), 101 | }, 102 | ], 103 | } 104 | } catch (e) { 105 | agent.server.recordError(e) 106 | return { 107 | content: [ 108 | { 109 | type: 'text', 110 | text: `Error setting activeAccountID: ${e instanceof Error && e.message}`, 111 | }, 112 | ], 113 | } 114 | } 115 | } 116 | ) 117 | } 118 | } 119 | ``` -------------------------------------------------------------------------------- /apps/sandbox-container/server/prompts.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const BASE_INSTRUCTIONS = /* markdown */ ` 2 | # Container MCP Agent 3 | 4 | The Container MCP Agent provides access to a sandboxed container environment. This is an ephemeral container and has access to the internet. 5 | 6 | The container is an Ubuntu 20.04 base image with the following packages installed: 7 | - curl 8 | - git 9 | - net-tools 10 | - build-essential 11 | - nodejs 12 | - npm 13 | - python3 14 | - python3-pip 15 | 16 | If necessary, you may install additional packages. 17 | 18 | You are given a working directory in which you can create or delete files and execute commands as described below. 19 | 20 | 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. 21 | 22 | ## Resources 23 | 24 | The primary resource in this image is the \`container_files\` resource. 25 | 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. 26 | 27 | The \`container_files_list\` allows you to list all file resources in your working directory. Content is omitted from the response of this tool. 28 | 29 | 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, 30 | or a base64 encoded blob for binary files. 31 | 32 | 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. 33 | 34 | 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. 35 | 36 | ## Tools 37 | 38 | 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. 39 | 40 | You can execute actions in the container using the \`container_exec\` tool. By default, stdout is returned back as a string. 41 | To write a file, use the \`container_file_write\` tool. To delete a file, use the \`container_file_delete\` tool. 42 | 43 | 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. 44 | If you want to get the file contents of a file resource, use \`container_file_read\`, which will return the file contents. 45 | 46 | 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. 47 | 48 | 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 49 | ` 50 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/types/workers-builds.types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | export type BuildDetails = z.infer<typeof BuildDetails> 4 | export const BuildDetails = z.object({ 5 | // TODO: Maybe remove fields we don't need to reduce surface area of things we need to update 6 | build_uuid: z.string(), 7 | status: z.string(), 8 | build_outcome: z.string().nullable(), 9 | created_on: z.coerce.date(), 10 | modified_on: z.coerce.date(), 11 | initializing_on: z.coerce.date().nullable(), 12 | running_on: z.coerce.date().nullable(), 13 | stopped_on: z.coerce.date().nullable(), 14 | trigger: z.object({ 15 | trigger_uuid: z.string(), 16 | external_script_id: z.string(), 17 | trigger_name: z.string(), 18 | build_command: z.string(), 19 | deploy_command: z.string(), 20 | root_directory: z.string(), 21 | branch_includes: z.array(z.string()), 22 | branch_excludes: z.array(z.string()), 23 | path_includes: z.array(z.string()), 24 | path_excludes: z.array(z.string()), 25 | build_caching_enabled: z.boolean(), 26 | created_on: z.coerce.date(), 27 | modified_on: z.coerce.date(), 28 | deleted_on: z.coerce.date().nullable(), 29 | repo_connection: z.object({ 30 | repo_connection_uuid: z.string(), 31 | repo_id: z.string(), 32 | repo_name: z.string(), 33 | provider_type: z.string(), 34 | provider_account_id: z.string(), 35 | provider_account_name: z.string(), 36 | created_on: z.coerce.date(), 37 | modified_on: z.coerce.date(), 38 | deleted_on: z.coerce.date().nullable(), 39 | }), 40 | }), 41 | build_trigger_metadata: z.object({ 42 | build_trigger_source: z.string(), 43 | branch: z.string(), 44 | commit_hash: z.string(), 45 | commit_message: z.string(), 46 | author: z.string(), 47 | build_command: z.string(), 48 | deploy_command: z.string(), 49 | root_directory: z.string(), 50 | build_token_uuid: z.string(), 51 | environment_variables: z.record( 52 | z.string(), 53 | z.object({ 54 | is_secret: z.boolean(), 55 | created_on: z.coerce.date(), 56 | value: z.string().nullable(), 57 | }) 58 | ), 59 | repo_name: z.string(), 60 | provider_account_name: z.string(), 61 | provider_type: z.string(), 62 | }), 63 | pull_request: z.unknown(), 64 | }) 65 | 66 | /** 67 | * GET /builds/workers/:external_script_id/builds 68 | */ 69 | export type ListBuildsByScriptResult = z.infer<typeof ListBuildsByScriptResult> 70 | export const ListBuildsByScriptResult = z.array(BuildDetails) 71 | 72 | export type ListBuildsByScriptResultInfo = z.infer<typeof ListBuildsByScriptResultInfo> 73 | export const ListBuildsByScriptResultInfo = z.object({ 74 | next_page: z.boolean(), 75 | page: z.number(), 76 | per_page: z.number(), 77 | count: z.number(), 78 | total_count: z.number(), 79 | total_pages: z.number(), 80 | }) 81 | 82 | export type GetBuildResult = z.infer<typeof GetBuildResult> 83 | export const GetBuildResult = BuildDetails 84 | 85 | export type LogLine = z.infer<typeof LogLine> 86 | export const LogLine = z.tuple([ 87 | z.coerce.date().describe('line timestamp'), 88 | z.string().describe('line message'), 89 | ]) 90 | 91 | export type GetBuildLogsResult = z.infer<typeof GetBuildLogsResult> 92 | export const GetBuildLogsResult = z.object({ 93 | cursor: z.string().optional().describe('pagination cursor'), 94 | truncated: z.boolean(), 95 | lines: z.array(LogLine), 96 | }) 97 | ``` -------------------------------------------------------------------------------- /apps/demo-day/frontend/public/stripe.svg: -------------------------------------------------------------------------------- ``` 1 | <svg width="498" height="498" viewBox="0 0 498 498" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <g filter="url(#filter0_d_1171_29)"> 3 | <rect x="5" y="1" width="488" height="488" rx="77" stroke="white" stroke-width="2" shape-rendering="crispEdges"/> 4 | </g> 5 | <g filter="url(#filter1_d_1171_29)"> 6 | <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"/> 7 | </g> 8 | <defs> 9 | <filter id="filter0_d_1171_29" x="0" y="0" width="498" height="498" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> 10 | <feFlood flood-opacity="0" result="BackgroundImageFix"/> 11 | <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"/> 12 | <feOffset dy="4"/> 13 | <feGaussianBlur stdDeviation="2"/> 14 | <feComposite in2="hardAlpha" operator="out"/> 15 | <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"/> 16 | <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1171_29"/> 17 | <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1171_29" result="shape"/> 18 | </filter> 19 | <filter id="filter1_d_1171_29" x="138.143" y="95.082" width="219.422" height="311.613" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> 20 | <feFlood flood-opacity="0" result="BackgroundImageFix"/> 21 | <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"/> 22 | <feOffset dy="4"/> 23 | <feGaussianBlur stdDeviation="2"/> 24 | <feComposite in2="hardAlpha" operator="out"/> 25 | <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"/> 26 | <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1171_29"/> 27 | <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1171_29" result="shape"/> 28 | </filter> 29 | </defs> 30 | </svg> 31 | ``` -------------------------------------------------------------------------------- /packages/eval-tools/src/test-models.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createAnthropic } from '@ai-sdk/anthropic' 2 | import { AnthropicMessagesModelId } from '@ai-sdk/anthropic/internal' 3 | import { createGoogleGenerativeAI } from '@ai-sdk/google' 4 | import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal' 5 | import { createOpenAI } from '@ai-sdk/openai' 6 | import { OpenAIChatModelId } from '@ai-sdk/openai/internal' 7 | import { createAiGateway } from 'ai-gateway-provider' 8 | import { env } from 'cloudflare:test' 9 | import { describe } from 'vitest' 10 | import { createWorkersAI } from 'workers-ai-provider' 11 | 12 | export const factualityModel = getOpenAiModel('gpt-4o') 13 | 14 | type value2key<T, V> = { 15 | [K in keyof T]: T[K] extends V ? K : never 16 | }[keyof T] 17 | type AiTextGenerationModels = Exclude< 18 | value2key<AiModels, BaseAiTextGeneration>, 19 | value2key<AiModels, BaseAiTextToImage> 20 | > 21 | 22 | function getOpenAiModel(modelName: OpenAIChatModelId) { 23 | if (!env.CLOUDFLARE_ACCOUNT_ID || !env.AI_GATEWAY_ID || !env.AI_GATEWAY_TOKEN) { 24 | throw new Error('No AI gateway credentials set!') 25 | } 26 | 27 | const aigateway = createAiGateway({ 28 | accountId: env.CLOUDFLARE_ACCOUNT_ID, 29 | gateway: env.AI_GATEWAY_ID, 30 | apiKey: env.AI_GATEWAY_TOKEN, 31 | }) 32 | 33 | const ai = createOpenAI({ 34 | apiKey: '', 35 | }) 36 | 37 | const model = aigateway([ai(modelName)]) 38 | 39 | return { modelName, model, ai } 40 | } 41 | 42 | function getAnthropicModel(modelName: AnthropicMessagesModelId) { 43 | const aigateway = createAiGateway({ 44 | accountId: env.CLOUDFLARE_ACCOUNT_ID, 45 | gateway: env.AI_GATEWAY_ID, 46 | apiKey: env.AI_GATEWAY_TOKEN, 47 | }) 48 | 49 | const ai = createAnthropic({ 50 | apiKey: '', 51 | }) 52 | 53 | const model = aigateway([ai(modelName)]) 54 | 55 | return { modelName, model, ai } 56 | } 57 | 58 | function getGeminiModel(modelName: GoogleGenerativeAILanguageModel['modelId']) { 59 | if (!env.CLOUDFLARE_ACCOUNT_ID || !env.AI_GATEWAY_ID || !env.AI_GATEWAY_TOKEN) { 60 | throw new Error('No AI gateway credentials set!') 61 | } 62 | 63 | const aigateway = createAiGateway({ 64 | accountId: env.CLOUDFLARE_ACCOUNT_ID, 65 | gateway: env.AI_GATEWAY_ID, 66 | apiKey: env.AI_GATEWAY_TOKEN, 67 | }) 68 | 69 | const ai = createGoogleGenerativeAI({ apiKey: '' }) 70 | 71 | const model = aigateway([ai(modelName)]) 72 | 73 | return { modelName, model, ai } 74 | } 75 | 76 | function getWorkersAiModel(modelName: AiTextGenerationModels) { 77 | if (!env.AI) { 78 | throw new Error('No AI binding provided!') 79 | } 80 | 81 | const ai = createWorkersAI({ binding: env.AI }) 82 | 83 | const model = ai(modelName) 84 | return { modelName, model, ai } 85 | } 86 | 87 | export const eachModel = describe.each([ 88 | getOpenAiModel('gpt-4o'), 89 | getOpenAiModel('gpt-4o-mini'), 90 | // 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 91 | getGeminiModel('gemini-2.0-flash'), 92 | // llama 3 is somewhat inconsistent 93 | //getWorkersAiModel("@cf/meta/llama-3.3-70b-instruct-fp8-fast") 94 | // Currently llama 4 is having issues with tool calling 95 | //getWorkersAiModel("@cf/meta/llama-4-scout-17b-16e-instruct") 96 | ]) 97 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/types/cf1-integrations.types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | const Vendor = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | display_name: z.string(), 7 | description: z.string().nullable(), 8 | logo: z.string().nullable(), 9 | static_logo: z.string().nullable(), 10 | }) 11 | 12 | const Policy = z.object({ 13 | id: z.string(), 14 | name: z.string(), 15 | permissions: z.array(z.string()), 16 | link: z.string().nullable(), 17 | dlp_enabled: z.boolean(), 18 | }) 19 | 20 | // Base Integration schema 21 | export const Integration = z.object({ 22 | id: z.string(), 23 | name: z.string(), 24 | status: z.enum(['Healthy', 'Unhealthy', 'Initializing', 'Paused']), 25 | upgradable: z.boolean(), 26 | permissions: z.array(z.string()), 27 | 28 | vendor: Vendor, 29 | policy: Policy, 30 | 31 | created: z.string(), 32 | updated: z.string(), 33 | credentials_expiry: z.string().nullable(), 34 | last_hydrated: z.string().nullable(), 35 | }) 36 | 37 | // Schema for output: a single integration 38 | export const IntegrationResponse = Integration 39 | export type zReturnedIntegrationResult = z.infer<typeof IntegrationResponse> 40 | 41 | // Schema for output: multiple integrations 42 | export const IntegrationsResponse = z.array(Integration) 43 | export type zReturnedIntegrationsResult = z.infer<typeof IntegrationsResponse> 44 | 45 | export const AssetCategory = z.object({ 46 | id: z.string().uuid(), 47 | type: z.string(), 48 | vendor: z.string(), 49 | service: z.string().nullable(), 50 | }) 51 | 52 | export const AssetDetail = z.object({ 53 | id: z.string().uuid(), 54 | external_id: z.string(), 55 | name: z.string(), 56 | link: z.string().nullable(), 57 | fields: z.array( 58 | z.object({ 59 | link: z.string().nullable(), 60 | name: z.string(), 61 | value: z.any(), 62 | }) 63 | ), 64 | category: AssetCategory, 65 | integration: Integration, 66 | }) 67 | 68 | export type zReturnedAssetResult = z.infer<typeof AssetDetail> 69 | 70 | export const AssetsResponse = z.array(AssetDetail) 71 | export type zReturnedAssetsResult = z.infer<typeof AssetsResponse> 72 | 73 | export const AssetCategoriesResponse = z.array(AssetCategory) 74 | export type zReturnedAssetCategoriesResult = z.infer<typeof AssetCategoriesResponse> 75 | 76 | export const assetCategoryTypeParam = z 77 | .enum([ 78 | 'Account', 79 | 'Alert', 80 | 'App', 81 | 'Authentication Method', 82 | 'Bucket', 83 | 'Bucket Iam Permission', 84 | 'Bucket Permission', 85 | 'Calendar', 86 | 'Certificate', 87 | 'Channel', 88 | 'Commit', 89 | 'Content', 90 | 'Credential', 91 | 'Domain', 92 | 'Drive', 93 | 'Environment', 94 | 'Factor', 95 | 'File', 96 | 'File Permission', 97 | 'Folder', 98 | 'Group', 99 | 'Incident', 100 | 'Instance', 101 | 'Issue', 102 | 'Label', 103 | 'Meeting', 104 | 'Message', 105 | 'Message Rule', 106 | 'Namespace', 107 | 'Organization', 108 | 'Package', 109 | 'Pipeline', 110 | 'Project', 111 | 'Report', 112 | 'Repository', 113 | 'Risky User', 114 | 'Role', 115 | 'Server', 116 | 'Site', 117 | 'Space', 118 | 'Submodule', 119 | 'Third Party User', 120 | 'User', 121 | 'User No Mfa', 122 | 'Variable', 123 | 'Webhook', 124 | 'Workspace', 125 | ]) 126 | .optional() 127 | .describe('Type of cloud resource or service category') 128 | 129 | export const assetCategoryVendorParam = z 130 | .enum([ 131 | 'AWS', 132 | 'Bitbucket', 133 | 'Box', 134 | 'Confluence', 135 | 'Dropbox', 136 | 'GitHub', 137 | 'Google Cloud Platform', 138 | 'Google Workspace', 139 | 'Jira', 140 | 'Microsoft', 141 | 'Microsoft Azure', 142 | 'Okta', 143 | 'Salesforce', 144 | 'ServiceNow', 145 | 'Slack', 146 | 'Workday', 147 | 'Zoom', 148 | ]) 149 | .describe('Vendor of the cloud service or resource') 150 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { isPromise } from 'node:util/types' 2 | import { type ServerOptions } from '@modelcontextprotocol/sdk/server/index.js' 3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' 4 | import { type ZodRawShape } from 'zod' 5 | 6 | import { MetricsTracker, SessionStart, ToolCall } from '../../mcp-observability/src' 7 | import { McpError } from './mcp-error' 8 | 9 | import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js' 10 | import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js' 11 | import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js' 12 | import type { SentryClient } from './sentry' 13 | 14 | export class CloudflareMCPServer extends McpServer { 15 | private metrics 16 | private sentry?: SentryClient 17 | 18 | constructor({ 19 | userId, 20 | wae, 21 | serverInfo, 22 | options, 23 | sentry, 24 | }: { 25 | userId?: string 26 | wae: AnalyticsEngineDataset 27 | serverInfo: { 28 | [x: string]: unknown 29 | name: string 30 | version: string 31 | } 32 | options?: ServerOptions 33 | sentry?: SentryClient 34 | }) { 35 | super(serverInfo, options) 36 | this.metrics = new MetricsTracker(wae, serverInfo) 37 | this.sentry = sentry 38 | 39 | this.server.oninitialized = () => { 40 | const clientInfo = this.server.getClientVersion() 41 | const clientCapabilities = this.server.getClientCapabilities() 42 | this.metrics.logEvent( 43 | new SessionStart({ 44 | userId, 45 | clientInfo, 46 | clientCapabilities, 47 | }) 48 | ) 49 | } 50 | 51 | this.server.onerror = (e) => { 52 | this.recordError(e) 53 | } 54 | 55 | const _tool = this.tool.bind(this) 56 | this.tool = (name: string, ...rest: unknown[]): ReturnType<typeof this.tool> => { 57 | const toolCb = rest[rest.length - 1] as ToolCallback<ZodRawShape | undefined> 58 | const replacementToolCb: ToolCallback<ZodRawShape | undefined> = (arg1, arg2) => { 59 | const toolCall = toolCb( 60 | arg1 as { [x: string]: any } & RequestHandlerExtra<ServerRequest, ServerNotification>, 61 | arg2 62 | ) 63 | // There are 4 cases to track: 64 | try { 65 | if (isPromise(toolCall)) { 66 | return toolCall 67 | .then((r: any) => { 68 | // promise succeeds 69 | this.metrics.logEvent( 70 | new ToolCall({ 71 | toolName: name, 72 | userId, 73 | }) 74 | ) 75 | return r 76 | }) 77 | .catch((e: unknown) => { 78 | // promise throws 79 | this.trackToolCallError(e, name, userId) 80 | throw e 81 | }) 82 | } else { 83 | // non-promise succeeds 84 | this.metrics.logEvent( 85 | new ToolCall({ 86 | toolName: name, 87 | userId, 88 | }) 89 | ) 90 | return toolCall 91 | } 92 | } catch (e: unknown) { 93 | // non-promise throws 94 | this.trackToolCallError(e, name, userId) 95 | throw e 96 | } 97 | } 98 | rest[rest.length - 1] = replacementToolCb 99 | 100 | // @ts-ignore 101 | return _tool(name, ...rest) 102 | } 103 | } 104 | 105 | private trackToolCallError(e: unknown, toolName: string, userId?: string) { 106 | // placeholder error code 107 | let errorCode = -1 108 | if (e instanceof McpError) { 109 | errorCode = e.code 110 | } 111 | this.metrics.logEvent( 112 | new ToolCall({ 113 | toolName, 114 | userId: userId, 115 | errorCode: errorCode, 116 | }) 117 | ) 118 | } 119 | 120 | public recordError(e: unknown) { 121 | this.sentry?.recordError(e) 122 | } 123 | } 124 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/durable-kv-store.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { ZodSchema } from 'zod' 2 | 3 | export type DurableKVStorageKeys = { [key: string]: ZodSchema } 4 | 5 | /** 6 | * DurableKVStore is a type-safe key/value store backed by Durable Object storage. 7 | * 8 | * @example 9 | * 10 | * ```ts 11 | * export class MyDurableObject extends DurableObject<Bindings> { 12 | * readonly kv 13 | * constructor( 14 | * readonly state: DurableObjectState, 15 | * env: Bindings 16 | * ) { 17 | * super(state, env) 18 | * this.kv = new DurableKVStore({ 19 | * state, 20 | * prefix: 'meta', 21 | * keys: { 22 | * // Each key has a matching Zod schema enforcing what's stored 23 | * date_key: z.coerce.date(), 24 | * // While empty keys will always return null, adding 25 | * // `nullable()` allows us to explicitly set it to null 26 | * string_key: z.string().nullable(), 27 | * number_key: z.number(), 28 | * } as const satisfies StorageKeys, 29 | * }) 30 | * } 31 | * 32 | * async example(): Promise<void> { 33 | * await this.kv.get('number_key') // -> null 34 | * this.kv.put('number_key', 5) 35 | * await this.kv.get('number_key') // -> 5 36 | * } 37 | * } 38 | * ``` 39 | */ 40 | export class DurableKVStore<T extends DurableKVStorageKeys> { 41 | private readonly prefix: string 42 | private readonly keys: T 43 | private readonly state: DurableObjectState 44 | 45 | constructor({ state, prefix, keys }: { state: DurableObjectState; prefix: string; keys: T }) { 46 | this.state = state 47 | this.prefix = prefix 48 | this.keys = keys 49 | } 50 | 51 | /** Add the prefix to a key (used for get/put operations) */ 52 | private addPrefix<K extends keyof T>(key: K): string { 53 | if (this.prefix.length > 0) { 54 | return `${this.prefix}/${key.toString()}` 55 | } 56 | return key.toString() 57 | } 58 | 59 | /** 60 | * Get a value from KV storage. Returns `null` if the value 61 | * is not set (or if it's explicitly set to `null`) 62 | */ 63 | async get<K extends keyof T>(key: K): Promise<T[K]['_output'] | null> 64 | /** 65 | * Get a value from KV storage or return the provided 66 | * default if they value in storage is unset (undefined). 67 | * The default value must match the schema for the given key. 68 | * 69 | * If defaultValue is explicitly set to undefined, it will still return null (avoid this). 70 | * 71 | * If the value in storage is null then this will return null instead of the default. 72 | */ 73 | async get<K extends keyof T>(key: K, defaultValue: T[K]['_output']): Promise<T[K]['_output']> 74 | async get<K extends keyof T>( 75 | key: K, 76 | defaultValue?: T[K]['_output'] 77 | ): Promise<T[K]['_output'] | null> { 78 | const schema = this.keys[key] 79 | if (schema === undefined) { 80 | throw new TypeError(`key ${key.toString()} has no matching schema`) 81 | } 82 | 83 | const res = await this.state.storage.get(this.addPrefix(key)) 84 | if (res === undefined) { 85 | if (defaultValue !== undefined) { 86 | return schema.parse(defaultValue) 87 | } 88 | return null 89 | } 90 | 91 | return schema.parse(res) 92 | } 93 | 94 | /** Write value to KV storage */ 95 | put<K extends keyof T>(key: K, value: T[K]['_input']): void { 96 | const schema = this.keys[key] 97 | if (schema === undefined) { 98 | throw new TypeError(`key ${key.toString()} has no matching schema`) 99 | } 100 | const parsedValue = schema.parse(value) 101 | void this.state.storage.put(this.addPrefix(key), parsedValue) 102 | } 103 | 104 | /** 105 | * Delete value in KV storage. **Does not need to be awaited** 106 | * 107 | * @returns `true` if a value was deleted, or `false` if it did not. 108 | */ 109 | async delete<K extends keyof T>(key: K): Promise<boolean> { 110 | return this.state.storage.delete(this.addPrefix(key)) 111 | } 112 | } 113 | ``` -------------------------------------------------------------------------------- /apps/dns-analytics/src/tools/dex-analytics.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' 4 | import { getProps } from '@repo/mcp-common/src/get-props' 5 | 6 | import type { AccountGetParams } from 'cloudflare/resources/accounts/accounts.mjs' 7 | import type { ReportGetParams } from 'cloudflare/resources/dns/analytics.mjs' 8 | import type { ZoneGetParams } from 'cloudflare/resources/dns/settings.mjs' 9 | import type { DNSAnalyticsMCP } from '../dns-analytics.app' 10 | 11 | function getStartDate(days: number) { 12 | const today = new Date() 13 | const start_date = new Date(today.setDate(today.getDate() - days)) 14 | return start_date.toISOString() 15 | } 16 | 17 | export function registerAnalyticTools(agent: DNSAnalyticsMCP) { 18 | // Register DNS Report tool 19 | agent.server.tool( 20 | 'dns_report', 21 | 'Fetch the DNS Report for a given zone since a date', 22 | { 23 | zone: z.string(), 24 | days: z.number(), 25 | }, 26 | async ({ zone, days }) => { 27 | try { 28 | const props = getProps(agent) 29 | const client = getCloudflareClient(props.accessToken) 30 | const start_date = getStartDate(days) 31 | const params: ReportGetParams = { 32 | zone_id: zone, 33 | metrics: 'responseTimeAvg,queryCount,uncachedCount,staleCount', 34 | dimensions: 'responseCode,responseCached', 35 | since: start_date, 36 | } 37 | const result = await client.dns.analytics.reports.get(params) 38 | return { 39 | content: [ 40 | { 41 | type: 'text', 42 | text: JSON.stringify({ 43 | result, 44 | }), 45 | }, 46 | ], 47 | } 48 | } catch (error) { 49 | return { 50 | content: [ 51 | { 52 | type: 'text', 53 | text: `Error fetching DNS report: ${error instanceof Error && error.message}`, 54 | }, 55 | ], 56 | } 57 | } 58 | } 59 | ) 60 | // Register Account DNS Settings display tool 61 | agent.server.tool( 62 | 'show_account_dns_settings', 63 | 'Show DNS settings for current account', 64 | async () => { 65 | try { 66 | const accountId = await agent.getActiveAccountId() 67 | if (!accountId) { 68 | return { 69 | content: [ 70 | { 71 | type: 'text', 72 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 73 | }, 74 | ], 75 | } 76 | } 77 | const props = getProps(agent) 78 | const client = getCloudflareClient(props.accessToken) 79 | const params: AccountGetParams = { 80 | account_id: accountId, 81 | } 82 | const result = await client.dns.settings.account.get(params) 83 | return { 84 | content: [ 85 | { 86 | type: 'text', 87 | text: JSON.stringify({ 88 | result, 89 | }), 90 | }, 91 | ], 92 | } 93 | } catch (error) { 94 | return { 95 | content: [ 96 | { 97 | type: 'text', 98 | text: `Error fetching DNS report: ${error instanceof Error && error.message}`, 99 | }, 100 | ], 101 | } 102 | } 103 | } 104 | ) 105 | // Register Zone DNS Settings display tool 106 | agent.server.tool( 107 | 'show_zone_dns_settings', 108 | 'Show DNS settings for a zone', 109 | { 110 | zone: z.string(), 111 | }, 112 | async ({ zone }) => { 113 | try { 114 | const props = getProps(agent) 115 | const client = getCloudflareClient(props.accessToken) 116 | const params: ZoneGetParams = { 117 | zone_id: zone, 118 | } 119 | const result = await client.dns.settings.zone.get(params) 120 | return { 121 | content: [ 122 | { 123 | type: 'text', 124 | text: JSON.stringify({ 125 | result, 126 | }), 127 | }, 128 | ], 129 | } 130 | } catch (error) { 131 | return { 132 | content: [ 133 | { 134 | type: 'text', 135 | text: `Error fetching DNS report: ${error instanceof Error && error.message}`, 136 | }, 137 | ], 138 | } 139 | } 140 | } 141 | ) 142 | } 143 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/tools/zone.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import { handleZonesList } from '../api/zone.api' 4 | import { getCloudflareClient } from '../cloudflare-api' 5 | import { getProps } from '../get-props' 6 | import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent.types' 7 | 8 | export function registerZoneTools(agent: CloudflareMcpAgent) { 9 | // Tool to list all zones under an account 10 | agent.server.tool( 11 | 'zones_list', 12 | 'List all zones under a Cloudflare account', 13 | { 14 | name: z.string().optional().describe('Filter zones by name'), 15 | status: z 16 | .string() 17 | .optional() 18 | .describe( 19 | 'Filter zones by status (active, pending, initializing, moved, deleted, deactivated, read only)' 20 | ), 21 | page: z.number().min(1).default(1).describe('Page number for pagination'), 22 | perPage: z.number().min(5).max(1000).default(50).describe('Number of zones per page'), 23 | order: z 24 | .string() 25 | .default('name') 26 | .describe('Field to order results by (name, status, account_name)'), 27 | direction: z 28 | .enum(['asc', 'desc']) 29 | .default('desc') 30 | .describe('Direction to order results (asc, desc)'), 31 | }, 32 | { 33 | title: 'List zones', 34 | annotations: { 35 | readOnlyHint: true, 36 | destructiveHint: false, 37 | }, 38 | }, 39 | async (params) => { 40 | const accountId = await agent.getActiveAccountId() 41 | if (!accountId) { 42 | return { 43 | content: [ 44 | { 45 | type: 'text', 46 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 47 | }, 48 | ], 49 | } 50 | } 51 | 52 | try { 53 | const props = getProps(agent) 54 | const { page = 1, perPage = 50 } = params 55 | 56 | const zones = await handleZonesList({ 57 | client: getCloudflareClient(props.accessToken), 58 | accountId, 59 | ...params, 60 | }) 61 | 62 | return { 63 | content: [ 64 | { 65 | type: 'text', 66 | text: JSON.stringify({ 67 | zones, 68 | count: zones.length, 69 | page, 70 | perPage, 71 | accountId, 72 | }), 73 | }, 74 | ], 75 | } 76 | } catch (error) { 77 | return { 78 | content: [ 79 | { 80 | type: 'text', 81 | text: `Error listing zones: ${error instanceof Error ? error.message : String(error)}`, 82 | }, 83 | ], 84 | } 85 | } 86 | } 87 | ) 88 | 89 | // Tool to get zone details by ID 90 | agent.server.tool( 91 | 'zone_details', 92 | 'Get details for a specific Cloudflare zone', 93 | { 94 | zoneId: z.string().describe('The ID of the zone to get details for'), 95 | }, 96 | { 97 | title: 'Get zone details', 98 | annotations: { 99 | readOnlyHint: true, 100 | destructiveHint: false, 101 | }, 102 | }, 103 | async (params) => { 104 | const accountId = await agent.getActiveAccountId() 105 | if (!accountId) { 106 | return { 107 | content: [ 108 | { 109 | type: 'text', 110 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 111 | }, 112 | ], 113 | } 114 | } 115 | 116 | try { 117 | const props = getProps(agent) 118 | const { zoneId } = params 119 | const client = getCloudflareClient(props.accessToken) 120 | 121 | // Use the zones.get method to fetch a specific zone 122 | const response = await client.zones.get({ zone_id: zoneId }) 123 | 124 | return { 125 | content: [ 126 | { 127 | type: 'text', 128 | text: JSON.stringify({ 129 | zone: response, 130 | }), 131 | }, 132 | ], 133 | } 134 | } catch (error) { 135 | return { 136 | content: [ 137 | { 138 | type: 'text', 139 | text: `Error fetching zone details: ${error instanceof Error ? error.message : String(error)}`, 140 | }, 141 | ], 142 | } 143 | } 144 | } 145 | ) 146 | } 147 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/types/hyperdrive.types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import type { ConfigCreateParams } from 'cloudflare/resources/hyperdrive/configs.mjs' 4 | 5 | // --- Base Field Schemas --- 6 | 7 | /** Zod schema for a Hyperdrive config ID. */ 8 | export const HyperdriveConfigIdSchema = z 9 | .string() 10 | .describe('The ID of the Hyperdrive configuration') 11 | 12 | /** Zod schema for a Hyperdrive config name. */ 13 | export const HyperdriveConfigNameSchema: z.ZodType<ConfigCreateParams['name']> = z 14 | .string() 15 | .min(1) 16 | .max(64) 17 | .regex(/^[a-zA-Z0-9_-]+$/) 18 | .describe('The name of the Hyperdrive configuration (alphanumeric, underscore, hyphen)') 19 | 20 | // --- Origin Field Schemas --- 21 | 22 | /** Zod schema for the origin database name. */ 23 | export const HyperdriveOriginDatabaseSchema: z.ZodType< 24 | ConfigCreateParams.PublicDatabase['database'] 25 | > = z.string().describe('The database name') 26 | /** Zod schema for the origin database host. */ 27 | export const HyperdriveOriginHostSchema: z.ZodType<ConfigCreateParams.PublicDatabase['host']> = z 28 | .string() 29 | .describe('The database host address') 30 | /** Zod schema for the origin database port. */ 31 | export const HyperdriveOriginPortSchema: z.ZodType<ConfigCreateParams.PublicDatabase['port']> = z 32 | .number() 33 | .int() 34 | .min(1) 35 | .max(65535) 36 | .describe('The database port') 37 | /** Zod schema for the origin database scheme. */ 38 | export const HyperdriveOriginSchemeSchema: z.ZodType<ConfigCreateParams.PublicDatabase['scheme']> = 39 | z.enum(['postgresql']).describe('The database protocol') 40 | /** Zod schema for the origin database user. */ 41 | export const HyperdriveOriginUserSchema: z.ZodType<ConfigCreateParams.PublicDatabase['user']> = z 42 | .string() 43 | .describe('The database user') 44 | /** Zod schema for the origin database password. */ 45 | export const HyperdriveOriginPasswordSchema: z.ZodType< 46 | ConfigCreateParams.PublicDatabase['password'] 47 | > = z.string().describe('The database password') 48 | 49 | // --- Caching Field Schemas (Referencing ConfigCreateParams.HyperdriveHyperdriveCachingEnabled) --- 50 | 51 | /** Zod schema for disabling caching. */ 52 | export const HyperdriveCachingDisabledSchema: z.ZodType< 53 | ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['disabled'] 54 | > = z.boolean().optional().describe('Whether caching is disabled') 55 | /** Zod schema for the maximum cache age. */ 56 | export const HyperdriveCachingMaxAgeSchema: z.ZodType< 57 | ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['max_age'] 58 | > = z.number().int().min(1).optional().describe('Maximum cache age in seconds') 59 | /** Zod schema for the stale while revalidate duration. */ 60 | export const HyperdriveCachingStaleWhileRevalidateSchema: z.ZodType< 61 | ConfigCreateParams.HyperdriveHyperdriveCachingEnabled['stale_while_revalidate'] 62 | > = z.number().int().min(1).optional().describe('Stale while revalidate duration in seconds') 63 | 64 | // --- List Parameter Schemas (Cannot directly type against SDK ConfigListParams which only has account_id) --- 65 | 66 | /** Zod schema for the list page number. */ 67 | export const HyperdriveListParamPageSchema = z 68 | .number() 69 | .int() 70 | .positive() 71 | .optional() 72 | .describe('Page number of results') 73 | /** Zod schema for the list results per page. */ 74 | export const HyperdriveListParamPerPageSchema = z 75 | .number() 76 | .int() 77 | .min(1) 78 | .max(100) 79 | .optional() 80 | .describe('Number of results per page') 81 | /** Zod schema for the list order field. */ 82 | export const HyperdriveListParamOrderSchema = z 83 | .enum(['id', 'name']) 84 | .optional() 85 | .describe('Field to order by') 86 | /** Zod schema for the list order direction. */ 87 | export const HyperdriveListParamDirectionSchema = z 88 | .enum(['asc', 'desc']) 89 | .optional() 90 | .describe('Direction to order') 91 | 92 | // --- Tool Parameter Schemas --- 93 | ``` -------------------------------------------------------------------------------- /packages/mcp-common/src/format.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { fmt } from './format' 4 | 5 | describe('fmt', () => { 6 | describe('trim()', () => { 7 | it('should return an empty string for an empty input', () => { 8 | expect(fmt.trim('')).toBe('') 9 | }) 10 | 11 | it('should trim leading and trailing spaces', () => { 12 | expect(fmt.trim(' hello ')).toBe('hello') 13 | }) 14 | 15 | it('should trim leading and trailing newlines', () => { 16 | expect(fmt.trim('\n\nhello\n\n')).toBe('hello') 17 | }) 18 | 19 | it('should trim leading/trailing spaces and newlines from each line but not remove empty lines', () => { 20 | const input = ` 21 | line1 22 | line2 23 | 24 | line3 25 | ` 26 | const expected = `line1 27 | line2 28 | 29 | line3` 30 | expect(fmt.trim(input)).toBe(expected) 31 | }) 32 | 33 | it('should handle a string that is already trimmed', () => { 34 | expect(fmt.trim('hello\nworld')).toBe('hello\nworld') 35 | }) 36 | 37 | it('should handle a string with only spaces', () => { 38 | expect(fmt.trim(' ')).toBe('') 39 | }) 40 | 41 | it('should handle a string with only newlines', () => { 42 | expect(fmt.trim('\n\n\n')).toBe('') 43 | }) 44 | 45 | it('should preserve empty lines from the middle', () => { 46 | expect(fmt.trim('hello\n\nworld')).toBe('hello\n\nworld') 47 | }) 48 | }) 49 | 50 | describe('oneLine()', () => { 51 | it('should return an empty string for an empty input', () => { 52 | expect(fmt.oneLine('')).toBe('') 53 | }) 54 | 55 | it('should convert a multi-line string to a single line', () => { 56 | expect(fmt.oneLine('hello\nworld')).toBe('hello world') 57 | }) 58 | 59 | it('should trim leading/trailing spaces and newlines before joining', () => { 60 | expect(fmt.oneLine(' hello \n world \n')).toBe('hello world') 61 | }) 62 | 63 | it('should remove empty lines before joining', () => { 64 | expect(fmt.oneLine('hello\n\nworld')).toBe('hello world') 65 | }) 66 | 67 | it('should handle a string that is already a single line', () => { 68 | expect(fmt.oneLine('hello world')).toBe('hello world') 69 | }) 70 | 71 | it('should handle a string with only spaces and newlines', () => { 72 | expect(fmt.oneLine(' \n \n ')).toBe('') 73 | }) 74 | }) 75 | 76 | describe('asTSV()', () => { 77 | it('should convert an empty array to an empty string', async () => { 78 | expect(await fmt.asTSV([])).toBe('') 79 | }) 80 | 81 | it('should convert an array of one object to a TSV string', async () => { 82 | const data = [{ a: 1, b: 'hello' }] 83 | expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello') 84 | }) 85 | 86 | it('should convert an array of multiple objects to a TSV string', async () => { 87 | const data = [ 88 | { a: 1, b: 'hello' }, 89 | { a: 2, b: 'world' }, 90 | ] 91 | expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\tworld') 92 | }) 93 | 94 | it('should handle objects with different keys (using keys from the first object as headers)', async () => { 95 | const data = [ 96 | { a: 1, b: 'hello' }, 97 | { a: 2, c: 'world' }, 98 | ] 99 | expect(await fmt.asTSV(data)).toBe('a\tb\n1\thello\n2\t') 100 | expect(await fmt.asTSV(data)).toMatchInlineSnapshot(` 101 | "a b 102 | 1 hello 103 | 2 " 104 | `) 105 | }) 106 | 107 | it('should handle values with tabs and newlines (fast-csv should quote them)', async () => { 108 | const data = [{ name: 'John\tDoe', description: 'Line1\nLine2' }] 109 | expect(await fmt.asTSV(data)).toBe('name\tdescription\n"John\tDoe"\t"Line1\nLine2"') 110 | expect(await fmt.asTSV(data)).toMatchInlineSnapshot(` 111 | "name description 112 | "John Doe" "Line1 113 | Line2"" 114 | `) 115 | }) 116 | 117 | it('should handle values with quotes (fast-csv should escape them)', async () => { 118 | const data = [{ name: 'James "Jim" Raynor' }] 119 | expect(await fmt.asTSV(data)).toBe('name\n"James ""Jim"" Raynor"') 120 | expect(await fmt.asTSV(data)).toMatchInlineSnapshot(` 121 | "name 122 | "James ""Jim"" Raynor"" 123 | `) 124 | }) 125 | }) 126 | }) 127 | ``` -------------------------------------------------------------------------------- /apps/logpush/src/tools/logpush.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api' 4 | import { getProps } from '@repo/mcp-common/src/get-props' 5 | 6 | import type { LogsMCP } from '../logpush.app' 7 | 8 | const zJobIdentifier = z.number().int().min(1).optional().describe('Unique id of the job.') 9 | const zEnabled = z.boolean().optional().describe('Flag that indicates if the job is enabled.') 10 | const zName = z 11 | .string() 12 | .regex(/^[a-zA-Z0-9\-.]*$/) 13 | .max(512) 14 | .nullable() 15 | .optional() 16 | .describe('Optional human readable job name. Not unique.') 17 | const zDataset = z 18 | .string() 19 | .regex(/^[a-zA-Z0-9_-]*$/) 20 | .max(256) 21 | .nullable() 22 | .optional() 23 | .describe('Name of the dataset.') 24 | const zLastComplete = z 25 | .string() 26 | .datetime() 27 | .nullable() 28 | .optional() 29 | .describe('Records the last time for which logs have been successfully pushed.') 30 | const zLastError = z 31 | .string() 32 | .datetime() 33 | .nullable() 34 | .optional() 35 | .describe('Records the last time the job failed.') 36 | const zErrorMessage = z 37 | .string() 38 | .nullable() 39 | .optional() 40 | .describe('If not null, the job is currently failing.') 41 | 42 | export const zLogpushJob = z 43 | .object({ 44 | id: zJobIdentifier, 45 | enabled: zEnabled, 46 | name: zName, 47 | dataset: zDataset, 48 | last_complete: zLastComplete, 49 | last_error: zLastError, 50 | error_message: zErrorMessage, 51 | }) 52 | .nullable() 53 | .optional() 54 | 55 | const zApiResponseCommon = z.object({ 56 | success: z.literal(true), 57 | errors: z.array(z.object({ message: z.string() })).optional(), 58 | }) 59 | 60 | const zLogPushJobResults = z.array(zLogpushJob).optional() 61 | 62 | // The complete schema for zone_logpush_job_response_collection 63 | export const zLogpushJobResponseCollection = zApiResponseCommon.extend({ 64 | result: zLogPushJobResults, 65 | }) 66 | 67 | /** 68 | * Fetches available telemetry keys for a specified Cloudflare Worker 69 | * @param accountId Cloudflare account ID 70 | * @param apiToken Cloudflare API token 71 | * @returns List of telemetry keys available for the worker 72 | */ 73 | 74 | export async function handleGetAccountLogPushJobs( 75 | accountId: string, 76 | apiToken: string 77 | ): Promise<z.infer<typeof zLogPushJobResults>> { 78 | // Call the Public API 79 | const data = await fetchCloudflareApi({ 80 | endpoint: `/logpush/jobs`, 81 | accountId, 82 | apiToken, 83 | responseSchema: zLogpushJobResponseCollection, 84 | options: { 85 | method: 'GET', 86 | headers: { 87 | 'Content-Type': 'application/json', 88 | 'portal-version': '2', 89 | }, 90 | }, 91 | }) 92 | 93 | const res = data as z.infer<typeof zLogpushJobResponseCollection> 94 | return (res.result ?? []).slice(0, 100) 95 | } 96 | 97 | /** 98 | * Registers the logs analysis tool with the MCP server 99 | * @param server The MCP server instance 100 | * @param accountId Cloudflare account ID 101 | * @param apiToken Cloudflare API token 102 | */ 103 | export function registerLogsTools(agent: LogsMCP) { 104 | // Register the worker logs analysis tool by worker name 105 | agent.server.tool( 106 | 'logpush_jobs_by_account_id', 107 | `All Logpush jobs by Account ID. 108 | 109 | You should use this tool when: 110 | - You have questions or wish to request information about their Cloudflare Logpush jobs by account 111 | - You want a condensed version for the output results of your account's Cloudflare Logpush job 112 | 113 | This tool returns at most the first 100 jobs. 114 | `, 115 | {}, 116 | async () => { 117 | const accountId = await agent.getActiveAccountId() 118 | if (!accountId) { 119 | return { 120 | content: [ 121 | { 122 | type: 'text', 123 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 124 | }, 125 | ], 126 | } 127 | } 128 | try { 129 | const props = getProps(agent) 130 | const result = await handleGetAccountLogPushJobs(accountId, props.accessToken) 131 | return { 132 | content: [ 133 | { 134 | type: 'text', 135 | text: JSON.stringify({ 136 | result, 137 | }), 138 | }, 139 | ], 140 | } 141 | } catch (e) { 142 | agent.server.recordError(e) 143 | return { 144 | content: [ 145 | { 146 | type: 'text', 147 | text: JSON.stringify({ 148 | error: `Error analyzing logpush jobs: ${e instanceof Error && e.message}`, 149 | }), 150 | }, 151 | ], 152 | } 153 | } 154 | } 155 | ) 156 | } 157 | ``` -------------------------------------------------------------------------------- /apps/browser-rendering/src/tools/browser.tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod' 2 | 3 | import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' 4 | import { getProps } from '@repo/mcp-common/src/get-props' 5 | 6 | import type { BrowserMCP } from '../browser.app' 7 | 8 | export function registerBrowserTools(agent: BrowserMCP) { 9 | agent.server.tool( 10 | 'get_url_html_content', 11 | 'Get page HTML content', 12 | { 13 | url: z.string().url(), 14 | }, 15 | async (params) => { 16 | const accountId = await agent.getActiveAccountId() 17 | if (!accountId) { 18 | return { 19 | content: [ 20 | { 21 | type: 'text', 22 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 23 | }, 24 | ], 25 | } 26 | } 27 | try { 28 | const props = getProps(agent) 29 | const client = getCloudflareClient(props.accessToken) 30 | const r = await client.browserRendering.content.create({ 31 | account_id: accountId, 32 | url: params.url, 33 | }) 34 | 35 | return { 36 | content: [ 37 | { 38 | type: 'text', 39 | text: JSON.stringify({ 40 | result: r, 41 | }), 42 | }, 43 | ], 44 | } 45 | } catch (error) { 46 | return { 47 | content: [ 48 | { 49 | type: 'text', 50 | text: `Error getting page html: ${error instanceof Error && error.message}`, 51 | }, 52 | ], 53 | } 54 | } 55 | } 56 | ) 57 | 58 | agent.server.tool( 59 | 'get_url_markdown', 60 | 'Get page converted into Markdown', 61 | { 62 | url: z.string().url(), 63 | }, 64 | async (params) => { 65 | const accountId = await agent.getActiveAccountId() 66 | if (!accountId) { 67 | return { 68 | content: [ 69 | { 70 | type: 'text', 71 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 72 | }, 73 | ], 74 | } 75 | } 76 | try { 77 | const props = getProps(agent) 78 | const client = getCloudflareClient(props.accessToken) 79 | const r = (await client.post(`/accounts/${accountId}/browser-rendering/markdown`, { 80 | body: { 81 | url: params.url, 82 | }, 83 | })) as { result: string } 84 | 85 | return { 86 | content: [ 87 | { 88 | type: 'text', 89 | text: JSON.stringify({ 90 | result: r.result, 91 | }), 92 | }, 93 | ], 94 | } 95 | } catch (error) { 96 | return { 97 | content: [ 98 | { 99 | type: 'text', 100 | text: `Error getting page in markdown: ${error instanceof Error && error.message}`, 101 | }, 102 | ], 103 | } 104 | } 105 | } 106 | ) 107 | 108 | agent.server.tool( 109 | 'get_url_screenshot', 110 | 'Get page screenshot', 111 | { 112 | url: z.string().url(), 113 | viewport: z 114 | .object({ 115 | height: z.number().default(600), 116 | width: z.number().default(800), 117 | }) 118 | .optional(), 119 | }, 120 | async (params) => { 121 | const accountId = await agent.getActiveAccountId() 122 | if (!accountId) { 123 | return { 124 | content: [ 125 | { 126 | type: 'text', 127 | text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', 128 | }, 129 | ], 130 | } 131 | } 132 | try { 133 | const props = getProps(agent) 134 | // Cf client appears to be broken, so we use the raw API instead. 135 | // const client = getCloudflareClient(props.accessToken) 136 | // const r = await client.browserRendering.screenshot.create({ 137 | // account_id: accountId, 138 | // url: params.url, 139 | // viewport: params.viewport, 140 | // }) 141 | 142 | const r = await fetch( 143 | `https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/screenshot`, 144 | { 145 | method: 'POST', 146 | headers: { 147 | 'Content-Type': 'application/json', 148 | Authorization: `Bearer ${props.accessToken}`, 149 | }, 150 | body: JSON.stringify({ 151 | url: params.url, 152 | viewport: params.viewport, 153 | }), 154 | } 155 | ) 156 | 157 | const arrayBuffer = await r.arrayBuffer() 158 | const base64Image = Buffer.from(arrayBuffer).toString('base64') 159 | 160 | return { 161 | content: [ 162 | { 163 | type: 'image', 164 | mimeType: 'image/png', 165 | data: base64Image, 166 | }, 167 | ], 168 | } 169 | } catch (error) { 170 | return { 171 | content: [ 172 | { 173 | type: 'text', 174 | text: `Error getting page in markdown: ${error instanceof Error && error.message}`, 175 | }, 176 | ], 177 | } 178 | } 179 | } 180 | ) 181 | } 182 | ``` -------------------------------------------------------------------------------- /apps/cloudflare-one-casb/src/cf1-casb.app.ts: -------------------------------------------------------------------------------- ```typescript 1 | import OAuthProvider from '@cloudflare/workers-oauth-provider' 2 | import { McpAgent } from 'agents/mcp' 3 | 4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' 5 | import { 6 | createAuthHandlers, 7 | handleTokenExchangeCallback, 8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler' 9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' 10 | import { getEnv } from '@repo/mcp-common/src/env' 11 | import { getProps } from '@repo/mcp-common/src/get-props' 12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes' 13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server' 14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' 15 | 16 | import { MetricsTracker } from '../../../packages/mcp-observability/src' 17 | import { registerIntegrationsTools } from './tools/integrations.tools' 18 | 19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' 20 | import type { Env } from './cf1-casb.context' 21 | 22 | export { UserDetails } 23 | 24 | const env = getEnv<Env>() 25 | 26 | const metrics = new MetricsTracker(env.MCP_METRICS, { 27 | name: env.MCP_SERVER_NAME, 28 | version: env.MCP_SERVER_VERSION, 29 | }) 30 | 31 | // Context from the auth process, encrypted & stored in the auth token 32 | // and provided to the DurableMCP as this.props 33 | type Props = AuthProps 34 | 35 | type State = { activeAccountId: string | null } 36 | export class CASBMCP extends McpAgent<Env, State, Props> { 37 | _server: CloudflareMCPServer | undefined 38 | set server(server: CloudflareMCPServer) { 39 | this._server = server 40 | } 41 | 42 | get server(): CloudflareMCPServer { 43 | if (!this._server) { 44 | throw new Error('Tried to access server before it was initialized') 45 | } 46 | 47 | return this._server 48 | } 49 | 50 | constructor(ctx: DurableObjectState, env: Env) { 51 | super(ctx, env) 52 | } 53 | 54 | async init() { 55 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point 56 | const props = getProps(this) 57 | const userId = props.type === 'user_token' ? props.user.id : undefined 58 | 59 | this.server = new CloudflareMCPServer({ 60 | userId, 61 | wae: this.env.MCP_METRICS, 62 | serverInfo: { 63 | name: this.env.MCP_SERVER_NAME, 64 | version: this.env.MCP_SERVER_VERSION, 65 | }, 66 | }) 67 | 68 | registerAccountTools(this) 69 | registerIntegrationsTools(this) 70 | } 71 | 72 | async getActiveAccountId() { 73 | try { 74 | const props = getProps(this) 75 | // account tokens are scoped to one account 76 | if (props.type === 'account_token') { 77 | return props.account.id 78 | } 79 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it 80 | // we do this so we can persist activeAccountId across sessions 81 | const userDetails = getUserDetails(env, props.user.id) 82 | return await userDetails.getActiveAccountId() 83 | } catch (e) { 84 | this.server.recordError(e) 85 | return null 86 | } 87 | } 88 | 89 | async setActiveAccountId(accountId: string) { 90 | try { 91 | const props = getProps(this) 92 | // account tokens are scoped to one account 93 | if (props.type === 'account_token') { 94 | return 95 | } 96 | const userDetails = getUserDetails(env, props.user.id) 97 | await userDetails.setActiveAccountId(accountId) 98 | } catch (e) { 99 | this.server.recordError(e) 100 | } 101 | } 102 | } 103 | const CloudflareOneCasbScopes = { 104 | ...RequiredScopes, 105 | 'account:read': 'See your account info such as account details, analytics, and memberships.', 106 | 'teams:read': 'See Cloudflare One Resources', 107 | } as const 108 | 109 | export default { 110 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { 111 | if (await isApiTokenRequest(req, env)) { 112 | return await handleApiTokenMode(CASBMCP, req, env, ctx) 113 | } 114 | 115 | return new OAuthProvider({ 116 | apiHandlers: { 117 | '/mcp': CASBMCP.serve('/mcp'), 118 | '/sse': CASBMCP.serveSSE('/sse'), 119 | }, 120 | // @ts-ignore 121 | defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }), 122 | authorizeEndpoint: '/oauth/authorize', 123 | tokenEndpoint: '/token', 124 | tokenExchangeCallback: (options) => 125 | handleTokenExchangeCallback( 126 | options, 127 | env.CLOUDFLARE_CLIENT_ID, 128 | env.CLOUDFLARE_CLIENT_SECRET 129 | ), 130 | // Cloudflare access token TTL 131 | accessTokenTTL: 3600, 132 | clientRegistrationEndpoint: '/register', 133 | }).fetch(req, env, ctx) 134 | }, 135 | } 136 | ``` -------------------------------------------------------------------------------- /apps/autorag/src/autorag.app.ts: -------------------------------------------------------------------------------- ```typescript 1 | import OAuthProvider from '@cloudflare/workers-oauth-provider' 2 | import { McpAgent } from 'agents/mcp' 3 | 4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' 5 | import { 6 | createAuthHandlers, 7 | handleTokenExchangeCallback, 8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler' 9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' 10 | import { getEnv } from '@repo/mcp-common/src/env' 11 | import { getProps } from '@repo/mcp-common/src/get-props' 12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes' 13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server' 14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' 15 | 16 | import { MetricsTracker } from '../../../packages/mcp-observability/src' 17 | import { registerAutoRAGTools } from './tools/autorag.tools' 18 | 19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' 20 | import type { Env } from './autorag.context' 21 | 22 | const env = getEnv<Env>() 23 | 24 | export { UserDetails } 25 | 26 | const metrics = new MetricsTracker(env.MCP_METRICS, { 27 | name: env.MCP_SERVER_NAME, 28 | version: env.MCP_SERVER_VERSION, 29 | }) 30 | 31 | // Context from the auth process, encrypted & stored in the auth token 32 | // and provided to the DurableMCP as this.props 33 | type Props = AuthProps 34 | type State = { activeAccountId: string | null } 35 | 36 | export class AutoRAGMCP extends McpAgent<Env, State, Props> { 37 | _server: CloudflareMCPServer | undefined 38 | set server(server: CloudflareMCPServer) { 39 | this._server = server 40 | } 41 | get server(): CloudflareMCPServer { 42 | if (!this._server) { 43 | throw new Error('Tried to access server before it was initialized') 44 | } 45 | 46 | return this._server 47 | } 48 | 49 | constructor(ctx: DurableObjectState, env: Env) { 50 | super(ctx, env) 51 | } 52 | 53 | async init() { 54 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point 55 | const props = getProps(this) 56 | const userId = props.type === 'user_token' ? props.user.id : undefined 57 | 58 | this.server = new CloudflareMCPServer({ 59 | userId, 60 | wae: this.env.MCP_METRICS, 61 | serverInfo: { 62 | name: this.env.MCP_SERVER_NAME, 63 | version: this.env.MCP_SERVER_VERSION, 64 | }, 65 | }) 66 | 67 | registerAccountTools(this) 68 | 69 | // Register Cloudflare Log Push tools 70 | registerAutoRAGTools(this) 71 | } 72 | 73 | async getActiveAccountId() { 74 | try { 75 | const props = getProps(this) 76 | // account tokens are scoped to one account 77 | if (props.type === 'account_token') { 78 | return props.account.id 79 | } 80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it 81 | // we do this so we can persist activeAccountId across sessions 82 | const userDetails = getUserDetails(env, props.user.id) 83 | return await userDetails.getActiveAccountId() 84 | } catch (e) { 85 | this.server.recordError(e) 86 | return null 87 | } 88 | } 89 | 90 | async setActiveAccountId(accountId: string) { 91 | try { 92 | const props = getProps(this) 93 | // account tokens are scoped to one account 94 | if (props.type === 'account_token') { 95 | return 96 | } 97 | const userDetails = getUserDetails(env, props.user.id) 98 | await userDetails.setActiveAccountId(accountId) 99 | } catch (e) { 100 | this.server.recordError(e) 101 | } 102 | } 103 | } 104 | 105 | const LogPushScopes = { 106 | ...RequiredScopes, 107 | 'account:read': 'See your account info such as account details, analytics, and memberships.', 108 | 'rag:write': 'Grants write level access to AutoRag.', 109 | } as const 110 | 111 | export default { 112 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { 113 | if (await isApiTokenRequest(req, env)) { 114 | return await handleApiTokenMode(AutoRAGMCP, req, env, ctx) 115 | } 116 | 117 | return new OAuthProvider({ 118 | apiHandlers: { 119 | '/mcp': AutoRAGMCP.serve('/mcp'), 120 | '/sse': AutoRAGMCP.serveSSE('/sse'), 121 | }, 122 | // @ts-ignore 123 | defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }), 124 | authorizeEndpoint: '/oauth/authorize', 125 | tokenEndpoint: '/token', 126 | tokenExchangeCallback: (options) => 127 | handleTokenExchangeCallback( 128 | options, 129 | env.CLOUDFLARE_CLIENT_ID, 130 | env.CLOUDFLARE_CLIENT_SECRET 131 | ), 132 | // Cloudflare access token TTL 133 | accessTokenTTL: 3600, 134 | clientRegistrationEndpoint: '/register', 135 | }).fetch(req, env, ctx) 136 | }, 137 | } 138 | ``` -------------------------------------------------------------------------------- /apps/browser-rendering/src/browser.app.ts: -------------------------------------------------------------------------------- ```typescript 1 | import OAuthProvider from '@cloudflare/workers-oauth-provider' 2 | import { McpAgent } from 'agents/mcp' 3 | 4 | import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode' 5 | import { 6 | createAuthHandlers, 7 | handleTokenExchangeCallback, 8 | } from '@repo/mcp-common/src/cloudflare-oauth-handler' 9 | import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do' 10 | import { getEnv } from '@repo/mcp-common/src/env' 11 | import { getProps } from '@repo/mcp-common/src/get-props' 12 | import { RequiredScopes } from '@repo/mcp-common/src/scopes' 13 | import { CloudflareMCPServer } from '@repo/mcp-common/src/server' 14 | import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools' 15 | 16 | import { MetricsTracker } from '../../../packages/mcp-observability/src' 17 | import { registerBrowserTools } from './tools/browser.tools' 18 | 19 | import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' 20 | import type { Env } from './browser.context' 21 | 22 | const env = getEnv<Env>() 23 | 24 | export { UserDetails } 25 | 26 | const metrics = new MetricsTracker(env.MCP_METRICS, { 27 | name: env.MCP_SERVER_NAME, 28 | version: env.MCP_SERVER_VERSION, 29 | }) 30 | 31 | // Context from the auth process, encrypted & stored in the auth token 32 | // and provided to the DurableMCP as this.props 33 | type Props = AuthProps 34 | type State = { activeAccountId: string | null } 35 | 36 | export class BrowserMCP extends McpAgent<Env, State, Props> { 37 | _server: CloudflareMCPServer | undefined 38 | set server(server: CloudflareMCPServer) { 39 | this._server = server 40 | } 41 | get server(): CloudflareMCPServer { 42 | if (!this._server) { 43 | throw new Error('Tried to access server before it was initialized') 44 | } 45 | 46 | return this._server 47 | } 48 | 49 | constructor(ctx: DurableObjectState, env: Env) { 50 | super(ctx, env) 51 | } 52 | 53 | async init() { 54 | // TODO: Probably we'll want to track account tokens usage through an account identifier at some point 55 | const props = getProps(this) 56 | const userId = props.type === 'user_token' ? props.user.id : undefined 57 | 58 | this.server = new CloudflareMCPServer({ 59 | userId, 60 | wae: this.env.MCP_METRICS, 61 | serverInfo: { 62 | name: this.env.MCP_SERVER_NAME, 63 | version: this.env.MCP_SERVER_VERSION, 64 | }, 65 | }) 66 | 67 | registerAccountTools(this) 68 | 69 | // Register Cloudflare Log Push tools 70 | registerBrowserTools(this) 71 | } 72 | 73 | async getActiveAccountId() { 74 | try { 75 | const props = getProps(this) 76 | // account tokens are scoped to one account 77 | if (props.type === 'account_token') { 78 | return props.account.id 79 | } 80 | // Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it 81 | // we do this so we can persist activeAccountId across sessions 82 | const userDetails = getUserDetails(env, props.user.id) 83 | return await userDetails.getActiveAccountId() 84 | } catch (e) { 85 | this.server.recordError(e) 86 | return null 87 | } 88 | } 89 | 90 | async setActiveAccountId(accountId: string) { 91 | try { 92 | const props = getProps(this) 93 | // account tokens are scoped to one account 94 | if (props.type === 'account_token') { 95 | return 96 | } 97 | const userDetails = getUserDetails(env, props.user.id) 98 | await userDetails.setActiveAccountId(accountId) 99 | } catch (e) { 100 | this.server.recordError(e) 101 | } 102 | } 103 | } 104 | 105 | const BrowserScopes = { 106 | ...RequiredScopes, 107 | 'account:read': 'See your account info such as account details, analytics, and memberships.', 108 | 'browser:write': 'Grants write level access to Browser Rendering.', 109 | } as const 110 | 111 | export default { 112 | fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { 113 | if (await isApiTokenRequest(req, env)) { 114 | return await handleApiTokenMode(BrowserMCP, req, env, ctx) 115 | } 116 | 117 | return new OAuthProvider({ 118 | apiHandlers: { 119 | '/mcp': BrowserMCP.serve('/mcp'), 120 | '/sse': BrowserMCP.serveSSE('/sse'), 121 | }, 122 | // @ts-ignore 123 | defaultHandler: createAuthHandlers({ scopes: BrowserScopes, metrics }), 124 | authorizeEndpoint: '/oauth/authorize', 125 | tokenEndpoint: '/token', 126 | tokenExchangeCallback: (options) => 127 | handleTokenExchangeCallback( 128 | options, 129 | env.CLOUDFLARE_CLIENT_ID, 130 | env.CLOUDFLARE_CLIENT_SECRET 131 | ), 132 | // Cloudflare access token TTL 133 | accessTokenTTL: 3600, 134 | clientRegistrationEndpoint: '/register', 135 | }).fetch(req, env, ctx) 136 | }, 137 | } 138 | ```