This is page 6 of 29. Use http://codebase.md/cloudflare/mcp-server-cloudflare?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .dockerignore
├── .editorconfig
├── .eslintrc.cjs
├── .github
│ ├── actions
│ │ └── setup
│ │ └── action.yml
│ ├── ISSUE_TEMPLATE
│ │ └── bug_report.md
│ └── workflows
│ ├── branches.yml
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.cjs
├── .syncpackrc.cjs
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── apps
│ ├── ai-gateway
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── ai-gateway.app.ts
│ │ │ ├── ai-gateway.context.ts
│ │ │ ├── tools
│ │ │ │ └── ai-gateway.tools.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── auditlogs
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── auditlogs.app.ts
│ │ │ ├── auditlogs.context.ts
│ │ │ └── tools
│ │ │ └── auditlogs.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── autorag
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── autorag.app.ts
│ │ │ ├── autorag.context.ts
│ │ │ ├── tools
│ │ │ │ └── autorag.tools.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── browser-rendering
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── browser.app.ts
│ │ │ ├── browser.context.ts
│ │ │ └── tools
│ │ │ └── browser.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── cloudflare-one-casb
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── cf1-casb.app.ts
│ │ │ ├── cf1-casb.context.ts
│ │ │ └── tools
│ │ │ └── integrations.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── demo-day
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── frontend
│ │ │ ├── index.html
│ │ │ ├── public
│ │ │ │ ├── anthropic.svg
│ │ │ │ ├── asana.svg
│ │ │ │ ├── atlassian.svg
│ │ │ │ ├── canva.svg
│ │ │ │ ├── cloudflare_logo.svg
│ │ │ │ ├── cloudflare.svg
│ │ │ │ ├── dina.jpg
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── favicon.png
│ │ │ │ ├── intercom.svg
│ │ │ │ ├── linear.svg
│ │ │ │ ├── matt.jpg
│ │ │ │ ├── mcp_demo_day.svg
│ │ │ │ ├── mcpog.png
│ │ │ │ ├── more.svg
│ │ │ │ ├── paypal.svg
│ │ │ │ ├── pete.jpeg
│ │ │ │ ├── sentry.svg
│ │ │ │ ├── special_guest.png
│ │ │ │ ├── square.svg
│ │ │ │ ├── stripe.svg
│ │ │ │ ├── sunil.jpg
│ │ │ │ └── webflow.svg
│ │ │ ├── script.js
│ │ │ └── styles.css
│ │ ├── package.json
│ │ ├── src
│ │ │ └── demo-day.app.ts
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.json
│ ├── dex-analysis
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── dex-analysis.app.ts
│ │ │ ├── dex-analysis.context.ts
│ │ │ ├── tools
│ │ │ │ └── dex-analysis.tools.ts
│ │ │ └── warp_diag_reader.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── dns-analytics
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── dns-analytics.app.ts
│ │ │ ├── dns-analytics.context.ts
│ │ │ └── tools
│ │ │ └── dex-analytics.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-ai-search
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-ai-search.app.ts
│ │ │ └── docs-ai-search.context.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-autorag
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-autorag.app.ts
│ │ │ ├── docs-autorag.context.ts
│ │ │ └── tools
│ │ │ └── docs-autorag.tools.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── docs-vectorize
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── docs-vectorize.app.ts
│ │ │ └── docs-vectorize.context.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── graphql
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── graphql.app.ts
│ │ │ ├── graphql.context.ts
│ │ │ └── tools
│ │ │ └── graphql.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── logpush
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── logpush.app.ts
│ │ │ ├── logpush.context.ts
│ │ │ └── tools
│ │ │ └── logpush.tools.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── radar
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── radar.app.ts
│ │ │ ├── radar.context.ts
│ │ │ ├── tools
│ │ │ │ ├── radar.tools.ts
│ │ │ │ └── url-scanner.tools.ts
│ │ │ ├── types
│ │ │ │ ├── radar.ts
│ │ │ │ └── url-scanner.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── sandbox-container
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── container
│ │ │ ├── fileUtils.spec.ts
│ │ │ ├── fileUtils.ts
│ │ │ ├── sandbox.container.app.ts
│ │ │ └── tsconfig.json
│ │ ├── CONTRIBUTING.md
│ │ ├── Dockerfile
│ │ ├── evals
│ │ │ ├── exec.eval.ts
│ │ │ ├── files.eval.ts
│ │ │ ├── initialize.eval.ts
│ │ │ └── utils.ts
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── server
│ │ │ ├── containerHelpers.ts
│ │ │ ├── containerManager.ts
│ │ │ ├── containerMcp.ts
│ │ │ ├── metrics.ts
│ │ │ ├── prompts.ts
│ │ │ ├── sandbox.server.app.ts
│ │ │ ├── sandbox.server.context.ts
│ │ │ ├── userContainer.ts
│ │ │ ├── utils.spec.ts
│ │ │ └── utils.ts
│ │ ├── shared
│ │ │ ├── consts.ts
│ │ │ └── schema.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.evals.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── workers-bindings
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── evals
│ │ │ ├── accounts.eval.ts
│ │ │ ├── hyperdrive.eval.ts
│ │ │ ├── kv_namespaces.eval.ts
│ │ │ ├── types.d.ts
│ │ │ └── utils.ts
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── bindings.app.ts
│ │ │ └── bindings.context.ts
│ │ ├── tsconfig.json
│ │ ├── vitest.config.evals.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ ├── workers-builds
│ │ ├── .dev.vars.example
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── tools
│ │ │ │ └── workers-builds.tools.ts
│ │ │ ├── workers-builds.app.ts
│ │ │ └── workers-builds.context.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vite.config.mts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.jsonc
│ └── workers-observability
│ ├── .dev.vars.example
│ ├── .eslintrc.cjs
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING.md
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── tools
│ │ │ └── workers-observability.tools.ts
│ │ ├── workers-observability.app.ts
│ │ └── workers-observability.context.ts
│ ├── tsconfig.json
│ ├── types.d.ts
│ ├── vitest.config.ts
│ ├── worker-configuration.d.ts
│ └── wrangler.jsonc
├── CONTRIBUTING.md
├── implementation-guides
│ ├── evals.md
│ ├── tools.md
│ └── type-validators.md
├── LICENSE
├── package.json
├── packages
│ ├── eslint-config
│ │ ├── CHANGELOG.md
│ │ ├── default.cjs
│ │ ├── package.json
│ │ └── README.md
│ ├── eval-tools
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── runTask.ts
│ │ │ ├── scorers.ts
│ │ │ └── test-models.ts
│ │ ├── tsconfig.json
│ │ ├── worker-configuration.d.ts
│ │ └── wrangler.json
│ ├── mcp-common
│ │ ├── .eslintrc.cjs
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── api
│ │ │ │ ├── account.api.ts
│ │ │ │ ├── cf1-integration.api.ts
│ │ │ │ ├── workers-builds.api.ts
│ │ │ │ ├── workers-observability.api.ts
│ │ │ │ ├── workers.api.ts
│ │ │ │ └── zone.api.ts
│ │ │ ├── api-handler.ts
│ │ │ ├── api-token-mode.ts
│ │ │ ├── cloudflare-api.ts
│ │ │ ├── cloudflare-auth.ts
│ │ │ ├── cloudflare-oauth-handler.ts
│ │ │ ├── config.ts
│ │ │ ├── constants.ts
│ │ │ ├── durable-kv-store.ts
│ │ │ ├── durable-objects
│ │ │ │ └── user_details.do.ts
│ │ │ ├── env.ts
│ │ │ ├── format.spec.ts
│ │ │ ├── format.ts
│ │ │ ├── get-props.ts
│ │ │ ├── mcp-error.ts
│ │ │ ├── poll.ts
│ │ │ ├── prompts
│ │ │ │ ├── docs-ai-search.prompts.ts
│ │ │ │ └── docs-vectorize.prompts.ts
│ │ │ ├── scopes.ts
│ │ │ ├── sentry.ts
│ │ │ ├── server.ts
│ │ │ ├── tools
│ │ │ │ ├── account.tools.ts
│ │ │ │ ├── d1.tools.ts
│ │ │ │ ├── docs-ai-search.tools.ts
│ │ │ │ ├── docs-vectorize.tools.ts
│ │ │ │ ├── hyperdrive.tools.ts
│ │ │ │ ├── kv_namespace.tools.ts
│ │ │ │ ├── r2_bucket.tools.ts
│ │ │ │ ├── worker.tools.ts
│ │ │ │ └── zone.tools.ts
│ │ │ ├── types
│ │ │ │ ├── cf1-integrations.types.ts
│ │ │ │ ├── cloudflare-mcp-agent.types.ts
│ │ │ │ ├── d1.types.ts
│ │ │ │ ├── hyperdrive.types.ts
│ │ │ │ ├── kv_namespace.types.ts
│ │ │ │ ├── r2_bucket.types.ts
│ │ │ │ ├── shared.types.ts
│ │ │ │ ├── tools.types.ts
│ │ │ │ ├── workers-builds.types.ts
│ │ │ │ ├── workers-logs.types.ts
│ │ │ │ └── workers.types.ts
│ │ │ ├── utils.spec.ts
│ │ │ ├── utils.ts
│ │ │ ├── v4-api.ts
│ │ │ └── workers-oauth-utils.ts
│ │ ├── tests
│ │ │ └── utils
│ │ │ └── cloudflare-mock.ts
│ │ ├── tsconfig.json
│ │ ├── types.d.ts
│ │ ├── vitest.config.ts
│ │ └── worker-configuration.d.ts
│ ├── mcp-observability
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── analytics-engine.ts
│ │ │ ├── index.ts
│ │ │ └── metrics.ts
│ │ ├── tsconfig.json
│ │ └── worker-configuration.d.ts
│ ├── tools
│ │ ├── .eslintrc.cjs
│ │ ├── bin
│ │ │ ├── run-changeset-new
│ │ │ ├── run-eslint-workers
│ │ │ ├── run-fix-deps
│ │ │ ├── run-tsc
│ │ │ ├── run-turbo
│ │ │ ├── run-vitest
│ │ │ ├── run-vitest-ci
│ │ │ ├── run-wrangler-deploy
│ │ │ ├── run-wrangler-types
│ │ │ └── runx
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── bin
│ │ │ │ └── runx.ts
│ │ │ ├── changesets.spec.ts
│ │ │ ├── changesets.ts
│ │ │ ├── cmd
│ │ │ │ └── deploy-published-packages.ts
│ │ │ ├── proc.ts
│ │ │ ├── test
│ │ │ │ ├── fixtures
│ │ │ │ │ └── changesets
│ │ │ │ │ ├── empty
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ ├── invalid-json
│ │ │ │ │ │ └── published-packages.json
│ │ │ │ │ ├── invalid-schema
│ │ │ │ │ │ └── published-packages.json
│ │ │ │ │ └── valid
│ │ │ │ │ └── published-packages.json
│ │ │ │ └── setup.ts
│ │ │ └── tsconfig.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── typescript-config
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── tools.json
│ ├── workers-lib.json
│ └── workers.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── server.json
├── tsconfig.json
├── turbo.json
└── vitest.workspace.ts
```
# Files
--------------------------------------------------------------------------------
/packages/mcp-common/src/cloudflare-oauth-handler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { zValidator } from '@hono/zod-validator'
2 | import { Hono } from 'hono'
3 | import { z } from 'zod'
4 |
5 | import { AuthUser } from '../../mcp-observability/src'
6 | import {
7 | generatePKCECodes,
8 | getAuthorizationURL,
9 | getAuthToken,
10 | refreshAuthToken,
11 | } from './cloudflare-auth'
12 | import { McpError } from './mcp-error'
13 | import { useSentry } from './sentry'
14 | import { V4Schema } from './v4-api'
15 | import {
16 | bindStateToSession,
17 | clientIdAlreadyApproved,
18 | createOAuthState,
19 | generateCSRFProtection,
20 | OAuthError,
21 | parseRedirectApproval,
22 | renderApprovalDialog,
23 | validateOAuthState,
24 | } from './workers-oauth-utils'
25 |
26 | import type {
27 | AuthRequest,
28 | OAuthHelpers,
29 | TokenExchangeCallbackOptions,
30 | TokenExchangeCallbackResult,
31 | } from '@cloudflare/workers-oauth-provider'
32 | import type { Context } from 'hono'
33 | import type { MetricsTracker } from '../../mcp-observability/src'
34 | import type { BaseHonoContext } from './sentry'
35 |
36 | type AuthContext = {
37 | Bindings: {
38 | OAUTH_PROVIDER: OAuthHelpers
39 | OAUTH_KV: KVNamespace
40 | MCP_COOKIE_ENCRYPTION_KEY: string
41 | CLOUDFLARE_CLIENT_ID: string
42 | CLOUDFLARE_CLIENT_SECRET: string
43 | MCP_SERVER_NAME?: string
44 | MCP_SERVER_DESCRIPTION?: string
45 | }
46 | } & BaseHonoContext
47 |
48 | const AuthQuery = z.object({
49 | code: z.string().describe('OAuth code from CF dash'),
50 | state: z.string().describe('Value of the OAuth state'),
51 | scope: z.string().describe('OAuth scopes granted'),
52 | })
53 |
54 | type UserSchema = z.infer<typeof UserSchema>
55 | const UserSchema = z.object({
56 | id: z.string(),
57 | email: z.string(),
58 | })
59 | const AccountSchema = z.object({
60 | name: z.string(),
61 | id: z.string(),
62 | })
63 | type AccountsSchema = z.infer<typeof AccountsSchema>
64 | const AccountsSchema = z.array(AccountSchema)
65 |
66 | const AccountAuthProps = z.object({
67 | type: z.literal('account_token'),
68 | accessToken: z.string(),
69 | account: AccountSchema,
70 | })
71 | const UserAuthProps = z.object({
72 | type: z.literal('user_token'),
73 | accessToken: z.string(),
74 | user: UserSchema,
75 | accounts: AccountsSchema,
76 | refreshToken: z.string().optional(),
77 | })
78 | export type AuthProps = z.infer<typeof AuthProps>
79 | const AuthProps = z.discriminatedUnion('type', [AccountAuthProps, UserAuthProps])
80 |
81 | export async function getUserAndAccounts(
82 | accessToken: string,
83 | devModeHeaders?: HeadersInit
84 | ): Promise<{ user: UserSchema | null; accounts: AccountsSchema }> {
85 | const headers = devModeHeaders
86 | ? devModeHeaders
87 | : {
88 | Authorization: `Bearer ${accessToken}`,
89 | }
90 |
91 | // Fetch the user & accounts info from Cloudflare
92 | const [userResponse, accountsResponse] = await Promise.all([
93 | fetch('https://api.cloudflare.com/client/v4/user', {
94 | headers,
95 | }),
96 | fetch('https://api.cloudflare.com/client/v4/accounts', {
97 | headers,
98 | }),
99 | ])
100 |
101 | const { result: user } = V4Schema(UserSchema).parse(await userResponse.json())
102 | const { result: accounts } = V4Schema(AccountsSchema).parse(await accountsResponse.json())
103 | if (!user || !userResponse.ok) {
104 | // If accounts is present, then assume that we have an account scoped token
105 | if (accounts !== null) {
106 | return { user: null, accounts }
107 | }
108 | console.log(user)
109 | throw new McpError('Failed to fetch user', 500, { reportToSentry: true })
110 | }
111 | if (!accounts || !accountsResponse.ok) {
112 | console.log(accounts)
113 | throw new McpError('Failed to fetch accounts', 500, { reportToSentry: true })
114 | }
115 |
116 | return { user, accounts }
117 | }
118 |
119 | /**
120 | * Exchanges an OAuth authorization code for access and refresh tokens, then fetches user and account details.
121 | *
122 | * @param c - Hono context containing OAuth environment variables (client ID/secret)
123 | * @param code - OAuth authorization code received from the authorization server
124 | * @param code_verifier - PKCE code verifier used to validate the authorization request
125 | * @returns Promise resolving to an object containing access token, refresh token, user profile, and accounts
126 | */
127 | async function getTokenAndUserDetails(
128 | c: Context<AuthContext>,
129 | code: string,
130 | code_verifier: string
131 | ): Promise<{
132 | accessToken: string
133 | refreshToken: string
134 | user: UserSchema
135 | accounts: AccountsSchema
136 | }> {
137 | // Exchange the code for an access token
138 | const { access_token: accessToken, refresh_token: refreshToken } = await getAuthToken({
139 | client_id: c.env.CLOUDFLARE_CLIENT_ID,
140 | client_secret: c.env.CLOUDFLARE_CLIENT_SECRET,
141 | redirect_uri: new URL('/oauth/callback', c.req.url).href,
142 | code,
143 | code_verifier,
144 | })
145 |
146 | const { user, accounts } = await getUserAndAccounts(accessToken)
147 | // User cannot be null for OAuth flow
148 | if (user === null) {
149 | throw new McpError('Failed to fetch user', 500, { reportToSentry: true })
150 | }
151 |
152 | return { accessToken, refreshToken, user, accounts }
153 | }
154 |
155 | export async function handleTokenExchangeCallback(
156 | options: TokenExchangeCallbackOptions,
157 | clientId: string,
158 | clientSecret: string
159 | ): Promise<TokenExchangeCallbackResult | undefined> {
160 | // options.props contains the current props
161 | if (options.grantType === 'refresh_token') {
162 | const props = AuthProps.parse(options.props)
163 | if (props.type === 'account_token') {
164 | // Refreshing an account_token should not be possible, as we only do this for user tokens
165 | throw new McpError('Internal Server Error', 500)
166 | }
167 | if (!props.refreshToken) {
168 | throw new McpError('Missing refreshToken', 500)
169 | }
170 |
171 | // handle token refreshes
172 | const {
173 | access_token: accessToken,
174 | refresh_token: refreshToken,
175 | expires_in,
176 | } = await refreshAuthToken({
177 | client_id: clientId,
178 | client_secret: clientSecret,
179 | refresh_token: props.refreshToken,
180 | })
181 |
182 | return {
183 | newProps: {
184 | ...options.props,
185 | accessToken,
186 | refreshToken,
187 | } satisfies AuthProps,
188 | accessTokenTTL: expires_in,
189 | }
190 | }
191 | }
192 |
193 | /**
194 | * Helper function to redirect to Cloudflare OAuth
195 | *
196 | * Note: We pass the stateToken as a simple string in the URL.
197 | * The existing getAuthorizationURL function will wrap it with the oauthReqInfo
198 | * before base64-encoding.
199 | * On callback, we extract the stateToken, look up the original oauthReqInfo in KV.
200 | */
201 | async function redirectToCloudflare(
202 | c: Context<AuthContext>,
203 | oauthReqInfo: AuthRequest,
204 | stateToken: string,
205 | codeChallenge: string,
206 | scopes: Record<string, string>,
207 | additionalHeaders: Record<string, string> = {}
208 | ): Promise<Response> {
209 | // Create a modified oauthReqInfo that includes our stateToken
210 | const stateWithToken: AuthRequest = {
211 | ...oauthReqInfo,
212 | state: stateToken, // embed our KV state token
213 | }
214 |
215 | const { authUrl } = await getAuthorizationURL({
216 | client_id: c.env.CLOUDFLARE_CLIENT_ID,
217 | redirect_uri: new URL('/oauth/callback', c.req.url).href,
218 | state: stateWithToken,
219 | scopes,
220 | codeChallenge,
221 | })
222 |
223 | return new Response(null, {
224 | status: 302,
225 | headers: {
226 | ...additionalHeaders,
227 | Location: authUrl,
228 | },
229 | })
230 | }
231 |
232 | /**
233 | * Creates a Hono app with OAuth routes for a specific Cloudflare worker
234 | *
235 | * @param scopes optional subset of scopes to request when handling authorization requests
236 | * @param metrics MetricsTracker which is used to track auth metrics
237 | * @returns a Hono app with configured OAuth routes
238 | */
239 | export function createAuthHandlers({
240 | scopes,
241 | metrics,
242 | }: {
243 | scopes: Record<string, string>
244 | metrics: MetricsTracker
245 | }) {
246 | const app = new Hono<AuthContext>()
247 | app.use(useSentry)
248 |
249 | /**
250 | * GET /oauth/authorize - Show consent dialog or redirect if approved
251 | */
252 | app.get(`/oauth/authorize`, async (c) => {
253 | try {
254 | const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw)
255 | oauthReqInfo.scope = Object.keys(scopes)
256 |
257 | if (!oauthReqInfo.clientId) {
258 | return new OAuthError('invalid_request', 'Missing client_id parameter', 400).toResponse()
259 | }
260 |
261 | // Check if client was previously approved (skip consent if so)
262 | if (
263 | await clientIdAlreadyApproved(
264 | c.req.raw,
265 | oauthReqInfo.clientId,
266 | c.env.MCP_COOKIE_ENCRYPTION_KEY
267 | )
268 | ) {
269 | // Client already approved - create state and redirect immediately
270 | const { codeChallenge, codeVerifier } = await generatePKCECodes()
271 | const stateToken = await createOAuthState(oauthReqInfo, c.env.OAUTH_KV, codeVerifier)
272 | const { setCookie: sessionCookie } = await bindStateToSession(stateToken)
273 |
274 | return redirectToCloudflare(c, oauthReqInfo, stateToken, codeChallenge, scopes, {
275 | 'Set-Cookie': sessionCookie,
276 | })
277 | }
278 |
279 | // Client not approved - show consent dialog
280 | const { token: csrfToken, setCookie: csrfCookie } = generateCSRFProtection()
281 |
282 | // Render approval dialog
283 | const response = renderApprovalDialog(c.req.raw, {
284 | client: await c.env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId),
285 | server: {
286 | name: c.env.MCP_SERVER_NAME || 'Cloudflare MCP Server',
287 | logo: 'https://images.mcp.cloudflare.com/mcp.svg',
288 | description:
289 | c.env.MCP_SERVER_DESCRIPTION || 'This server uses Cloudflare for authentication.',
290 | },
291 | state: {
292 | oauthReqInfo,
293 | },
294 | csrfToken,
295 | setCookie: csrfCookie,
296 | })
297 |
298 | return response
299 | } catch (e) {
300 | c.var.sentry?.recordError(e)
301 | let message: string | undefined
302 | if (e instanceof Error) {
303 | message = `${e.name}: ${e.message}`
304 | } else if (typeof e === 'string') {
305 | message = e
306 | } else {
307 | message = 'Unknown error'
308 | }
309 | metrics.logEvent(
310 | new AuthUser({
311 | errorMessage: `Authorize Error: ${message}`,
312 | })
313 | )
314 | if (e instanceof OAuthError) {
315 | return e.toResponse()
316 | }
317 | if (e instanceof McpError) {
318 | return c.text(e.message, { status: e.code })
319 | }
320 | console.error(e)
321 | return c.text('Internal Error', 500)
322 | }
323 | })
324 |
325 | /**
326 | * POST /oauth/authorize - Handle consent form submission
327 | */
328 | app.post(`/oauth/authorize`, async (c) => {
329 | try {
330 | // Validates CSRF token, extracts state, and generates approved client cookie
331 | const { state, headers } = await parseRedirectApproval(
332 | c.req.raw,
333 | c.env.MCP_COOKIE_ENCRYPTION_KEY
334 | )
335 |
336 | if (!state.oauthReqInfo) {
337 | return new OAuthError(
338 | 'invalid_request',
339 | 'Missing OAuth request info in state',
340 | 400
341 | ).toResponse()
342 | }
343 |
344 | const oauthReqInfo = state.oauthReqInfo as AuthRequest
345 |
346 | // Create OAuth state in KV and bind to session
347 | const { codeChallenge, codeVerifier } = await generatePKCECodes()
348 | const stateToken = await createOAuthState(oauthReqInfo, c.env.OAUTH_KV, codeVerifier)
349 | const { setCookie: sessionCookie } = await bindStateToSession(stateToken)
350 |
351 | // Build redirect response
352 | const redirectResponse = await redirectToCloudflare(
353 | c,
354 | oauthReqInfo,
355 | stateToken,
356 | codeChallenge,
357 | scopes
358 | )
359 |
360 | // Add both cookies: approved client cookie (if present) and session binding cookie
361 | // Note: We must use append() for multiple Set-Cookie headers, not combine with commas
362 | if (headers['Set-Cookie']) {
363 | redirectResponse.headers.append('Set-Cookie', headers['Set-Cookie'])
364 | }
365 | redirectResponse.headers.append('Set-Cookie', sessionCookie)
366 |
367 | return redirectResponse
368 | } catch (e) {
369 | c.var.sentry?.recordError(e)
370 | let message: string | undefined
371 | if (e instanceof Error) {
372 | message = `${e.name}: ${e.message}`
373 | } else if (typeof e === 'string') {
374 | message = e
375 | } else {
376 | message = 'Unknown error'
377 | }
378 | metrics.logEvent(
379 | new AuthUser({
380 | errorMessage: `Authorize POST Error: ${message}`,
381 | })
382 | )
383 | if (e instanceof OAuthError) {
384 | return e.toResponse()
385 | }
386 | console.error(e)
387 | return c.text('Internal Error', 500)
388 | }
389 | })
390 |
391 | /**
392 | * GET /oauth/callback - Handle OAuth callback from Cloudflare
393 | */
394 | app.get(`/oauth/callback`, zValidator('query', AuthQuery), async (c) => {
395 | try {
396 | const { code } = c.req.valid('query')
397 |
398 | // Validate state using dual validation (KV + session cookie)
399 | const { oauthReqInfo, codeVerifier, clearCookie } = await validateOAuthState(
400 | c.req.raw,
401 | c.env.OAUTH_KV
402 | )
403 |
404 | if (!oauthReqInfo.clientId) {
405 | return new OAuthError('invalid_request', 'Invalid OAuth request info', 400).toResponse()
406 | }
407 |
408 | // Exchange code for tokens and get user details
409 | const [{ accessToken, refreshToken, user, accounts }] = await Promise.all([
410 | getTokenAndUserDetails(c, code, codeVerifier), // use codeVerifier from KV
411 | c.env.OAUTH_PROVIDER.createClient({
412 | clientId: oauthReqInfo.clientId,
413 | tokenEndpointAuthMethod: 'none',
414 | }),
415 | ])
416 |
417 | // Complete authorization and issue token to MCP client
418 | const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
419 | request: oauthReqInfo,
420 | userId: user.id,
421 | metadata: {
422 | label: user.email,
423 | },
424 | scope: oauthReqInfo.scope,
425 | props: {
426 | type: 'user_token',
427 | user,
428 | accounts,
429 | accessToken,
430 | refreshToken,
431 | } satisfies AuthProps,
432 | })
433 |
434 | metrics.logEvent(
435 | new AuthUser({
436 | userId: user.id,
437 | })
438 | )
439 |
440 | // Redirect back to MCP client with cleared session cookie
441 | return new Response(null, {
442 | status: 302,
443 | headers: {
444 | Location: redirectTo,
445 | 'Set-Cookie': clearCookie,
446 | },
447 | })
448 | } catch (e) {
449 | c.var.sentry?.recordError(e)
450 | let message: string | undefined
451 | if (e instanceof Error) {
452 | console.error(e)
453 | message = `${e.name}: ${e.message}`
454 | } else if (typeof e === 'string') {
455 | message = e
456 | } else {
457 | message = 'Unknown error'
458 | }
459 | metrics.logEvent(
460 | new AuthUser({
461 | errorMessage: `Callback Error: ${message}`,
462 | })
463 | )
464 | if (e instanceof OAuthError) {
465 | return e.toResponse()
466 | }
467 | if (e instanceof McpError) {
468 | return c.text(e.message, { status: e.code })
469 | }
470 | return c.text('Internal Error', 500)
471 | }
472 | })
473 |
474 | return app
475 | }
476 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/types/workers-logs.types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod'
2 |
3 | import { nowISO, parseRelativeTime } from '../utils'
4 |
5 | export const numericalOperations = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'] as const
6 |
7 | export const queryOperations = [
8 | // applies only to strings
9 | 'includes',
10 | 'not_includes',
11 |
12 | // string operations
13 | 'starts_with',
14 | 'regex',
15 |
16 | // existence check
17 | 'exists',
18 | 'is_null',
19 |
20 | // right hand side must be a string with comma separated values
21 | 'in',
22 | 'not_in',
23 |
24 | // numerica
25 | ...numericalOperations,
26 | ] as const
27 |
28 | export const queryOperators = [
29 | 'uniq',
30 | 'count',
31 | 'max',
32 | 'min',
33 | 'sum',
34 | 'avg',
35 | 'median',
36 | 'p001',
37 | 'p01',
38 | 'p05',
39 | 'p10',
40 | 'p25',
41 | 'p75',
42 | 'p90',
43 | 'p95',
44 | 'p99',
45 | 'p999',
46 | 'stddev',
47 | 'variance',
48 | ] as const
49 |
50 | export const zQueryOperator = z.enum(queryOperators)
51 | export const zQueryOperation = z.enum(queryOperations)
52 | export const zQueryNumericalOperations = z.enum(numericalOperations)
53 |
54 | export const zOffsetDirection = z.enum(['next', 'prev'])
55 | export const zFilterCombination = z.enum(['and', 'or', 'AND', 'OR'])
56 |
57 | export const zPrimitiveUnion = z.union([z.string(), z.number(), z.boolean()])
58 |
59 | export const zQueryFilter = z.object({
60 | key: z.string().describe(`Filter field name. IMPORTANT:
61 |
62 | • DO NOT guess keys - always use verified keys from either:
63 | - Previous query results
64 | - The observability_keys response
65 |
66 | • PREFERRED KEYS (faster & always available):
67 | - $metadata.service: Worker service name
68 | - $metadata.origin: Trigger type (e.g., fetch, scheduled, etc.)
69 | - $metadata.trigger: Trigger type (e.g., GET /users, POST /orders, etc.)
70 | - $metadata.message: Log message text (present in nearly all logs)
71 | - $metadata.error: Error message (when applicable)
72 | `),
73 | operation: zQueryOperation,
74 | value: zPrimitiveUnion.optional().describe(`Filter comparison value. IMPORTANT:
75 |
76 | • MUST match actual values in your logs
77 | • VERIFY using either:
78 | - Actual values from previous query results
79 | - The '/values' endpoint with your selected key
80 |
81 | • TYPE MATCHING:
82 | - Ensure value type (string/number/boolean) matches the field type
83 | - String comparisons are case-sensitive unless using specific operations
84 |
85 | • PATTERN USAGE:
86 | - For 'contains', use simple wildcard patterns
87 | - For 'regex', MUST use ClickHouse regex syntax:
88 | - Uses RE2 syntax (not PCRE/JavaScript)
89 | - No lookaheads/lookbehinds
90 | - Examples: '^5\\d{2}$' for HTTP 5xx codes, '\\bERROR\\b' for word boundary
91 | - Escape backslashes with double backslash`),
92 | type: z.enum(['string', 'number', 'boolean']),
93 | }).describe(`
94 | ## Filtering Best Practices
95 | - Before applying filters, use the observability_keys and observability_values queries to confirm available filter fields and values.
96 | - If the query is asking to find something you should check that it exists. I.e. to requests with errors filter for $metadata.error exists.
97 | `)
98 |
99 | export const zQueryCalculation = z.object({
100 | key: z.string().optional()
101 | .describe(`The key to use for the calculation. This key must exist in the logs.
102 | Use the Keys endpoint to confirm that this key exists
103 |
104 | • DO NOT guess keys - always use verified keys from either:
105 | - Previous query results
106 | - The observability_keys response`),
107 | keyType: z.enum(['string', 'number', 'boolean']).optional(),
108 | operator: zQueryOperator,
109 | alias: z.string().optional(),
110 | })
111 | export const zQueryGroupBy = z.object({
112 | type: z.enum(['string', 'number', 'boolean']),
113 | value: z.string(),
114 | })
115 |
116 | export const zSearchNeedle = z.object({
117 | value: zPrimitiveUnion,
118 | isRegex: z.boolean().optional(),
119 | matchCase: z.boolean().optional(),
120 | })
121 |
122 | const zViews = z.enum(['events', 'calculations', 'invocations'])
123 |
124 | export const zAggregateResult = z.object({
125 | groups: z.array(z.object({ key: z.string(), value: zPrimitiveUnion })).optional(),
126 | value: z.number(),
127 | count: z.number(),
128 | interval: z.number(),
129 | sampleInterval: z.number(),
130 | })
131 |
132 | export const zQueryRunCalculationsV2 = z.array(
133 | z.object({
134 | alias: z
135 | .string()
136 | .transform((val) => (val === '' ? undefined : val))
137 | .optional(),
138 | calculation: z.string(),
139 | aggregates: z.array(zAggregateResult),
140 | series: z.array(
141 | z.object({
142 | time: z.string(),
143 | data: z.array(zAggregateResult),
144 | })
145 | ),
146 | })
147 | )
148 |
149 | export const zStatistics = z.object({
150 | elapsed: z.number(),
151 | rows_read: z.number(),
152 | bytes_read: z.number(),
153 | })
154 |
155 | export const zTimeframeAbsolute = z
156 | .object({
157 | to: z.string(),
158 | from: z.string(),
159 | })
160 | .describe(
161 | `An absolute timeframe for your query (ISO-8601 format).
162 |
163 | • Current server time: ${nowISO()}
164 | • Default: Last hour from current time
165 | • Maximum range: Last 7 days
166 | • Format: "YYYY-MM-DDTHH:MM:SSZ" (e.g., "2025-04-29T14:30:00Z")
167 |
168 | Examples:
169 | - Between April 1st and 5th: from="2025-04-01T00:00:00Z", to="2025-04-05T23:59:59Z"
170 |
171 | Note: Narrower timeframes provide faster responses and more specific results.
172 | Omit this parameter entirely to use the default (last hour).`
173 | )
174 |
175 | export const zTimeframeRelative = z
176 | .object({
177 | reference: z.string(),
178 | offset: z.string(),
179 | })
180 | .describe(
181 | `Relative timeframe for your query, composed of a reference time and an offset.
182 |
183 | • Current server time: ${nowISO()}
184 | • Default: Last hour from current time
185 | • Maximum range: Last 7 days
186 | • Reference time format: "YYYY-MM-DDTHH:MM:SSZ" (ISO-8601) (e.g., "2025-04-29T14:30:00Z")
187 | • Offset format: Must start with a '+' or '-' sign, which indicates whether the offset is in the past or future, followed by one or more time units (e.g., '+5d', '-2h', '+6h20m').
188 | Units: s (seconds), m (minutes), h (hours), d (days), w (weeks).
189 | • You should not use a future looking offset in combination with the current server time as the reference time, as this will yield no results. (e.g. "the next 20 minutes")
190 |
191 | Examples:
192 | - Last 30 minutes: reference="${nowISO()}", offset="-30m"
193 | - Yesterday: reference="${nowISO()}", offset="-1d"
194 |
195 | Note: Narrower timeframes provide faster responses and more specific results.
196 | Omit this parameter entirely to use the default (last hour).`
197 | )
198 | .transform((val) => {
199 | const referenceTime = new Date(val.reference).getTime() / 1000
200 |
201 | if (isNaN(referenceTime)) {
202 | throw new Error(`Invalid reference time: ${val.reference}`)
203 | }
204 |
205 | const offsetSeconds = parseRelativeTime(val.offset)
206 |
207 | const from = new Date(Math.min(referenceTime + offsetSeconds, referenceTime) * 1000)
208 | const to = new Date(Math.max(referenceTime + offsetSeconds, referenceTime) * 1000)
209 |
210 | return {
211 | from: from.toISOString(),
212 | to: to.toISOString(),
213 | }
214 | })
215 |
216 | export const zTimeframe = z.union([zTimeframeAbsolute, zTimeframeRelative]).describe(
217 | `Timeframe for your query, which can be either absolute or relative.
218 |
219 | • Absolute timeframe: Specify exact start and end times in ISO-8601 format (e.g., "2025-04-29T14:30:00Z").
220 | • Relative timeframe: Specify a reference time and an offset (e.g., reference="2025-04-29T14:30:00Z", offset="-30m").
221 |
222 | Examples:
223 | - Absolute: from="2025-04-01T00:00:00Z", to="2025-04-05T23:59:59Z"
224 | - Relative: reference="2025-04-29T14:30:00Z", offset="-30m"
225 |
226 | Note: Narrower timeframes provide faster responses and more specific results.`
227 | )
228 |
229 | const zCloudflareMiniEventDetailsRequest = z.object({
230 | url: z.string().optional(),
231 | method: z.string().optional(),
232 | path: z.string().optional(),
233 | search: z.record(z.any()).optional(),
234 | })
235 |
236 | const zCloudflareMiniEventDetailsResponse = z.object({
237 | status: z.number().optional(),
238 | })
239 |
240 | const zCloudflareMiniEventDetails = z.object({
241 | request: zCloudflareMiniEventDetailsRequest.optional(),
242 | response: zCloudflareMiniEventDetailsResponse.optional(),
243 | rpcMethod: z.string().optional(),
244 | rayId: z.string().optional(),
245 | executionModel: z.string().optional(),
246 | })
247 |
248 | export const zCloudflareMiniEvent = z.object({
249 | event: zCloudflareMiniEventDetails,
250 | scriptName: z.string(),
251 | outcome: z.string(),
252 | eventType: z.enum([
253 | 'fetch',
254 | 'scheduled',
255 | 'alarm',
256 | 'cron',
257 | 'queue',
258 | 'email',
259 | 'tail',
260 | 'rpc',
261 | 'websocket',
262 | 'unknown',
263 | ]),
264 | entrypoint: z.string().optional(),
265 | scriptVersion: z
266 | .object({
267 | id: z.string().optional(),
268 | tag: z.string().optional(),
269 | message: z.string().optional(),
270 | })
271 | .optional(),
272 | truncated: z.boolean().optional(),
273 | executionModel: z.enum(['durableObject', 'stateless']).optional(),
274 | requestId: z.string(),
275 | cpuTimeMs: z.number().optional(),
276 | wallTimeMs: z.number().optional(),
277 | })
278 |
279 | export const zCloudflareEvent = zCloudflareMiniEvent.extend({
280 | diagnosticsChannelEvents: z
281 | .array(
282 | z.object({
283 | timestamp: z.number(),
284 | channel: z.string(),
285 | message: z.string(),
286 | })
287 | )
288 | .optional(),
289 | dispatchNamespace: z.string().optional(),
290 | wallTimeMs: z.number(),
291 | cpuTimeMs: z.number(),
292 | })
293 |
294 | const zSourceSchema = z.object({
295 | exception: z
296 | .object({
297 | stack: z.string().optional(),
298 | name: z.string().optional(),
299 | message: z.string().optional(),
300 | timestamp: z.number().optional(),
301 | })
302 | .optional(),
303 | })
304 |
305 | export const zReturnedTelemetryEvent = z.object({
306 | dataset: z.string(),
307 | timestamp: z.number().int().positive(),
308 | source: z.union([z.string(), zSourceSchema]),
309 | $workers: z.union([zCloudflareMiniEvent, zCloudflareEvent]).optional(),
310 | $metadata: z.object({
311 | id: z.string(),
312 | requestId: z.string().optional(),
313 | traceId: z.string().optional(),
314 | spanId: z.string().optional(),
315 | trigger: z.string().optional(),
316 | parentSpanId: z.string().optional(),
317 | service: z.string().optional(),
318 | level: z.string().optional(),
319 | duration: z.number().positive().int().optional(),
320 | statusCode: z.number().positive().int().optional(),
321 | traceDuration: z.number().positive().int().optional(),
322 | error: z.string().optional(),
323 | message: z.string().optional(),
324 | spanName: z.string().optional(),
325 | url: z.string().optional(),
326 | region: z.string().optional(),
327 | account: z.string().optional(),
328 | provider: z.string().optional(),
329 | type: z.string().optional(),
330 | fingerprint: z.string().optional(),
331 | origin: z.string().optional(),
332 | metricName: z.string().optional(),
333 | stackId: z.string().optional(),
334 | coldStart: z.number().positive().int().optional(),
335 | cost: z.number().positive().int().optional(),
336 | cloudService: z.string().optional(),
337 | messageTemplate: z.string().optional(),
338 | errorTemplate: z.string().optional(),
339 | }),
340 | })
341 |
342 | export type zReturnedQueryRunEvents = z.infer<typeof zReturnedQueryRunEvents>
343 | export const zReturnedQueryRunEvents = z.object({
344 | events: z.array(zReturnedTelemetryEvent).optional(),
345 | fields: z
346 | .array(
347 | z.object({
348 | key: z.string(),
349 | type: z.string(),
350 | })
351 | )
352 | .optional(),
353 | count: z.number().optional(),
354 | })
355 |
356 | /**
357 | * The request to run a query
358 | */
359 | export const zQueryRunRequest = z.object({
360 | // TODO: Fix these types
361 | queryId: z.string(),
362 | view: zViews.optional().default('calculations').describe(`## Examples by View Type
363 | ### Events View
364 | - "Show me all errors for the worker api-proxy in the last 30 minutes"
365 | - "Show events from worker auth-service where the path contains /login"
366 |
367 | ### Calculation View
368 | - "What is the p99 of wall time for worker api-proxy?"
369 | - "What's the count of requests by status code for worker cdn-router?"
370 |
371 | ### Invocation View
372 | - "Find a request to worker api-proxy that resulted in a 500 error"
373 | - "List successful requests for the image-resizer worker with status code 200"
374 | `),
375 | parameters: z.object({
376 | datasets: z
377 | .array(z.string())
378 | .optional()
379 | .describe('Leave this empty to use the default datasets'),
380 | filters: z.array(zQueryFilter).optional(),
381 | filterCombination: zFilterCombination.optional(),
382 | calculations: z.array(zQueryCalculation).optional(),
383 | groupBys: z.array(zQueryGroupBy).optional().describe(`Only valid when doing a Calculation`),
384 | orderBy: z
385 | .object({
386 | value: z.string().describe('This must be the alias of a calculation'),
387 | order: z.enum(['asc', 'desc']).optional(),
388 | })
389 | .optional()
390 | .describe('Order By only workers when a group by is present'),
391 | limit: z
392 | .number()
393 | .int()
394 | .nonnegative()
395 | .max(100)
396 | .optional()
397 | .describe(
398 | 'Use this limit when view is calculation and a group by is present. 10 is a sensible default'
399 | ),
400 | needle: zSearchNeedle.optional(),
401 | }),
402 | timeframe: zTimeframe,
403 | granularity: z
404 | .number()
405 | .optional()
406 | .describe(
407 | 'This is only used when the view is calculations - by leaving it empty workers observability will detect the correct granularity'
408 | ),
409 | limit: z
410 | .number()
411 | .max(100)
412 | .optional()
413 | .default(5)
414 | .describe(
415 | 'Use this limit to limit the number of events returned when the view is events. 5 is a sensible default'
416 | ),
417 |
418 | dry: z.boolean().optional().default(true),
419 | offset: z
420 | .string()
421 | .optional()
422 | .describe(
423 | 'The offset to use for pagination. Use the $metadata.id field to get the next offset.'
424 | ),
425 | offsetBy: z.number().optional(),
426 | offsetDirection: z
427 | .string()
428 | .optional()
429 | .describe('The direction to use for pagination. Use "next" or "prev".'),
430 | })
431 |
432 | /**
433 | * The response from the API
434 | */
435 | export type ReturnedQueryRunResult = z.infer<typeof zReturnedQueryRunResult>
436 | export const zReturnedQueryRunResult = z.object({
437 | // run: zQueryRunRequest,
438 | calculations: zQueryRunCalculationsV2.optional(),
439 | compare: zQueryRunCalculationsV2.optional(),
440 | events: zReturnedQueryRunEvents.optional(),
441 | invocations: z.record(z.string(), z.array(zReturnedTelemetryEvent)).optional(),
442 | statistics: zStatistics,
443 | })
444 |
445 | /**
446 | * Keys Request
447 | */
448 | export const zKeysRequest = z.object({
449 | timeframe: zTimeframe,
450 | datasets: z
451 | .array(z.string())
452 | .default([])
453 | .describe('Leave this empty to use the default datasets'),
454 | filters: z.array(zQueryFilter).default([]),
455 | limit: z.number().optional().describe(`
456 | • ADVANCED USAGE:
457 | set limit=1000+ to retrieve comprehensive key options without needing additional filtering`),
458 | needle: zSearchNeedle.optional(),
459 | keyNeedle: zSearchNeedle.optional()
460 | .describe(`If the user makes a suggestion for a key, use this to narrow down the list of keys returned.
461 | Make sure match case is fals to avoid case sensitivity issues.`),
462 | })
463 |
464 | /**
465 | * Keys Response
466 | */
467 | export type zKeysResponse = z.infer<typeof zKeysResponse>
468 | export const zKeysResponse = z.array(
469 | z.object({
470 | key: z.string(),
471 | type: z.enum(['string', 'boolean', 'number']),
472 | lastSeenAt: z.number(),
473 | })
474 | )
475 |
476 | /**
477 | * Values Request
478 | */
479 | export const zValuesRequest = z.object({
480 | timeframe: zTimeframe,
481 | key: z.string(),
482 | type: z.enum(['string', 'boolean', 'number']),
483 | datasets: z
484 | .array(z.string())
485 | .default([])
486 | .describe('Leave this empty to use the default datasets'),
487 | filters: z.array(zQueryFilter).default([]),
488 | limit: z.number().default(50),
489 | needle: zSearchNeedle.optional(),
490 | })
491 |
492 | /** Values Response */
493 | export const zValuesResponse = z.array(
494 | z.object({
495 | key: z.string(),
496 | type: z.enum(['string', 'boolean', 'number']),
497 | value: z.union([z.string(), z.number(), z.boolean()]),
498 | dataset: z.string(),
499 | })
500 | )
501 |
```
--------------------------------------------------------------------------------
/packages/mcp-common/src/types/r2_bucket.types.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * This file contains the validators for the r2 bucket tools.
3 | */
4 | import { z } from 'zod'
5 |
6 | import type {
7 | CORSDeleteParams,
8 | CORSUpdateParams,
9 | EventNotificationDeleteParams,
10 | EventNotificationGetParams,
11 | EventNotificationUpdateParams,
12 | LockGetParams,
13 | LockUpdateParams,
14 | SippyGetParams,
15 | SippyUpdateParams,
16 | } from 'cloudflare/resources/r2/buckets.mjs'
17 | import type {
18 | CustomCreateParams,
19 | CustomDeleteParams,
20 | CustomListParams,
21 | CustomUpdateParams,
22 | } from 'cloudflare/resources/r2/buckets/domains.mjs'
23 | import type {
24 | ProviderParam,
25 | Sippy,
26 | SippyDeleteParams,
27 | } from 'cloudflare/resources/r2/buckets/sippy.mjs'
28 | import type { CustomGetParams } from 'cloudflare/resources/zero-trust/devices/policies.mjs'
29 | import type {
30 | BucketCreateParams,
31 | TemporaryCredentialCreateParams,
32 | } from 'cloudflare/src/resources/r2.js'
33 | import type { CORSGetParams } from 'cloudflare/src/resources/r2/buckets.js'
34 | import type { CustomGetResponse } from 'cloudflare/src/resources/r2/buckets/domains.js'
35 |
36 | export const BucketNameSchema: z.ZodType<BucketCreateParams['name']> = z
37 | .string()
38 | .describe('The name of the r2 bucket')
39 |
40 | export const BucketListCursorParam = z
41 | .string()
42 | .nullable()
43 | .optional()
44 | .describe(
45 | 'Query param: Pagination cursor received during the last List Buckets call. R2 buckets are paginated using cursors instead of page numbers.'
46 | )
47 | export const BucketListDirectionParam = z
48 | .enum(['asc', 'desc'])
49 | .nullable()
50 | .optional()
51 | .describe('Direction to order buckets')
52 | export const BucketListNameContainsParam = z
53 | .string()
54 | .nullable()
55 | .optional()
56 | .describe(
57 | 'Bucket names to filter by. Only buckets with this phrase in their name will be returned.'
58 | )
59 | export const BucketListStartAfterParam = z
60 | .string()
61 | .nullable()
62 | .optional()
63 | .describe('Bucket name to start searching after. Buckets are ordered lexicographically.')
64 |
65 | export const AllowedMethodsEnum: z.ZodType<CORSUpdateParams.Rule['allowed']['methods']> = z.array(
66 | z.union([
67 | z.literal('GET'),
68 | z.literal('PUT'),
69 | z.literal('POST'),
70 | z.literal('DELETE'),
71 | z.literal('HEAD'),
72 | ])
73 | )
74 | export const JurisdictionEnum: z.ZodType<CORSUpdateParams['jurisdiction']> = z
75 | .enum(['default', 'eu', 'fedramp'])
76 | .describe(
77 | 'Use Jurisdictional Restrictions when you need to ensure data is stored and processed within a jurisdiction to meet data residency requirements, including local regulations such as the GDPR or FedRAMP.'
78 | )
79 |
80 | // CORS ZOD SCHEMAS
81 | export const CorsAllowedSchema: z.ZodType<CORSUpdateParams.Rule['allowed']> = z
82 | .object({
83 | methods: AllowedMethodsEnum.describe(
84 | 'Specifies the value for the Access-Control-Allow-Methods header'
85 | ),
86 | origins: z
87 | .array(z.string())
88 | .describe('Specifies the value for the Access-Control-Allow-Origin header'),
89 | headers: z
90 | .array(z.string())
91 | .optional()
92 | .describe('Specifies the value for the Access-Control-Allow-Headers header'),
93 | })
94 | .describe('Object specifying allowed origins, methods and headers for this CORS rule')
95 |
96 | export const CorsRuleSchema: z.ZodType<CORSUpdateParams.Rule> = z
97 | .object({
98 | allowed: CorsAllowedSchema,
99 | id: z.string().optional().describe('Identifier for this rule'),
100 | exposeHeaders: z
101 | .array(z.string())
102 | .optional()
103 | .describe('Headers that can be exposed back and accessed by JavaScript'),
104 | maxAgeSeconds: z
105 | .number()
106 | .optional()
107 | .describe('Time in seconds browsers can cache CORS preflight responses (max 86400)'),
108 | })
109 | .describe('Object specifying allowed origins, methods and headers for this CORS rule')
110 |
111 | export const CorsRulesSchema: z.ZodType<Omit<CORSUpdateParams, 'account_id'>> = z
112 | .object({
113 | rules: z.array(CorsRuleSchema).optional().describe('Array of CORS rules for the bucket'),
114 | jurisdiction: JurisdictionEnum.optional(),
115 | })
116 | .describe('CORS configuration for the bucket')
117 |
118 | export const CorsGetParamsSchema: z.ZodType<Omit<CORSGetParams, 'account_id'>> = z
119 | .object({
120 | jurisdiction: JurisdictionEnum.optional(),
121 | })
122 | .describe('Params for getting CORS configuration for an R2 bucket')
123 |
124 | export const CorsDeleteParamsSchema: z.ZodType<Omit<CORSDeleteParams, 'account_id'>> = z
125 | .object({
126 | jurisdiction: JurisdictionEnum.optional(),
127 | })
128 | .describe('Params for deleting CORS configuration for an R2 bucket')
129 |
130 | // TEMPORARY CREDENTIALS ZOD SCHEMAS
131 | export const TemporaryCredentialsCreateParamsSchema: z.ZodType<
132 | Omit<TemporaryCredentialCreateParams, 'account_id'>
133 | > = z
134 | .object({
135 | bucket: BucketNameSchema,
136 | ttlSeconds: z.number().describe('The time to live for the temporary credentials'),
137 | permission: z
138 | .enum(['admin-read-write', 'admin-read-only', 'object-read-write', 'object-read-only'])
139 | .describe('The permission for the temporary credentials'),
140 | objects: z
141 | .array(z.string())
142 | .optional()
143 | .describe('The objects to scope the temporary credentials to'),
144 | prefixes: z
145 | .array(z.string())
146 | .optional()
147 | .describe('The prefixes to scope the temporary credentials to'),
148 | parentAccessKeyId: z.string().describe('The parent access key id to use for signing'),
149 | })
150 | .describe('Temporary credentials for the bucket')
151 |
152 | // CUSTOM DOMAIN ZOD SCHEMAS
153 | export const CustomDomainListParamsSchema: z.ZodType<Omit<CustomListParams, 'account_id'>> = z
154 | .object({
155 | jurisdiction: JurisdictionEnum.optional(),
156 | })
157 | .describe('Params for listing custom domains for an R2 bucket')
158 |
159 | export const CustomDomainNameSchema: z.ZodType<CustomGetResponse['domain']> = z
160 | .string()
161 | .describe('The custom domain')
162 |
163 | export const CustomDomainGetParamsSchema: z.ZodType<Omit<CustomGetParams, 'account_id'>> = z
164 | .object({
165 | jurisdiction: JurisdictionEnum.optional(),
166 | })
167 | .describe('Params for getting a custom domain for an R2 bucket')
168 |
169 | export const CustomDomainCreateParamsSchema: z.ZodType<Omit<CustomCreateParams, 'account_id'>> = z
170 | .object({
171 | domain: CustomDomainNameSchema,
172 | enabled: z
173 | .boolean()
174 | .describe(
175 | 'Whether to enable public bucket access at the custom domain. If undefined, the domain will be enabled.'
176 | ),
177 | zoneId: z.string().describe('The zone id of the custom domain'),
178 | minTLS: z
179 | .enum(['1.0', '1.1', '1.2', '1.3'])
180 | .optional()
181 | .describe(
182 | 'The minimum TLS version the custom domain will accept for incoming connections. If not set, defaults to 1.0.'
183 | ),
184 | jurisdiction: JurisdictionEnum.optional(),
185 | })
186 | .describe('Params for creating a custom domain for an R2 bucket')
187 |
188 | export const CustomDomainDeleteParamsSchema: z.ZodType<Omit<CustomDeleteParams, 'account_id'>> = z
189 | .object({
190 | jurisdiction: JurisdictionEnum.optional(),
191 | })
192 | .describe('Params for deleting a custom domain for an R2 bucket')
193 |
194 | export const CustomDomainUpdateParamsSchema: z.ZodType<Omit<CustomUpdateParams, 'account_id'>> = z
195 | .object({
196 | enabled: z
197 | .boolean()
198 | .describe(
199 | 'Whether to enable public bucket access at the custom domain. If undefined, the domain will be enabled.'
200 | ),
201 | zoneId: z.string().describe('The zone id of the custom domain'),
202 | minTLS: z
203 | .enum(['1.0', '1.1', '1.2', '1.3'])
204 | .optional()
205 | .describe(
206 | 'The minimum TLS version the custom domain will accept for incoming connections. If not set, defaults to 1.0.'
207 | ),
208 | jurisdiction: JurisdictionEnum.optional(),
209 | })
210 | .describe('Params for updating a custom domain for an R2 bucket')
211 |
212 | // EVENT NOTIFICATION ZOD SCHEMAS
213 | export const QueueIdSchema = z.string().describe('The queue id of the event notification')
214 |
215 | const EventActionEnum = z.enum([
216 | 'PutObject',
217 | 'CopyObject',
218 | 'DeleteObject',
219 | 'CompleteMultipartUpload',
220 | 'LifecycleDeletion',
221 | ])
222 |
223 | export const EventNotificationRuleSchema: z.ZodType<EventNotificationUpdateParams.Rule> = z
224 | .object({
225 | actions: z
226 | .array(EventActionEnum)
227 | .describe('Array of R2 object actions that will trigger notifications'),
228 | description: z
229 | .string()
230 | .optional()
231 | .describe(
232 | 'A description that can be used to identify the event notification rule after creation'
233 | ),
234 | prefix: z
235 | .string()
236 | .optional()
237 | .describe('Notifications will be sent only for objects with this prefix'),
238 | suffix: z
239 | .string()
240 | .optional()
241 | .describe('Notifications will be sent only for objects with this suffix'),
242 | })
243 | .describe('Rule configuration for event notifications')
244 |
245 | // Main EventNotificationUpdateParams schema
246 | export const EventNotificationUpdateParamsSchema: z.ZodType<
247 | Omit<EventNotificationUpdateParams, 'account_id'>
248 | > = z
249 | .object({
250 | rules: z
251 | .array(EventNotificationRuleSchema)
252 | .optional()
253 | .describe('Array of rules to drive notifications'),
254 | jurisdiction: JurisdictionEnum.optional(),
255 | })
256 | .describe('Parameters for updating event notification configuration')
257 |
258 | export const EventNotificationGetParamsSchema: z.ZodType<
259 | Omit<EventNotificationGetParams, 'account_id'>
260 | > = z
261 | .object({
262 | jurisdiction: JurisdictionEnum.optional(),
263 | })
264 | .describe('Params for getting event notifications for an R2 bucket')
265 |
266 | export const EventNotificationDeleteParamsSchema: z.ZodType<
267 | Omit<EventNotificationDeleteParams, 'account_id'>
268 | > = z
269 | .object({
270 | jurisdiction: JurisdictionEnum.optional(),
271 | })
272 | .describe('Params for deleting event notifications for an R2 bucket')
273 |
274 | // LOCK ZOD SCHEMAS
275 | export const LockGetParamsSchema: z.ZodType<Omit<LockGetParams, 'account_id'>> = z
276 | .object({
277 | jurisdiction: JurisdictionEnum.optional(),
278 | })
279 | .describe('Params for getting locks for an R2 bucket')
280 |
281 | // Condition: Age-based
282 | const R2LockRuleAgeCondition: z.ZodType<LockUpdateParams.Rule.R2LockRuleAgeCondition> = z
283 | .object({
284 | maxAgeSeconds: z
285 | .number()
286 | .describe('Condition to apply a lock rule to an object for how long in seconds'),
287 | type: z.literal('Age').describe('Age-based condition'),
288 | })
289 | .describe('Condition to apply a lock rule to an object for how long in seconds')
290 |
291 | // Condition: Date-based
292 | const R2LockRuleDateCondition: z.ZodType<LockUpdateParams.Rule.R2LockRuleDateCondition> = z
293 | .object({
294 | date: z.string().describe('Condition to apply a lock rule to an object until a specific date'),
295 | type: z.literal('Date').describe('Date-based condition'),
296 | })
297 | .describe('Condition to apply a lock rule to an object until a specific date')
298 |
299 | // Condition: Indefinite
300 | const R2LockRuleIndefiniteCondition: z.ZodType<LockUpdateParams.Rule.R2LockRuleIndefiniteCondition> =
301 | z
302 | .object({
303 | type: z.literal('Indefinite').describe('Indefinite condition'),
304 | })
305 | .describe('Condition to apply a lock rule indefinitely')
306 |
307 | // Union of all possible condition types
308 | const LockRuleCondition = z
309 | .union([R2LockRuleAgeCondition, R2LockRuleDateCondition, R2LockRuleIndefiniteCondition])
310 | .describe('Condition to apply a lock rule to an object')
311 |
312 | export const LockRuleSchema: z.ZodType<LockUpdateParams.Rule> = z
313 | .object({
314 | id: z.string().describe('Unique identifier for this rule'),
315 | condition: LockRuleCondition,
316 | enabled: z.boolean().describe('Whether or not this rule is in effect'),
317 | prefix: z
318 | .string()
319 | .optional()
320 | .describe(
321 | 'Rule will only apply to objects/uploads in the bucket that start with the given prefix; an empty prefix can be provided to scope rule to all objects/uploads'
322 | ),
323 | })
324 | .describe('Lock rule definition')
325 |
326 | // Main schema
327 | export const LockUpdateParamsSchema: z.ZodType<Omit<LockUpdateParams, 'account_id'>> = z
328 | .object({
329 | rules: z.array(LockRuleSchema).optional().describe('Body param: Optional list of lock rules'),
330 | jurisdiction: JurisdictionEnum.optional(),
331 | })
332 | .describe('Lock update parameters')
333 |
334 | // SIPPY ZOD SCHEMAS
335 | export const SippyGetParamsSchema: z.ZodType<Omit<SippyGetParams, 'account_id'>> = z
336 | .object({
337 | jurisdiction: JurisdictionEnum.optional(),
338 | })
339 | .describe('Params for getting sippy configuration for an R2 bucket')
340 |
341 | const ProviderParam: z.ZodType<ProviderParam> = z.literal('r2').describe('Provider parameter')
342 |
343 | // Common destination schema for reuse
344 | const BaseDestination: z.ZodType<Sippy.Destination> = z.object({
345 | accessKeyId: z
346 | .string()
347 | .optional()
348 | .describe(
349 | 'ID of a Cloudflare API token. This is the value labelled "Access Key ID" when creating an API token from the R2 dashboard. Sippy will use this token when writing objects to R2, so it is best to scope this token to the bucket you\'re enabling Sippy for.'
350 | ),
351 | secretAccessKey: z
352 | .string()
353 | .optional()
354 | .describe(
355 | 'Value of a Cloudflare API token. This is the value labelled "Secret Access Key" when creating an API token from the R2 dashboard. Sippy will use this token when writing objects to R2, so it is best to scope this token to the bucket you\'re enabling Sippy for.'
356 | ),
357 | provider: ProviderParam,
358 | })
359 |
360 | // AWS Source schema
361 | const AwsSourceSchema: z.ZodType<SippyUpdateParams.R2EnableSippyAws.Source> = z
362 | .object({
363 | accessKeyId: z
364 | .string()
365 | .optional()
366 | .describe('Access Key ID of an IAM credential (ideally scoped to a single S3 bucket)'),
367 | secretAccessKey: z
368 | .string()
369 | .optional()
370 | .describe('Secret Access Key of an IAM credential (ideally scoped to a single S3 bucket)'),
371 | bucket: z.string().optional().describe('Name of the AWS S3 bucket'),
372 | region: z.string().optional().describe('Name of the AWS availability zone'),
373 | provider: z.literal('aws').optional().describe('AWS provider indicator'),
374 | })
375 | .describe('AWS S3 bucket to copy objects from')
376 |
377 | // GCS Source schema
378 | const GcsSourceSchema: z.ZodType<SippyUpdateParams.R2EnableSippyGcs.Source> = z
379 | .object({
380 | bucket: z.string().optional().describe('Name of the GCS bucket'),
381 | clientEmail: z
382 | .string()
383 | .optional()
384 | .describe('Client email of an IAM credential (ideally scoped to a single GCS bucket)'),
385 | privateKey: z
386 | .string()
387 | .optional()
388 | .describe('Private Key of an IAM credential (ideally scoped to a single GCS bucket)'),
389 | provider: z.literal('gcs').optional().describe('GCS provider indicator'),
390 | })
391 | .describe('GCS bucket to copy objects from')
392 |
393 | // R2EnableSippyAws schema
394 | const R2EnableSippyAwsSchema: z.ZodType<Omit<SippyUpdateParams.R2EnableSippyAws, 'account_id'>> = z
395 | .object({
396 | destination: BaseDestination.describe('R2 bucket to copy objects to').optional(),
397 | source: AwsSourceSchema.optional(),
398 | jurisdiction: JurisdictionEnum.optional(),
399 | })
400 | .describe('Parameters to enable Sippy with AWS as source')
401 |
402 | // R2EnableSippyGcs schema
403 | const R2EnableSippyGcsSchema: z.ZodType<Omit<SippyUpdateParams.R2EnableSippyGcs, 'account_id'>> = z
404 | .object({
405 | destination: BaseDestination.describe('R2 bucket to copy objects to').optional(),
406 | source: GcsSourceSchema.optional(),
407 | jurisdiction: JurisdictionEnum.optional(),
408 | })
409 | .describe('Parameters to enable Sippy with GCS as source')
410 |
411 | // Combined SippyUpdateParams namespace schema
412 | export const SippyUpdateParamsSchema = z.union([R2EnableSippyAwsSchema, R2EnableSippyGcsSchema])
413 |
414 | export const SippyDeleteParamsSchema: z.ZodType<Omit<SippyDeleteParams, 'account_id'>> = z
415 | .object({
416 | jurisdiction: JurisdictionEnum.optional(),
417 | })
418 | .describe('Parameters to delete Sippy for an R2 bucket')
419 |
```
--------------------------------------------------------------------------------
/apps/demo-day/frontend/index.html:
--------------------------------------------------------------------------------
```html
1 | <!doctype html>
2 | <html lang="en">
3 | <head>
4 | <meta charset="UTF-8" />
5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 |
7 | <!-- Primary Meta Tags -->
8 | <title>MCP Demo Day - Preview the Future of Agentic Software</title>
9 | <meta
10 | name="description"
11 | content="Join us on May 1st, 2025 to witness groundbreaking demos from companies like Atlassian, Linear, Stripe, and more. Experience the next evolution of AI-powered software."
12 | />
13 |
14 | <!-- Open Graph / Facebook -->
15 | <meta property="og:type" content="website" />
16 | <meta property="og:url" content="https://demo-day.mcp.cloudflare.com/" />
17 | <meta property="og:title" content="MCP Demo Day - Preview the Future of Agentic Software" />
18 | <meta
19 | property="og:description"
20 | content="Join us on May 1st, 2025 to witness groundbreaking demos from companies like Atlassian, Linear, Stripe, and more. Experience the next evolution of AI-powered software."
21 | />
22 | <meta property="og:image" content="https://demo-day.mcp.cloudflare.com/public/mcpog.png" />
23 |
24 | <!-- Twitter -->
25 | <meta name="twitter:card" content="summary_large_image" />
26 | <meta name="twitter:title" content="MCP Demo Day - Preview the Future of Agentic Software" />
27 | <meta
28 | name="twitter:description"
29 | content="Join us on May 1st, 2025 to witness groundbreaking demos from companies like Atlassian, Linear, Stripe, and more. Experience the next evolution of AI-powered software."
30 | />
31 | <meta name="twitter:image" content="https://demo-day.mcp.cloudflare.com/public/mcpog.png" />
32 | <link rel="icon" type="image/png" href="public/favicon.ico" />
33 | <link rel="icon" type="image/png" sizes="32x32" href="public/favicon-32x32.png" />
34 | <link rel="icon" type="image/png" sizes="16x16" href="public/favicon-16x16.png" />
35 |
36 | <link rel="stylesheet" href="styles.css" />
37 | <link rel="preconnect" href="https://fonts.googleapis.com" />
38 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
39 | <link
40 | href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
41 | rel="stylesheet"
42 | />
43 | </head>
44 |
45 | <body>
46 | <div class="page-wrapper">
47 | <main class="container">
48 | <section class="left-panel">
49 | <div class="header">
50 | <div class="logo">
51 | <img src="public/mcp_demo_day.svg" alt="MCP Logo" class="cloud-logo" />
52 | </div>
53 | <div class="date-time" onclick="document.getElementById('calendarDialog').showModal()">
54 | <button class="calendar-trigger" aria-label="Add to calendar">
55 | <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
56 | <path
57 | d="M6 5V3M14 5V3M5 9H15M5 7.5H15M4.2 17H15.8C16.4627 17 17 16.4627 17 15.8V5.2C17 4.53726 16.4627 4 15.8 4H4.2C3.53726 4 3 4.53726 3 5.2V15.8C3 16.4627 3.53726 17 4.2 17Z"
58 | stroke="currentColor"
59 | stroke-width="1.5"
60 | stroke-linecap="round"
61 | stroke-linejoin="round"
62 | />
63 | </svg>
64 | </button>
65 | <div class="date-time-text">
66 | <h2>MAY 1, 2025</h2>
67 | <h3>ONLINE 10:00 AM PT / 1 PM ET</h3>
68 | </div>
69 | </div>
70 | </div>
71 |
72 | <div class="content">
73 | <h1>Preview the Future<br />of Agentic Software</h1>
74 |
75 | <p class="description">
76 | Come see how the world's most innovative platforms have connected agents to their
77 | services with MCP to build a new class of product experiences.
78 | </p>
79 |
80 | <div class="input-group">
81 | <a class="notify-btn" href="https://www.youtube.com/watch?v=njBGqr-BU54">
82 | Watch the stream
83 | <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
84 | <path
85 | d="M2.5 8H13.5M13.5 8L9 3.5M13.5 8L9 12.5"
86 | stroke="currentColor"
87 | stroke-width="1.5"
88 | stroke-linecap="round"
89 | stroke-linejoin="round"
90 | />
91 | </svg>
92 | </a>
93 | <div class="success-message">
94 | <div class="success-text">
95 | <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
96 | <path
97 | d="M15 5L7 13L3 9"
98 | stroke="currentColor"
99 | stroke-width="2"
100 | stroke-linecap="round"
101 | stroke-linejoin="round"
102 | />
103 | </svg>
104 | You're in! See you on May 1st
105 | </div>
106 | <div class="calendar-actions">
107 | <button class="calendar-action" data-calendar-type="google">
108 | <svg
109 | xmlns="http://www.w3.org/2000/svg"
110 | x="0px"
111 | y="0px"
112 | width="100"
113 | height="100"
114 | viewBox="0,0,256,256"
115 | style="fill: #ffffff"
116 | >
117 | <g
118 | fill="#ffffff"
119 | fill-rule="nonzero"
120 | stroke="none"
121 | stroke-width="1"
122 | stroke-linecap="butt"
123 | stroke-linejoin="miter"
124 | stroke-miterlimit="10"
125 | stroke-dasharray=""
126 | stroke-dashoffset="0"
127 | font-family="none"
128 | font-weight="none"
129 | font-size="none"
130 | text-anchor="none"
131 | style="mix-blend-mode: normal"
132 | >
133 | <g transform="scale(5.12,5.12)">
134 | <path
135 | d="M25.99609,48c-12.68359,0 -23.00391,-10.31641 -23.00391,-23c0,-12.68359 10.32031,-23 23.00391,-23c5.74609,0 11.24609,2.12891 15.49219,5.99609l0.77344,0.70703l-7.58594,7.58594l-0.70312,-0.60156c-2.22656,-1.90625 -5.05859,-2.95703 -7.97656,-2.95703c-6.76562,0 -12.27344,5.50391 -12.27344,12.26953c0,6.76563 5.50781,12.26953 12.27344,12.26953c4.87891,0 8.73438,-2.49219 10.55078,-6.73828h-11.55078v-10.35547l22.55078,0.03125l0.16797,0.79297c1.17578,5.58203 0.23438,13.79297 -4.53125,19.66797c-3.94531,4.86328 -9.72656,7.33203 -17.1875,7.33203z"
136 | ></path>
137 | </g>
138 | </g>
139 | </svg>
140 | Google Calendar
141 | </button>
142 | <button class="calendar-action" data-calendar-type="outlook">
143 | <svg
144 | xmlns="http://www.w3.org/2000/svg"
145 | x="0px"
146 | y="0px"
147 | width="100"
148 | height="100"
149 | viewBox="0,0,256,256"
150 | style="fill: #ffffff"
151 | >
152 | <g
153 | fill="#ffffff"
154 | fill-rule="nonzero"
155 | stroke="none"
156 | stroke-width="1"
157 | stroke-linecap="butt"
158 | stroke-linejoin="miter"
159 | stroke-miterlimit="10"
160 | stroke-dasharray=""
161 | stroke-dashoffset="0"
162 | font-family="none"
163 | font-weight="none"
164 | font-size="none"
165 | text-anchor="none"
166 | style="mix-blend-mode: normal"
167 | >
168 | <g transform="scale(5.12,5.12)">
169 | <path
170 | d="M5,4c-0.552,0 -1,0.447 -1,1v19h20v-20zM26,4v20h20v-19c0,-0.553 -0.448,-1 -1,-1zM4,26v19c0,0.553 0.448,1 1,1h19v-20zM26,26v20h19c0.552,0 1,-0.447 1,-1v-19z"
171 | ></path>
172 | </g>
173 | </g>
174 | </svg>
175 | Outlook
176 | </button>
177 | <button class="calendar-action" data-calendar-type="ics">
178 | <svg
179 | width="20"
180 | height="20"
181 | viewBox="0 0 24 24"
182 | fill="none"
183 | stroke="currentColor"
184 | >
185 | <path
186 | d="M12 15V3m0 12l-4-4m4 4l4-4M3 17l.6 2.6c.2.8.8 1.4 1.6 1.4h13.6c.8 0 1.4-.6 1.6-1.4l.6-2.6"
187 | stroke-width="2"
188 | stroke-linecap="round"
189 | stroke-linejoin="round"
190 | />
191 | </svg>
192 | Download .ics
193 | </button>
194 | </div>
195 | </div>
196 | </div>
197 | <div class="attendees">
198 | <div class="attendee-avatars">
199 | <div class="attendee-avatar" data-tooltip="Special Guest">
200 | <img src="public/special_guest.png" alt="Special Guest" />
201 | </div>
202 | <div class="attendee-avatar" data-tooltip="Sunil Pai">
203 | <img src="public/sunil.jpg" alt="Sunil Pai" />
204 | </div>
205 | <div class="attendee-avatar" data-tooltip="Dina Kozlov">
206 | <img src="public/dina.jpg" alt="Dina Kozlov" />
207 | </div>
208 | </div>
209 | <span class="attendee-count"><strong>+ all the other cool kids</strong> went</span>
210 | </div>
211 | </div>
212 | </section>
213 |
214 | <section class="right-panel">
215 | <div class="demos-section">
216 | <h4>DEMOS FROM</h4>
217 | <ul class="demo-companies">
218 | <li>Asana</li>
219 | <li>Atlassian</li>
220 | <li>Cloudflare</li>
221 | <li>Intercom</li>
222 | <li>Linear</li>
223 | <li>Paypal</li>
224 | <li>Sentry</li>
225 | <li>Square</li>
226 | <li>Stripe</li>
227 | <li>Webflow</li>
228 | <li>+ More</li>
229 | </ul>
230 | </div>
231 | </section>
232 | </main>
233 |
234 | <footer>
235 | <div class="footer-left">
236 | <img src="public/cloudflare_logo.svg" alt="Cloudflare" class="cloudflare-logo" />
237 | <div class="footer-links"></div>
238 | </div>
239 | <a
240 | href="https://developers.cloudflare.com/agents/guides/remote-mcp-server/?utm_source=website&utm_medium=reg+page&utm_campaign=MCP+Demo+Day"
241 | target="_blank"
242 | class="build-btn"
243 | >
244 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="24" height="24">
245 | <path d="M8.16 23h21.177v-5.86l-4.023-2.307-.694-.3-16.46.113z" fill="transparent" />
246 | <path
247 | d="M22.012 22.222c.197-.675.122-1.294-.206-1.754-.3-.422-.807-.666-1.416-.694l-11.545-.15c-.075 0-.14-.038-.178-.094s-.047-.13-.028-.206c.038-.113.15-.197.272-.206l11.648-.15c1.38-.066 2.88-1.182 3.404-2.55l.666-1.735a.38.38 0 0 0 .02-.225c-.75-3.395-3.78-5.927-7.4-5.927-3.34 0-6.17 2.157-7.184 5.15-.657-.488-1.5-.75-2.392-.666-1.604.16-2.9 1.444-3.048 3.048a3.58 3.58 0 0 0 .084 1.191A4.84 4.84 0 0 0 0 22.1c0 .234.02.47.047.703.02.113.113.197.225.197H21.58a.29.29 0 0 0 .272-.206l.16-.572z"
248 | fill="currentColor"
249 | />
250 | <path
251 | d="M25.688 14.803l-.32.01c-.075 0-.14.056-.17.13l-.45 1.566c-.197.675-.122 1.294.206 1.754.3.422.807.666 1.416.694l2.457.15c.075 0 .14.038.178.094s.047.14.028.206c-.038.113-.15.197-.272.206l-2.56.15c-1.388.066-2.88 1.182-3.404 2.55l-.188.478c-.038.094.028.188.13.188h8.797a.23.23 0 0 0 .225-.169A6.41 6.41 0 0 0 32 21.106a6.32 6.32 0 0 0-6.312-6.302"
252 | fill="currentColor"
253 | />
254 | </svg>
255 | Build An MCP Server
256 | </a>
257 | </footer>
258 | </div>
259 | <dialog id="calendarDialog" class="calendar-popover">
260 | <h4>Add to Calendar</h4>
261 | <button class="close-button" onclick="document.getElementById('calendarDialog').close()">
262 | <svg
263 | width="16"
264 | height="16"
265 | viewBox="0 0 24 24"
266 | fill="none"
267 | stroke="currentColor"
268 | stroke-width="1"
269 | >
270 | <path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" />
271 | </svg>
272 | </button>
273 | <div class="calendar-options">
274 | <button class="calendar-option" data-calendar-type="google">
275 | <svg
276 | xmlns="http://www.w3.org/2000/svg"
277 | x="0px"
278 | y="0px"
279 | width="100"
280 | height="100"
281 | viewBox="0,0,256,256"
282 | style="fill: #ffffff"
283 | >
284 | <g
285 | fill="#ffffff"
286 | fill-rule="nonzero"
287 | stroke="none"
288 | stroke-width="1"
289 | stroke-linecap="butt"
290 | stroke-linejoin="miter"
291 | stroke-miterlimit="10"
292 | stroke-dasharray=""
293 | stroke-dashoffset="0"
294 | font-family="none"
295 | font-weight="none"
296 | font-size="none"
297 | text-anchor="none"
298 | style="mix-blend-mode: normal"
299 | >
300 | <g transform="scale(5.12,5.12)">
301 | <path
302 | d="M25.99609,48c-12.68359,0 -23.00391,-10.31641 -23.00391,-23c0,-12.68359 10.32031,-23 23.00391,-23c5.74609,0 11.24609,2.12891 15.49219,5.99609l0.77344,0.70703l-7.58594,7.58594l-0.70312,-0.60156c-2.22656,-1.90625 -5.05859,-2.95703 -7.97656,-2.95703c-6.76562,0 -12.27344,5.50391 -12.27344,12.26953c0,6.76563 5.50781,12.26953 12.27344,12.26953c4.87891,0 8.73438,-2.49219 10.55078,-6.73828h-11.55078v-10.35547l22.55078,0.03125l0.16797,0.79297c1.17578,5.58203 0.23438,13.79297 -4.53125,19.66797c-3.94531,4.86328 -9.72656,7.33203 -17.1875,7.33203z"
303 | ></path>
304 | </g>
305 | </g>
306 | </svg>
307 | <span>Google Calendar</span>
308 | </button>
309 | <button class="calendar-option" data-calendar-type="outlook">
310 | <svg
311 | xmlns="http://www.w3.org/2000/svg"
312 | x="0px"
313 | y="0px"
314 | width="100"
315 | height="100"
316 | viewBox="0,0,256,256"
317 | style="fill: #ffffff"
318 | >
319 | <g
320 | fill="#ffffff"
321 | fill-rule="nonzero"
322 | stroke="none"
323 | stroke-width="1"
324 | stroke-linecap="butt"
325 | stroke-linejoin="miter"
326 | stroke-miterlimit="10"
327 | stroke-dasharray=""
328 | stroke-dashoffset="0"
329 | font-family="none"
330 | font-weight="none"
331 | font-size="none"
332 | text-anchor="none"
333 | style="mix-blend-mode: normal"
334 | >
335 | <g transform="scale(5.12,5.12)">
336 | <path
337 | d="M5,4c-0.552,0 -1,0.447 -1,1v19h20v-20zM26,4v20h20v-19c0,-0.553 -0.448,-1 -1,-1zM4,26v19c0,0.553 0.448,1 1,1h19v-20zM26,26v20h19c0.552,0 1,-0.447 1,-1v-19z"
338 | ></path>
339 | </g>
340 | </g>
341 | </svg>
342 | <span>Outlook Calendar</span>
343 | </button>
344 | <button class="calendar-option" data-calendar-type="apple">
345 | <svg
346 | xmlns="http://www.w3.org/2000/svg"
347 | x="0px"
348 | y="0px"
349 | width="100"
350 | height="100"
351 | viewBox="0,0,256,256"
352 | style="fill: #ffffff"
353 | >
354 | <g
355 | fill="#ffffff"
356 | fill-rule="nonzero"
357 | stroke="none"
358 | stroke-width="1"
359 | stroke-linecap="butt"
360 | stroke-linejoin="miter"
361 | stroke-miterlimit="10"
362 | stroke-dasharray=""
363 | stroke-dashoffset="0"
364 | font-family="none"
365 | font-weight="none"
366 | font-size="none"
367 | text-anchor="none"
368 | style="mix-blend-mode: normal"
369 | >
370 | <g transform="scale(5.12,5.12)">
371 | <path
372 | d="M44.52734,34.75c-1.07812,2.39453 -1.59766,3.46484 -2.98437,5.57813c-1.94141,2.95313 -4.67969,6.64063 -8.0625,6.66406c-3.01172,0.02734 -3.78906,-1.96484 -7.87891,-1.92969c-4.08594,0.01953 -4.9375,1.96875 -7.95312,1.9375c-3.38672,-0.03125 -5.97656,-3.35156 -7.91797,-6.30078c-5.42969,-8.26953 -6.00391,-17.96484 -2.64844,-23.12109c2.375,-3.65625 6.12891,-5.80469 9.65625,-5.80469c3.59375,0 5.85156,1.97266 8.82031,1.97266c2.88281,0 4.63672,-1.97656 8.79297,-1.97656c3.14063,0 6.46094,1.71094 8.83594,4.66406c-7.76562,4.25781 -6.50391,15.34766 1.33984,18.31641zM31.19531,8.46875c1.51172,-1.94141 2.66016,-4.67969 2.24219,-7.46875c-2.46484,0.16797 -5.34766,1.74219 -7.03125,3.78125c-1.52734,1.85938 -2.79297,4.61719 -2.30078,7.28516c2.69141,0.08594 5.47656,-1.51953 7.08984,-3.59766z"
373 | ></path>
374 | </g>
375 | </g>
376 | </svg>
377 | <span>Apple Calendar</span>
378 | </button>
379 | <button class="calendar-option" data-calendar-type="ics">
380 | <svg
381 | width="20"
382 | height="20"
383 | viewBox="0 0 24 24"
384 | fill="none"
385 | stroke="currentColor"
386 | stroke-width="1"
387 | >
388 | <path
389 | d="M12 15V3m0 12l-4-4m4 4l4-4M2 17l.621 2.485A2 2 0 004.561 21h14.878a2 2 0 001.94-1.515L22 17"
390 | stroke-linecap="round"
391 | stroke-linejoin="round"
392 | />
393 | </svg>
394 | <span>Download .ics File</span>
395 | </button>
396 | </div>
397 | </dialog>
398 | <script src="script.js"></script>
399 | </body>
400 | </html>
401 |
```
--------------------------------------------------------------------------------
/apps/radar/src/tools/radar.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 | import {
6 | PaginationLimitParam,
7 | PaginationOffsetParam,
8 | } from '@repo/mcp-common/src/types/shared.types'
9 |
10 | import {
11 | AiDimensionParam,
12 | AsnArrayParam,
13 | AsnParam,
14 | AsOrderByParam,
15 | ContinentArrayParam,
16 | DateEndArrayParam,
17 | DateEndParam,
18 | DateListParam,
19 | DateRangeArrayParam,
20 | DateRangeParam,
21 | DateStartArrayParam,
22 | DateStartParam,
23 | DnsDimensionParam,
24 | DomainParam,
25 | DomainRankingTypeParam,
26 | EmailRoutingDimensionParam,
27 | EmailSecurityDimensionParam,
28 | HttpDimensionParam,
29 | InternetQualityMetricParam,
30 | InternetServicesCategoryParam,
31 | InternetSpeedDimensionParam,
32 | InternetSpeedOrderByParam,
33 | IpParam,
34 | L3AttackDimensionParam,
35 | L7AttackDimensionParam,
36 | LocationArrayParam,
37 | LocationListParam,
38 | LocationParam,
39 | } from '../types/radar'
40 | import { resolveAndInvoke } from '../utils'
41 |
42 | import type { RadarMCP } from '../radar.app'
43 |
44 | export function registerRadarTools(agent: RadarMCP) {
45 | agent.server.tool(
46 | 'list_autonomous_systems',
47 | 'List Autonomous Systems',
48 | {
49 | limit: PaginationLimitParam,
50 | offset: PaginationOffsetParam,
51 | location: LocationParam.optional(),
52 | orderBy: AsOrderByParam,
53 | },
54 | async ({ limit, offset, location, orderBy }) => {
55 | try {
56 | const props = getProps(agent)
57 | const client = getCloudflareClient(props.accessToken)
58 | const r = await client.radar.entities.asns.list({
59 | limit,
60 | offset,
61 | location,
62 | orderBy,
63 | })
64 |
65 | return {
66 | content: [
67 | {
68 | type: 'text',
69 | text: JSON.stringify({
70 | result: r.asns,
71 | }),
72 | },
73 | ],
74 | }
75 | } catch (error) {
76 | return {
77 | content: [
78 | {
79 | type: 'text',
80 | text: `Error listing ASes: ${error instanceof Error && error.message}`,
81 | },
82 | ],
83 | }
84 | }
85 | }
86 | )
87 |
88 | agent.server.tool(
89 | 'get_as_details',
90 | 'Get Autonomous System details by ASN',
91 | {
92 | asn: AsnParam,
93 | },
94 | async ({ asn }) => {
95 | try {
96 | const props = getProps(agent)
97 | const client = getCloudflareClient(props.accessToken)
98 | const r = await client.radar.entities.asns.get(asn)
99 |
100 | return {
101 | content: [
102 | {
103 | type: 'text',
104 | text: JSON.stringify({
105 | result: r.asn,
106 | }),
107 | },
108 | ],
109 | }
110 | } catch (error) {
111 | return {
112 | content: [
113 | {
114 | type: 'text',
115 | text: `Error getting AS details: ${error instanceof Error && error.message}`,
116 | },
117 | ],
118 | }
119 | }
120 | }
121 | )
122 |
123 | agent.server.tool(
124 | 'get_ip_details',
125 | 'Get IP address information',
126 | {
127 | ip: IpParam,
128 | },
129 | async ({ ip }) => {
130 | try {
131 | const props = getProps(agent)
132 | const client = getCloudflareClient(props.accessToken)
133 | const r = await client.radar.entities.get({ ip })
134 |
135 | return {
136 | content: [
137 | {
138 | type: 'text',
139 | text: JSON.stringify({
140 | result: r.ip,
141 | }),
142 | },
143 | ],
144 | }
145 | } catch (error) {
146 | return {
147 | content: [
148 | {
149 | type: 'text',
150 | text: `Error getting IP details: ${error instanceof Error && error.message}`,
151 | },
152 | ],
153 | }
154 | }
155 | }
156 | )
157 |
158 | agent.server.tool(
159 | 'get_traffic_anomalies',
160 | 'Get traffic anomalies and outages',
161 | {
162 | limit: PaginationLimitParam,
163 | offset: PaginationOffsetParam,
164 | asn: AsnParam.optional(),
165 | location: LocationParam.optional(),
166 | dateRange: DateRangeParam.optional(),
167 | dateStart: DateStartParam.optional(),
168 | dateEnd: DateEndParam.optional(),
169 | },
170 | async ({ limit, offset, asn, location, dateStart, dateEnd, dateRange }) => {
171 | try {
172 | const props = getProps(agent)
173 | const client = getCloudflareClient(props.accessToken)
174 | const r = await client.radar.trafficAnomalies.get({
175 | limit,
176 | offset,
177 | asn,
178 | location,
179 | dateRange,
180 | dateStart,
181 | dateEnd,
182 | status: 'VERIFIED',
183 | })
184 |
185 | return {
186 | content: [
187 | {
188 | type: 'text',
189 | text: JSON.stringify({
190 | result: r.trafficAnomalies,
191 | }),
192 | },
193 | ],
194 | }
195 | } catch (error) {
196 | return {
197 | content: [
198 | {
199 | type: 'text',
200 | text: `Error getting IP details: ${error instanceof Error && error.message}`,
201 | },
202 | ],
203 | }
204 | }
205 | }
206 | )
207 |
208 | agent.server.tool(
209 | 'get_internet_services_ranking',
210 | 'Get top Internet services',
211 | {
212 | limit: PaginationLimitParam,
213 | date: DateListParam.optional(),
214 | serviceCategory: InternetServicesCategoryParam.optional(),
215 | },
216 | async ({ limit, date, serviceCategory }) => {
217 | try {
218 | const props = getProps(agent)
219 | const client = getCloudflareClient(props.accessToken)
220 | const r = await client.radar.ranking.internetServices.top({
221 | limit,
222 | date,
223 | serviceCategory,
224 | })
225 |
226 | return {
227 | content: [
228 | {
229 | type: 'text',
230 | text: JSON.stringify({
231 | result: r,
232 | }),
233 | },
234 | ],
235 | }
236 | } catch (error) {
237 | return {
238 | content: [
239 | {
240 | type: 'text',
241 | text: `Error getting Internet services ranking: ${error instanceof Error && error.message}`,
242 | },
243 | ],
244 | }
245 | }
246 | }
247 | )
248 |
249 | agent.server.tool(
250 | 'get_domains_ranking',
251 | 'Get top or trending domains',
252 | {
253 | limit: PaginationLimitParam,
254 | date: DateListParam.optional(),
255 | location: LocationListParam.optional(),
256 | rankingType: DomainRankingTypeParam.optional(),
257 | },
258 | async ({ limit, date, location, rankingType }) => {
259 | try {
260 | const props = getProps(agent)
261 | const client = getCloudflareClient(props.accessToken)
262 | const r = await client.radar.ranking.top({
263 | limit,
264 | date,
265 | location,
266 | rankingType,
267 | })
268 |
269 | return {
270 | content: [
271 | {
272 | type: 'text',
273 | text: JSON.stringify({
274 | result: r,
275 | }),
276 | },
277 | ],
278 | }
279 | } catch (error) {
280 | return {
281 | content: [
282 | {
283 | type: 'text',
284 | text: `Error getting domains ranking: ${error instanceof Error && error.message}`,
285 | },
286 | ],
287 | }
288 | }
289 | }
290 | )
291 |
292 | agent.server.tool(
293 | 'get_domain_rank_details',
294 | 'Get domain rank details',
295 | {
296 | domain: DomainParam,
297 | date: DateListParam.optional(),
298 | },
299 | async ({ domain, date }) => {
300 | try {
301 | const props = getProps(agent)
302 | const client = getCloudflareClient(props.accessToken)
303 | const r = await client.radar.ranking.domain.get(domain, { date })
304 |
305 | return {
306 | content: [
307 | {
308 | type: 'text',
309 | text: JSON.stringify({
310 | result: r,
311 | }),
312 | },
313 | ],
314 | }
315 | } catch (error) {
316 | return {
317 | content: [
318 | {
319 | type: 'text',
320 | text: `Error getting domain ranking details: ${error instanceof Error && error.message}`,
321 | },
322 | ],
323 | }
324 | }
325 | }
326 | )
327 |
328 | agent.server.tool(
329 | 'get_http_data',
330 | 'Retrieve HTTP traffic trends.',
331 | {
332 | dateRange: DateRangeArrayParam.optional(),
333 | dateStart: DateStartArrayParam.optional(),
334 | dateEnd: DateEndArrayParam.optional(),
335 | asn: AsnArrayParam,
336 | continent: ContinentArrayParam,
337 | location: LocationArrayParam,
338 | dimension: HttpDimensionParam,
339 | },
340 | async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => {
341 | try {
342 | const props = getProps(agent)
343 | const client = getCloudflareClient(props.accessToken)
344 | const r = await resolveAndInvoke(client.radar.http, dimension, {
345 | asn,
346 | continent,
347 | location,
348 | dateRange,
349 | dateStart,
350 | dateEnd,
351 | })
352 |
353 | return {
354 | content: [
355 | {
356 | type: 'text',
357 | text: JSON.stringify({
358 | result: r,
359 | }),
360 | },
361 | ],
362 | }
363 | } catch (error) {
364 | return {
365 | content: [
366 | {
367 | type: 'text',
368 | text: `Error getting HTTP data: ${error instanceof Error && error.message}`,
369 | },
370 | ],
371 | }
372 | }
373 | }
374 | )
375 |
376 | agent.server.tool(
377 | 'get_dns_queries_data',
378 | 'Retrieve trends in DNS queries to the 1.1.1.1 resolver.',
379 | {
380 | dateRange: DateRangeArrayParam.optional(),
381 | dateStart: DateStartArrayParam.optional(),
382 | dateEnd: DateEndArrayParam.optional(),
383 | asn: AsnArrayParam,
384 | continent: ContinentArrayParam,
385 | location: LocationArrayParam,
386 | dimension: DnsDimensionParam,
387 | },
388 | async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => {
389 | try {
390 | const props = getProps(agent)
391 | const client = getCloudflareClient(props.accessToken)
392 | const r = await resolveAndInvoke(client.radar.dns, dimension, {
393 | asn,
394 | continent,
395 | location,
396 | dateRange,
397 | dateStart,
398 | dateEnd,
399 | })
400 |
401 | return {
402 | content: [
403 | {
404 | type: 'text',
405 | text: JSON.stringify({
406 | result: r,
407 | }),
408 | },
409 | ],
410 | }
411 | } catch (error) {
412 | return {
413 | content: [
414 | {
415 | type: 'text',
416 | text: `Error getting DNS data: ${error instanceof Error && error.message}`,
417 | },
418 | ],
419 | }
420 | }
421 | }
422 | )
423 |
424 | agent.server.tool(
425 | 'get_l7_attack_data',
426 | 'Retrieve application layer (L7) attack trends.',
427 | {
428 | dateRange: DateRangeArrayParam.optional(),
429 | dateStart: DateStartArrayParam.optional(),
430 | dateEnd: DateEndArrayParam.optional(),
431 | asn: AsnArrayParam,
432 | continent: ContinentArrayParam,
433 | location: LocationArrayParam,
434 | dimension: L7AttackDimensionParam,
435 | },
436 | async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => {
437 | try {
438 | const props = getProps(agent)
439 | const client = getCloudflareClient(props.accessToken)
440 | const r = await resolveAndInvoke(client.radar.attacks.layer7, dimension, {
441 | asn,
442 | continent,
443 | location,
444 | dateRange,
445 | dateStart,
446 | dateEnd,
447 | })
448 |
449 | return {
450 | content: [
451 | {
452 | type: 'text',
453 | text: JSON.stringify({
454 | result: r,
455 | }),
456 | },
457 | ],
458 | }
459 | } catch (error) {
460 | return {
461 | content: [
462 | {
463 | type: 'text',
464 | text: `Error getting L7 attack data: ${error instanceof Error && error.message}`,
465 | },
466 | ],
467 | }
468 | }
469 | }
470 | )
471 |
472 | agent.server.tool(
473 | 'get_l3_attack_data',
474 | 'Retrieve application layer (L3) attack trends.',
475 | {
476 | dateRange: DateRangeArrayParam.optional(),
477 | dateStart: DateStartArrayParam.optional(),
478 | dateEnd: DateEndArrayParam.optional(),
479 | asn: AsnArrayParam,
480 | continent: ContinentArrayParam,
481 | location: LocationArrayParam,
482 | dimension: L3AttackDimensionParam,
483 | },
484 | async ({ dateStart, dateEnd, dateRange, asn, location, continent, dimension }) => {
485 | try {
486 | const props = getProps(agent)
487 | const client = getCloudflareClient(props.accessToken)
488 | const r = await resolveAndInvoke(client.radar.attacks.layer3, dimension, {
489 | asn,
490 | continent,
491 | location,
492 | dateRange,
493 | dateStart,
494 | dateEnd,
495 | })
496 |
497 | return {
498 | content: [
499 | {
500 | type: 'text',
501 | text: JSON.stringify({
502 | result: r,
503 | }),
504 | },
505 | ],
506 | }
507 | } catch (error) {
508 | return {
509 | content: [
510 | {
511 | type: 'text',
512 | text: `Error getting L3 attack data: ${error instanceof Error && error.message}`,
513 | },
514 | ],
515 | }
516 | }
517 | }
518 | )
519 |
520 | agent.server.tool(
521 | 'get_email_routing_data',
522 | 'Retrieve Email Routing trends.',
523 | {
524 | dateRange: DateRangeArrayParam.optional(),
525 | dateStart: DateStartArrayParam.optional(),
526 | dateEnd: DateEndArrayParam.optional(),
527 | dimension: EmailRoutingDimensionParam,
528 | },
529 | async ({ dateStart, dateEnd, dateRange, dimension }) => {
530 | try {
531 | const props = getProps(agent)
532 | const client = getCloudflareClient(props.accessToken)
533 | const r = await resolveAndInvoke(client.radar.email.routing, dimension, {
534 | dateRange,
535 | dateStart,
536 | dateEnd,
537 | })
538 |
539 | return {
540 | content: [
541 | {
542 | type: 'text',
543 | text: JSON.stringify({
544 | result: r,
545 | }),
546 | },
547 | ],
548 | }
549 | } catch (error) {
550 | return {
551 | content: [
552 | {
553 | type: 'text',
554 | text: `Error getting Email Routing data: ${error instanceof Error && error.message}`,
555 | },
556 | ],
557 | }
558 | }
559 | }
560 | )
561 |
562 | agent.server.tool(
563 | 'get_email_security_data',
564 | 'Retrieve Email Security trends.',
565 | {
566 | dateRange: DateRangeArrayParam.optional(),
567 | dateStart: DateStartArrayParam.optional(),
568 | dateEnd: DateEndArrayParam.optional(),
569 | dimension: EmailSecurityDimensionParam,
570 | },
571 | async ({ dateStart, dateEnd, dateRange, dimension }) => {
572 | try {
573 | const props = getProps(agent)
574 | const client = getCloudflareClient(props.accessToken)
575 | const r = await resolveAndInvoke(client.radar.email.security, dimension, {
576 | dateRange,
577 | dateStart,
578 | dateEnd,
579 | })
580 |
581 | return {
582 | content: [
583 | {
584 | type: 'text',
585 | text: JSON.stringify({
586 | result: r,
587 | }),
588 | },
589 | ],
590 | }
591 | } catch (error) {
592 | return {
593 | content: [
594 | {
595 | type: 'text',
596 | text: `Error getting Email Security data: ${error instanceof Error && error.message}`,
597 | },
598 | ],
599 | }
600 | }
601 | }
602 | )
603 |
604 | agent.server.tool(
605 | 'get_internet_speed_data',
606 | 'Retrieve summary of bandwidth, latency, jitter, and packet loss, from the previous 90 days of Cloudflare Speed Test.',
607 | {
608 | dateEnd: DateEndArrayParam.optional(),
609 | asn: AsnArrayParam,
610 | continent: ContinentArrayParam,
611 | location: LocationArrayParam,
612 | dimension: InternetSpeedDimensionParam,
613 | orderBy: InternetSpeedOrderByParam.optional(),
614 | },
615 | async ({ dateEnd, asn, location, continent, dimension, orderBy }) => {
616 | if (orderBy && dimension === 'summary') {
617 | throw new Error('Order by is only allowed for top locations and ASes')
618 | }
619 |
620 | try {
621 | const props = getProps(agent)
622 | const client = getCloudflareClient(props.accessToken)
623 | const r = await resolveAndInvoke(client.radar.quality.speed, dimension, {
624 | asn,
625 | continent,
626 | location,
627 | dateEnd,
628 | })
629 |
630 | return {
631 | content: [
632 | {
633 | type: 'text',
634 | text: JSON.stringify({
635 | result: r,
636 | }),
637 | },
638 | ],
639 | }
640 | } catch (error) {
641 | return {
642 | content: [
643 | {
644 | type: 'text',
645 | text: `Error getting Internet speed data: ${error instanceof Error && error.message}`,
646 | },
647 | ],
648 | }
649 | }
650 | }
651 | )
652 |
653 | agent.server.tool(
654 | 'get_internet_quality_data',
655 | 'Retrieves a summary or time series of bandwidth, latency, or DNS response time percentiles from the Radar Internet Quality Index (IQI).',
656 | {
657 | dateRange: DateRangeArrayParam.optional(),
658 | dateStart: DateStartArrayParam.optional(),
659 | dateEnd: DateEndArrayParam.optional(),
660 | asn: AsnArrayParam,
661 | continent: ContinentArrayParam,
662 | location: LocationArrayParam,
663 | format: z.enum(['summary', 'timeseriesGroups']),
664 | metric: InternetQualityMetricParam,
665 | },
666 | async ({ dateRange, dateStart, dateEnd, asn, location, continent, format, metric }) => {
667 | try {
668 | const props = getProps(agent)
669 | const client = getCloudflareClient(props.accessToken)
670 | const r = await client.radar.quality.iqi[format]({
671 | asn,
672 | continent,
673 | location,
674 | dateRange,
675 | dateStart,
676 | dateEnd,
677 | metric,
678 | })
679 |
680 | return {
681 | content: [
682 | {
683 | type: 'text',
684 | text: JSON.stringify({
685 | result: r,
686 | }),
687 | },
688 | ],
689 | }
690 | } catch (error) {
691 | return {
692 | content: [
693 | {
694 | type: 'text',
695 | text: `Error getting Internet quality data: ${error instanceof Error && error.message}`,
696 | },
697 | ],
698 | }
699 | }
700 | }
701 | )
702 |
703 | agent.server.tool(
704 | 'get_ai_data',
705 | 'Retrieves AI-related data, including traffic from AI user agents, as well as popular models and model tasks specifically from Cloudflare Workers AI.',
706 | {
707 | dateRange: DateRangeArrayParam.optional(),
708 | dateStart: DateStartArrayParam.optional(),
709 | dateEnd: DateEndArrayParam.optional(),
710 | asn: AsnArrayParam,
711 | continent: ContinentArrayParam,
712 | location: LocationArrayParam,
713 | dimension: AiDimensionParam,
714 | },
715 | async ({ dateRange, dateStart, dateEnd, asn, location, continent, dimension }) => {
716 | try {
717 | const props = getProps(agent)
718 | const client = getCloudflareClient(props.accessToken)
719 | const r = await resolveAndInvoke(client.radar.ai, dimension, {
720 | asn,
721 | continent,
722 | location,
723 | dateRange,
724 | dateStart,
725 | dateEnd,
726 | })
727 |
728 | return {
729 | content: [
730 | {
731 | type: 'text',
732 | text: JSON.stringify({
733 | result: r,
734 | }),
735 | },
736 | ],
737 | }
738 | } catch (error) {
739 | return {
740 | content: [
741 | {
742 | type: 'text',
743 | text: `Error getting AI data: ${error instanceof Error && error.message}`,
744 | },
745 | ],
746 | }
747 | }
748 | }
749 | )
750 | }
751 |
```
--------------------------------------------------------------------------------
/apps/demo-day/frontend/script.js:
--------------------------------------------------------------------------------
```javascript
1 | document.addEventListener('DOMContentLoaded', () => {
2 | const container = document.querySelector('.page-wrapper')
3 | const starfield = document.createElement('div')
4 | starfield.className = 'starfield'
5 | container.appendChild(starfield)
6 |
7 | // Create initial stars
8 | const numberOfStars = 300
9 | const stars = []
10 |
11 | function createStar() {
12 | const star = document.createElement('div')
13 | star.className = 'star'
14 |
15 | // Random position
16 | const x = Math.random() * window.innerWidth
17 | const y = Math.random() * window.innerHeight
18 |
19 | // Random size (more variation in sizes)
20 | const size = Math.random() * 2 + (Math.random() > 0.95 ? 1.5 : 0)
21 |
22 | // More subtle initial opacity
23 | const opacity = Math.random() * 0.15 + 0.05
24 |
25 | star.style.cssText = `
26 | left: ${x}px;
27 | top: ${y}px;
28 | width: ${size}px;
29 | height: ${size}px;
30 | opacity: ${opacity};
31 | `
32 |
33 | return star
34 | }
35 |
36 | // Initialize stars
37 | for (let i = 0; i < numberOfStars; i++) {
38 | const star = createStar()
39 | starfield.appendChild(star)
40 | stars.push(star)
41 | }
42 |
43 | // Animate stars
44 | function twinkle() {
45 | stars.forEach((star) => {
46 | // Random chance to twinkle
47 | if (Math.random() > 0.98) {
48 | const currentOpacity = parseFloat(star.style.opacity)
49 | const targetOpacity =
50 | currentOpacity < 0.1
51 | ? Math.random() * 0.15 + 0.05 // Brighter
52 | : Math.random() * 0.05 + 0.02 // Dimmer
53 |
54 | // Slower transition for more subtle effect
55 | star.style.transition = 'opacity 1.5s ease-in-out'
56 | star.style.opacity = targetOpacity
57 | }
58 | })
59 |
60 | requestAnimationFrame(twinkle)
61 | }
62 |
63 | // Start animation
64 | twinkle()
65 |
66 | // Form handling
67 | const emailForm = document.querySelector('.input-group')
68 | //const //emailInput = null //emailForm.querySelector('input[type="email"]')
69 | const honeypotInput = emailForm.querySelector('input[name="contact_me_by_fax"]')
70 | //const notifyButton = emailForm.querySelector('.notify-btn')
71 |
72 | // Check if user has already signed up
73 | /*if (localStorage.getItem('mcp_demo_signup')) {
74 | const savedEmail = localStorage.getItem('mcp_demo_signup')
75 | showSuccessState(savedEmail)
76 | }*/
77 |
78 | /*function showSuccessState(email) {
79 | const inputGroup = //emailInput.closest('.input-group')
80 | inputGroup.classList.add('success')
81 | //emailInput.value = email
82 | //emailInput.disabled = true
83 | notifyButton.disabled = true
84 |
85 | // Update button
86 | notifyButton.innerHTML = `
87 | <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
88 | <path d="M13.5 4.5L6 12L2.5 8.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
89 | </svg>
90 | `
91 | notifyButton.classList.add('success')
92 | createConfetti(notifyButton)
93 | }*/
94 |
95 | // Rate limiting configuration
96 | const RATE_LIMIT_DURATION = 60000 // 1 minute
97 | const MAX_ATTEMPTS = 5
98 | let attemptCount = 0
99 | let lastAttemptTime = 0
100 |
101 | function checkRateLimit() {
102 | const now = Date.now()
103 | if (now - lastAttemptTime > RATE_LIMIT_DURATION) {
104 | attemptCount = 0
105 | }
106 |
107 | if (attemptCount >= MAX_ATTEMPTS) {
108 | return false
109 | }
110 |
111 | attemptCount++
112 | lastAttemptTime = now
113 | return true
114 | }
115 |
116 | // Enhanced email validation
117 | function isValidEmail(email) {
118 | if (email.length > 254) return false
119 | const re =
120 | /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/
121 | return re.test(email)
122 | }
123 |
124 | // XSS Prevention
125 | function sanitizeInput(str) {
126 | const div = document.createElement('div')
127 | div.textContent = str
128 | return div.innerHTML
129 | }
130 |
131 | // Debounced email validation
132 | const debounce = (fn, delay) => {
133 | let timeoutId
134 | return (...args) => {
135 | clearTimeout(timeoutId)
136 | timeoutId = setTimeout(() => fn(...args), delay)
137 | }
138 | }
139 |
140 | // Retry logic for API calls
141 | async function retryFetch(url, options, maxRetries = 3) {
142 | for (let i = 0; i < maxRetries; i++) {
143 | try {
144 | const response = await fetch(url, options)
145 | const data = await response.json()
146 | return { response, data }
147 | } catch (error) {
148 | if (i === maxRetries - 1) throw error
149 | await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)))
150 | }
151 | }
152 | }
153 |
154 | // Enhanced toast function with accessibility
155 | function showToast(message, duration = 3000) {
156 | const existingToast = document.querySelector('.toast')
157 | if (existingToast) {
158 | existingToast.remove()
159 | }
160 |
161 | const toast = document.createElement('div')
162 | toast.className = 'toast'
163 | toast.setAttribute('role', 'alert')
164 | toast.setAttribute('aria-live', 'polite')
165 | toast.textContent = sanitizeInput(message)
166 | document.body.appendChild(toast)
167 |
168 | toast.offsetHeight
169 | toast.classList.add('show')
170 |
171 | setTimeout(() => {
172 | toast.classList.remove('show')
173 | setTimeout(() => toast.remove(), 300)
174 | }, duration)
175 | }
176 |
177 | // Setup accessibility attributes
178 | /*function setupAccessibility() {
179 | //emailInput.setAttribute('aria-label', 'Email address for notification')
180 | notifyButton.setAttribute('aria-label', 'Sign up for notification')
181 | document.querySelector('.success-message')?.setAttribute('role', 'status')
182 | }*/
183 |
184 | // Debounced email validation on input
185 | const validateEmailDebounced = debounce((email) => {
186 | const isValid = isValidEmail(email)
187 | //emailInput.style.border = isValid ? '' : '1px solid red'
188 | }, 300)
189 |
190 | //emailInput.addEventListener('input', (e) => validateEmailDebounced(e.target.value))
191 |
192 | // Enhanced click handler with all improvements
193 | /*notifyButton.addEventListener('click', async (e) => {
194 | e.preventDefault()
195 | //const email = //emailInput.value.trim()
196 |
197 | // Rate limit check
198 | if (!checkRateLimit()) {
199 | showToast('Please wait a minute before trying again.')
200 | return
201 | }
202 |
203 | // Basic validation
204 | if (!email) {
205 | //emailInput.style.border = '1px solid red'
206 | return
207 | }
208 |
209 | if (!isValidEmail(email)) {
210 | //emailInput.style.border = '1px solid red'
211 | showToast('Please enter a valid email address.')
212 | return
213 | }
214 |
215 | // Check if already signed up
216 | /*if (localStorage.getItem('mcp_demo_signup')) {
217 | return
218 | }*/
219 |
220 | // Honeypot check
221 | /*if (honeypotInput.value) {
222 | console.log('Bot detected')
223 | return
224 | }
225 |
226 | try {
227 | const { response, data } = await retryFetch(
228 | 'https://starbasedb-3285.outerbase.workers.dev/query',
229 | {
230 | method: 'POST',
231 | headers: {
232 | 'X-Starbase-Source': 'internal',
233 | Authorization: 'Bearer 8gmjguywgvsy2hvxnqpqzapwjq896ke3',
234 | 'Content-Type': 'application/json',
235 | },
236 | body: JSON.stringify({
237 | sql: 'INSERT INTO signups (email, status, created_at) VALUES (?, ?, CURRENT_TIMESTAMP)',
238 | params: [sanitizeInput(email), 'pending'],
239 | }),
240 | }
241 | )
242 |
243 | if (!response.ok) {
244 | throw new Error(data.error || 'Signup failed')
245 | }
246 |
247 | localStorage.setItem('mcp_demo_signup', email)
248 | showSuccessState(email)
249 | setupCalendarActions()
250 | setupAccessibility()
251 | } catch (error) {
252 | console.error('Error:', error)
253 | //emailInput.style.border = '1px solid red'
254 |
255 | if (error.message.includes('UNIQUE constraint failed')) {
256 | showToast('This email is already registered for the demo. Check your inbox for details.')
257 | localStorage.setItem('mcp_demo_signup', email)
258 | showSuccessState(email)
259 | setupCalendarActions()
260 | setupAccessibility()
261 | } else {
262 | showToast('Something went wrong. Please try again.')
263 | }
264 | }
265 | })*/
266 |
267 | // Initialize accessibility
268 | //()
269 |
270 | function setupCalendarActions() {
271 | const calendarActions = document.querySelectorAll(
272 | '.success-message .calendar-action, .calendar-option'
273 | )
274 | const eventDetails =
275 | "Get a preview of the future of agentic software. See how the world's most innovative platforms have connected agents to their services with MCP to build a new class of product experiences.\n\nJoin Live: https://cloudflare.tv/mcp-demo-day"
276 | const location = 'https://cloudflare.tv/mcp-demo-day'
277 |
278 | calendarActions.forEach((option) => {
279 | option.addEventListener('click', () => {
280 | const calendarType = option.dataset.calendarType
281 |
282 | switch (calendarType) {
283 | case 'google':
284 | window.open(
285 | 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=MCP+Demo+Day&details=Get+a+preview+of+the+future+of+agentic+software.+See+how+the+world%27s+most+innovative+platforms+have+connected+agents+to+their+services+with+MCP+to+build+a+new+class+of+product+experiences.%0A%0AJoin+Live%3A+https%3A%2F%2Fcloudflare.tv%2Fmcp-demo-day&location=https%3A%2F%2Fcloudflare.tv%2Fmcp-demo-day&dates=20250501T170000Z%2F20250501T183000Z',
286 | '_blank'
287 | )
288 | break
289 |
290 | case 'outlook':
291 | window.open(
292 | 'https://outlook.live.com/calendar/0/deeplink/compose?subject=MCP+Demo+Day&body=Get+a+preview+of+the+future+of+agentic+software.+See+how+the+world%27s+most+innovative+platforms+have+connected+agents+to+their+services+with+MCP+to+build+a+new+class+of+product+experiences.%0A%0AJoin+Live%3A+https%3A%2F%2Fcloudflare.tv%2Fmcp-demo-day&startdt=2025-05-01T17%3A00%3A00Z&enddt=2025-05-01T18%3A30%3A00Z&location=https%3A%2F%2Fcloudflare.tv%2Fmcp-demo-day',
293 | '_blank'
294 | )
295 | break
296 |
297 | case 'apple':
298 | case 'ics':
299 | const icsContent = `BEGIN:VCALENDAR
300 | VERSION:2.0
301 | PRODID:-//MCP Demo Day//EN
302 | CALSCALE:GREGORIAN
303 | BEGIN:VEVENT
304 | SUMMARY:MCP Demo Day
305 | DESCRIPTION:${eventDetails.replace(/\n/g, '\\n')}
306 | LOCATION:${location}
307 | DTSTART:20250501T170000Z
308 | DTEND:20250501T183000Z
309 | STATUS:CONFIRMED
310 | SEQUENCE:0
311 | END:VEVENT
312 | END:VCALENDAR`
313 |
314 | if (calendarType === 'apple') {
315 | const dataUri = 'data:text/calendar;charset=utf-8,' + encodeURIComponent(icsContent)
316 | window.open(dataUri)
317 | } else {
318 | const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' })
319 | const link = document.createElement('a')
320 | link.href = window.URL.createObjectURL(blob)
321 | link.download = 'mcp_demo_day.ics'
322 | document.body.appendChild(link)
323 | link.click()
324 | document.body.removeChild(link)
325 | }
326 | break
327 | }
328 |
329 | // Close the dialog if it's open
330 | const dialog = document.getElementById('calendarDialog')
331 | if (dialog) {
332 | dialog.close()
333 | }
334 | })
335 | })
336 | }
337 |
338 | // Call setupCalendarActions immediately if needed
339 | setupCalendarActions()
340 |
341 | // Remove red border on input focus
342 | //emailInput.addEventListener('focus', () => {
343 | //emailInput.style.border = 'none'
344 | //})
345 |
346 | // Company list hover effect
347 | const companies = document.querySelectorAll('.demo-companies li')
348 | companies.forEach((company) => {
349 | company.addEventListener('mouseenter', () => {
350 | companies.forEach((c) => {
351 | if (c !== company) {
352 | c.style.opacity = '0.5'
353 | }
354 | })
355 | })
356 |
357 | company.addEventListener('mouseleave', () => {
358 | companies.forEach((c) => {
359 | c.style.opacity = '1'
360 | })
361 | })
362 | })
363 |
364 | // Setup company backgrounds
365 | const companyNames = [
366 | 'asana',
367 | 'atlassian',
368 | 'cloudflare',
369 | 'intercom',
370 | 'linear',
371 | 'paypal',
372 | 'sentry',
373 | 'square',
374 | 'stripe',
375 | 'webflow',
376 | 'more',
377 | ]
378 |
379 | const demoList = document.querySelector('.demo-companies')
380 | const companyItems = demoList.querySelectorAll('li')
381 |
382 | // Add data attributes to company items
383 | companyItems.forEach((item, index) => {
384 | const company = companyNames[index]
385 | if (company) {
386 | item.setAttribute('data-company', company)
387 | }
388 | console.log('add attrib')
389 | })
390 |
391 | // Create background containers for each company
392 | companyNames.forEach((company) => {
393 | console.log('add container')
394 | const background = document.createElement('div')
395 | background.className = `company-background ${company}`
396 |
397 | // Load SVG from file
398 | fetch(`/public/${company}.svg`)
399 | .then((response) => response.text())
400 | .then((svgContent) => {
401 | // Clean up the SVG to remove any fill paths
402 | const parser = new DOMParser()
403 | const doc = parser.parseFromString(svgContent, 'image/svg+xml')
404 | const svg = doc.querySelector('svg')
405 |
406 | // Remove any fill attributes and set stroke
407 | svg.querySelectorAll('path, circle, rect').forEach((path) => {
408 | path.setAttribute('fill', 'none')
409 | path.setAttribute('stroke', 'currentColor')
410 | path.setAttribute('stroke-width', '.5')
411 | })
412 |
413 | background.innerHTML = svg.outerHTML
414 | })
415 | .catch((error) => console.error(`Error loading ${company}.svg:`, error))
416 |
417 | demoList.appendChild(background)
418 | })
419 |
420 | // Add cycling animation for company backgrounds
421 | let currentBackgroundIndex = 0
422 | let isHovering = false
423 | let hoverCompany = null
424 | const backgrounds = document.querySelectorAll('.company-background')
425 |
426 | function cycleBackgrounds() {
427 | // Remove active class from all backgrounds and company names
428 | backgrounds.forEach((bg) => bg.classList.remove('active'))
429 | companyItems.forEach((item) => item.classList.remove('active'))
430 |
431 | // If hovering, show the hovered company's background and highlight its name
432 | if (isHovering && hoverCompany) {
433 | const hoverBackground = Array.from(backgrounds).find((bg) =>
434 | bg.classList.contains(hoverCompany)
435 | )
436 | const hoverItem = Array.from(companyItems).find(
437 | (item) => item.getAttribute('data-company') === hoverCompany
438 | )
439 | if (hoverBackground) {
440 | hoverBackground.classList.add('active')
441 | if (hoverItem) hoverItem.classList.add('active')
442 | return
443 | }
444 | }
445 |
446 | // Otherwise, show the next background in the cycle and highlight its name
447 | backgrounds[currentBackgroundIndex].classList.add('active')
448 | companyItems[currentBackgroundIndex].classList.add('active')
449 | currentBackgroundIndex = (currentBackgroundIndex + 1) % backgrounds.length
450 | console.log('cucle')
451 | }
452 |
453 | // Start cycling every 3 seconds
454 | setInterval(cycleBackgrounds, 3000)
455 | // Show first background immediately
456 | cycleBackgrounds()
457 |
458 | // Handle hover states
459 | companyItems.forEach((item) => {
460 | item.addEventListener('mouseenter', () => {
461 | isHovering = true
462 | hoverCompany = item.getAttribute('data-company')
463 | cycleBackgrounds() // Show the hovered company immediately
464 | })
465 |
466 | item.addEventListener('mouseleave', () => {
467 | isHovering = false
468 | hoverCompany = null
469 | cycleBackgrounds() // Resume normal cycling
470 | })
471 | })
472 |
473 | function parseDateTime() {
474 | const dateText = document.querySelector('.date-time h2').textContent.trim() // e.g., "APRIL 30TH, 2025"
475 | const timeText = document.querySelector('.date-time h3').textContent.trim() // e.g., "ONLINE, 1:00 PM PT"
476 |
477 | // Remove ordinal indicators (st, nd, rd, th) and parse date
478 | const cleanDateText = dateText.replace(/(ST|ND|RD|TH),/i, ',')
479 |
480 | // Create a more precise regex to match the date format
481 | const dateTimeParts = cleanDateText.match(/^([A-Za-z]+)\s+(\d{1,2}),\s*(\d{4})$/i)
482 | // Handle "ONLINE, " prefix in time
483 | const timeParts = timeText
484 | .replace(/^ONLINE,\s*/i, '')
485 | .match(/^(\d{1,2}):(\d{2})\s*(AM|PM)\s*(PST|PDT|EST|EDT|CST|CDT|MST|MDT|PT)?$/i)
486 |
487 | if (!dateTimeParts || !timeParts) {
488 | throw new Error('Invalid date/time format')
489 | }
490 |
491 | const [, month, day, year] = dateTimeParts
492 | const [, hours, minutes, ampm, timezone] = timeParts
493 |
494 | let hour24 = parseInt(hours)
495 |
496 | // Convert to 24-hour format
497 | if (ampm.toUpperCase() === 'PM' && hour24 < 12) hour24 += 12
498 | if (ampm.toUpperCase() === 'AM' && hour24 === 12) hour24 = 0
499 |
500 | // Create date in UTC
501 | const date = new Date(
502 | Date.UTC(parseInt(year), getMonthIndex(month), parseInt(day), hour24, parseInt(minutes), 0)
503 | )
504 |
505 | // Since the time is specified in PT (Pacific Time), adjust for PT (-7 hours from UTC during PDT)
506 | date.setUTCHours(date.getUTCHours() - 7)
507 |
508 | if (isNaN(date.getTime())) {
509 | throw new Error('Invalid date/time format')
510 | }
511 |
512 | return date
513 | }
514 |
515 | // Helper function to get month index (0-11) from month name
516 | function getMonthIndex(month) {
517 | const months = {
518 | JANUARY: 0,
519 | JAN: 0,
520 | FEBRUARY: 1,
521 | FEB: 1,
522 | MARCH: 2,
523 | MAR: 2,
524 | APRIL: 3,
525 | APR: 3,
526 | MAY: 4,
527 | JUNE: 5,
528 | JUN: 5,
529 | JULY: 6,
530 | JUL: 6,
531 | AUGUST: 7,
532 | AUG: 7,
533 | SEPTEMBER: 8,
534 | SEP: 8,
535 | OCTOBER: 9,
536 | OCT: 9,
537 | NOVEMBER: 10,
538 | NOV: 10,
539 | DECEMBER: 11,
540 | DEC: 11,
541 | }
542 | return months[month.toUpperCase()]
543 | }
544 | })
545 |
546 | // Helper functions
547 | function isValidEmail(email) {
548 | const re =
549 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
550 | return re.test(email.toLowerCase())
551 | }
552 |
553 | // Particle System
554 | function createParticle() {
555 | const particle = document.createElement('div')
556 | particle.className = 'particle'
557 |
558 | // Random size between 3-6px
559 | const size = Math.random() * 3 + 3
560 | particle.style.width = `${size}px`
561 | particle.style.height = `${size}px`
562 |
563 | // Random starting position, but keep particles within viewport bounds
564 | const startX = Math.random() * (window.innerWidth - size)
565 | const startY = window.innerHeight + size
566 |
567 | particle.style.left = `${startX}px`
568 | particle.style.top = `${startY}px`
569 |
570 | // Random animation duration between 6-10 seconds
571 | const duration = Math.random() * 4000 + 6000
572 | particle.style.animation = `floatUp ${duration}ms cubic-bezier(0.4, 0, 0.2, 1) forwards`
573 |
574 | document.body.appendChild(particle)
575 |
576 | // Cleanup after animation
577 | setTimeout(() => {
578 | if (particle && particle.parentNode) {
579 | particle.parentNode.removeChild(particle)
580 | }
581 | }, duration)
582 | }
583 |
584 | // Particle manager
585 | let particleInterval
586 | const startParticles = () => {
587 | // Create initial batch of particles
588 | for (let i = 0; i < 5; i++) {
589 | setTimeout(() => createParticle(), i * 200)
590 | }
591 |
592 | // Create a new particle every 400ms
593 | particleInterval = setInterval(() => {
594 | // Limit to 15 particles at a time for better performance
595 | if (document.querySelectorAll('.particle').length < 15) {
596 | createParticle()
597 | }
598 | }, 400)
599 | }
600 |
601 | // Cleanup function
602 | const cleanupParticles = () => {
603 | if (particleInterval) {
604 | clearInterval(particleInterval)
605 | particleInterval = null
606 | }
607 | document.querySelectorAll('.particle').forEach((particle) => {
608 | if (particle.parentNode) {
609 | particle.parentNode.removeChild(particle)
610 | }
611 | })
612 | }
613 |
614 | // Start particles when page loads
615 | startParticles()
616 |
617 | // Cleanup on page unload
618 | window.addEventListener('unload', cleanupParticles)
619 |
620 | // Pause particles when page is not visible
621 | document.addEventListener('visibilitychange', () => {
622 | if (document.hidden) {
623 | cleanupParticles()
624 | } else {
625 | startParticles()
626 | }
627 | })
628 |
629 | // Restart particles on window resize
630 | let resizeTimeout
631 | window.addEventListener('resize', () => {
632 | if (resizeTimeout) {
633 | clearTimeout(resizeTimeout)
634 | }
635 | resizeTimeout = setTimeout(() => {
636 | cleanupParticles()
637 | startParticles()
638 | }, 200)
639 | })
640 |
641 | function createConfetti(button) {
642 | const colors = ['#FF6633', '#FF8533', '#FF9966', '#FFAA80']
643 | const confettiCount = 20
644 |
645 | for (let i = 0; i < confettiCount; i++) {
646 | const confetti = document.createElement('div')
647 | confetti.className = 'confetti'
648 |
649 | // Random size between 4-8px
650 | const size = Math.random() * 4 + 4
651 | confetti.style.width = `${size}px`
652 | confetti.style.height = `${size}px`
653 |
654 | // Random color from our palette
655 | confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]
656 |
657 | // Random position behind the button
658 | const startX = Math.random() * button.offsetWidth
659 | confetti.style.left = `${startX}px`
660 | confetti.style.top = '50%'
661 |
662 | // Random animation duration and delay
663 | const duration = Math.random() * 400 + 600
664 | const delay = Math.random() * 200
665 | confetti.style.animation = `confettiFall ${duration}ms cubic-bezier(0.4, 0, 0.2, 1) ${delay}ms forwards`
666 |
667 | button.appendChild(confetti)
668 |
669 | // Cleanup
670 | setTimeout(() => {
671 | if (confetti.parentNode === button) {
672 | button.removeChild(confetti)
673 | }
674 | }, duration + delay)
675 | }
676 | }
677 |
```