Skip to content

Commit bc4b1de

Browse files
authored
Merge branch 'main' into bib/mcp-servers
2 parents b3c41fa + 97e23c0 commit bc4b1de

File tree

19 files changed

+6511
-13
lines changed

19 files changed

+6511
-13
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ The following servers are included in this repository:
2222
| [**Digital Experience Monitoring server**](/apps/dex-analysis) | Get quick insight on critical applications for your organization | `https://dex.mcp.cloudflare.com/sse` |
2323
| [**Cloudflare One CASB server**](/apps/casb) | Quickly identify any security misconfigurations for SaaS applications to safeguard users & data | `https://casb.mcp.cloudflare.com/sse` |
2424

25-
2625
## Access the remote MCP server from any MCP client
2726

2827
If your MCP client has first class support for remote MCP servers, the client will provide a way to accept the server URL directly within its interface (e.g. [Cloudflare AI Playground](https://playground.ai.cloudflare.com/))

apps/auditlogs/src/tools/auditlogs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api'
55
import type { AuditlogMCP } from '../index'
66

77
export const actionResults = z.enum(['success', 'failure', ''])
8-
export const actionTypes = z.enum(['create', 'delete', 'view', 'update'])
8+
export const actionTypes = z.enum(['create', 'delete', 'view', 'update', 'login'])
99
export const actorContexts = z.enum(['api_key', 'api_token', 'dash', 'oauth', 'origin_ca_key'])
10-
export const actorTypes = z.enum(['cloudflare_admin', 'account', 'user'])
11-
export const resourceScopes = z.enum(['accounts', 'user', 'zones'])
10+
export const actorTypes = z.enum(['cloudflare_admin', 'account', 'user', 'system'])
11+
export const resourceScopes = z.enum(['memberships', 'accounts', 'user', 'zones'])
1212
export const sortDirections = z.enum(['desc', 'asc'])
1313

1414
export const auditLogsQuerySchema = z.object({
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CLOUDFLARE_CLIENT_ID=
2+
CLOUDFLARE_CLIENT_SECRET=
3+
DEV_CLOUDFLARE_API_TOKEN=

apps/dns-analytics/.eslintrc.cjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import("eslint").Linter.Config} */
2+
module.exports = {
3+
root: true,
4+
extends: ['@repo/eslint-config/default.cjs'],
5+
}

apps/dns-analytics/CONTRIBUTING.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Setup
2+
3+
If you'd like to iterate and test your MCP server, you can do so in local development.
4+
5+
## Local Development
6+
7+
1. Create a `.dev.vars` file in your project root:
8+
9+
If you're a Cloudflare employee:
10+
11+
```
12+
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
13+
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
14+
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
15+
```
16+
17+
If you're an external contributor, you can provide a development API token (See [Cloudflare API](https://developers.cloudflare.com/api/) for information on creating an API Token):
18+
19+
```
20+
DEV_DISABLE_OAUTH=true
21+
DEV_CLOUDFLARE_EMAIL=your_cloudflare_email
22+
# This is your api token with endpoint access.
23+
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
24+
```
25+
26+
2. Start the local development server:
27+
28+
```bash
29+
npx wrangler dev
30+
```
31+
32+
3. To test locally, open Inspector, and connect to `http://localhost:8976/sse`.
33+
Once you follow the prompts, you'll be able to "List Tools". You can also connect with any MCP client.
34+
35+
## Deploying the Worker ( Cloudflare employees only )
36+
37+
Set secrets via Wrangler:
38+
39+
```bash
40+
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT>
41+
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT>
42+
```
43+
44+
## Set up a KV namespace
45+
46+
Create the KV namespace:
47+
48+
```bash
49+
npx wrangler kv namespace create "OAUTH_KV"
50+
```
51+
52+
Then, update the Wrangler file with the generated KV namespace ID.
53+
54+
## Deploy & Test
55+
56+
Deploy the MCP server to make it available on your workers.dev domain:
57+
58+
```bash
59+
npx wrangler deploy -e <ENVIRONMENT>
60+
```
61+
62+
Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
63+
64+
```bash
65+
npx @modelcontextprotocol/inspector@latest
66+
```

apps/dns-analytics/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Cloudflare DNS Analytics MCP Server 📡
2+
3+
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP
4+
connections, with Cloudflare OAuth built-in.
5+
6+
It integrates tools powered by the [Cloudflare DNS Analytics API](https://developers.cloudflare.com/api/resources/dns/) to provide insights on DNS analytics and optimization.
7+
8+
## 🔨 Available Tools
9+
10+
Currently available tools:
11+
12+
| **Category** | **Tool** | **Description** |
13+
| ----------------------- | --------------------------- | -------------------------------------------------------------- |
14+
| **Zone Information** | `zones_list` | List zones under the current active account. |
15+
| **DNS Analytics** | `dns_report` | Fetch the DNS Report for a given zone over a given time frame. |
16+
| **Account DNS Setting** | `show_account_dns_settings` | Fetch the DNS setting for the current active account. |
17+
| **Zone DNS Setting** | `show_zone_dns_settings` | Fetch the DNS setting for a given zone. |
18+
19+
This MCP server is still a work in progress, and we plan to add more tools in the future.
20+
21+
### Prompt Examples
22+
23+
- `List zones under my Cloudflare account.`
24+
- `What are the DNS Settings for my account?`
25+
- `Show me the zones under my account and fetch DNS Report for them.`
26+
- `How can I optimize my DNS Setting based on my DNS Report?`
27+
- `Which of my zones has the highest traffic?`
28+
- `Read Cloudflare's documentation on managing DNS records and tell me how to optimize my DNS settings.`
29+
- `Show me DNS Report for https://example.com in the last X days.`
30+
31+
## Access the remote MCP server from from any MCP Client
32+
33+
If your MCP client has first class support for remote MCP servers, the client will provide a way to accept the server URL (`https://dns-analytics.mcp.cloudflare.com`) directly within its interface (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)).
34+
35+
If your client does not yet support remote MCP servers, you will need to set up its respective configuration file using [mcp-remote](https://www.npmjs.com/package/mcp-remote) to specify which servers your client can access.
36+
37+
Replace the content with the following configuration:
38+
39+
```json
40+
{
41+
"mcpServers": {
42+
"cloudflare": {
43+
"command": "npx",
44+
"args": ["mcp-remote", "https://dns-analytics.mcp.cloudflare.com/sse"]
45+
}
46+
}
47+
}
48+
```
49+
50+
Once you've set up your configuration file, restart MCP client and a browser window will open showing your OAuth login page. Proceed through the authentication flow to grant the client access to your MCP server. After you grant access, the tools will become available for you to use.
51+
52+
Interested in contributing, and running this server locally? See [CONTRIBUTING.md](CONTRIBUTING.md) to get started.

apps/dns-analytics/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "dns-analytics",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"check:lint": "run-eslint-workers",
7+
"check:types": "run-tsc",
8+
"deploy": "wrangler deploy",
9+
"dev": "wrangler dev",
10+
"start": "wrangler dev",
11+
"types": "wrangler types --include-env=false",
12+
"test": "vitest run"
13+
},
14+
"dependencies": {
15+
"@cloudflare/workers-oauth-provider": "0.0.5",
16+
"@hono/zod-validator": "0.4.3",
17+
"@modelcontextprotocol/sdk": "1.10.2",
18+
"@repo/mcp-common": "workspace:*",
19+
"@repo/mcp-observability": "workspace:*",
20+
"agents": "0.0.67",
21+
"cloudflare": "4.2.0",
22+
"hono": "4.7.6",
23+
"zod": "3.24.2"
24+
},
25+
"devDependencies": {
26+
"@cloudflare/vitest-pool-workers": "0.8.14",
27+
"@types/node": "22.14.1",
28+
"prettier": "3.5.3",
29+
"typescript": "5.5.4",
30+
"vitest": "3.0.9",
31+
"wrangler": "4.10.0"
32+
}
33+
}

apps/dns-analytics/src/context.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
2+
import type { DNSAnalyticsMCP } from './index'
3+
4+
export interface Env {
5+
OAUTH_KV: KVNamespace
6+
ENVIRONMENT: 'development' | 'staging' | 'production'
7+
MCP_SERVER_NAME: string
8+
MCP_SERVER_VERSION: string
9+
CLOUDFLARE_CLIENT_ID: string
10+
CLOUDFLARE_CLIENT_SECRET: string
11+
MCP_OBJECT: DurableObjectNamespace<DNSAnalyticsMCP>
12+
USER_DETAILS: DurableObjectNamespace<UserDetails>
13+
MCP_METRICS: AnalyticsEngineDataset
14+
SENTRY_ACCESS_CLIENT_ID: string
15+
SENTRY_ACCESS_CLIENT_SECRET: string
16+
GIT_HASH: string
17+
SENTRY_DSN: string
18+
DEV_DISABLE_OAUTH: string
19+
DEV_CLOUDFLARE_API_TOKEN: string
20+
DEV_CLOUDFLARE_EMAIL: string
21+
}

apps/dns-analytics/src/index.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import OAuthProvider from '@cloudflare/workers-oauth-provider'
2+
import { McpAgent } from 'agents/mcp'
3+
4+
import {
5+
createAuthHandlers,
6+
handleTokenExchangeCallback,
7+
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
8+
import { handleDevMode } from '@repo/mcp-common/src/dev-mode'
9+
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
10+
import { getEnv } from '@repo/mcp-common/src/env'
11+
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
12+
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
13+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
14+
import { registerZoneTools } from '@repo/mcp-common/src/tools/zone'
15+
16+
import { MetricsTracker } from '../../../packages/mcp-observability/src'
17+
import { registerAnalyticTools } from './tools/analytics'
18+
19+
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
20+
import type { Env } from './context'
21+
22+
export { UserDetails }
23+
24+
const env = getEnv<Env>()
25+
26+
const metrics = new MetricsTracker(env.MCP_METRICS, {
27+
name: env.MCP_SERVER_NAME,
28+
version: env.MCP_SERVER_VERSION,
29+
})
30+
31+
// Context from the auth process, encrypted & stored in the auth token
32+
// and provided to the DurableMCP as this.props
33+
export type Props = AuthProps
34+
35+
export type State = { activeAccountId: string | null }
36+
37+
export class DNSAnalyticsMCP extends McpAgent<Env, State, Props> {
38+
_server: CloudflareMCPServer | undefined
39+
set server(server: CloudflareMCPServer) {
40+
this._server = server
41+
}
42+
43+
get server(): CloudflareMCPServer {
44+
if (!this._server) {
45+
throw new Error('Tried to access server before it was initialized')
46+
}
47+
48+
return this._server
49+
}
50+
51+
constructor(ctx: DurableObjectState, env: Env) {
52+
super(ctx, env)
53+
}
54+
55+
async init() {
56+
this.server = new CloudflareMCPServer({
57+
userId: this.props.user.id,
58+
wae: this.env.MCP_METRICS,
59+
serverInfo: {
60+
name: this.env.MCP_SERVER_NAME,
61+
version: this.env.MCP_SERVER_VERSION,
62+
},
63+
})
64+
65+
registerAccountTools(this)
66+
67+
// Register Cloudflare DNS Analytics tools
68+
registerAnalyticTools(this)
69+
70+
registerZoneTools(this)
71+
}
72+
73+
async getActiveAccountId() {
74+
try {
75+
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
76+
// we do this so we can persist activeAccountId across sessions
77+
const userDetails = getUserDetails(env, this.props.user.id)
78+
return await userDetails.getActiveAccountId()
79+
} catch (e) {
80+
this.server.recordError(e)
81+
return null
82+
}
83+
}
84+
85+
async setActiveAccountId(accountId: string) {
86+
try {
87+
const userDetails = getUserDetails(env, this.props.user.id)
88+
await userDetails.setActiveAccountId(accountId)
89+
} catch (e) {
90+
this.server.recordError(e)
91+
}
92+
}
93+
}
94+
95+
const AnalyticsScopes = {
96+
...RequiredScopes,
97+
'account:read': 'See your account info such as account details, analytics, and memberships.',
98+
'zone:read': 'See your zones',
99+
'dns_settings:read': 'See your DNS settings',
100+
'dns_analytics:read': 'See your DNS analytics',
101+
} as const
102+
103+
export default {
104+
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
105+
if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') {
106+
return await handleDevMode(DNSAnalyticsMCP, req, env, ctx)
107+
}
108+
109+
return new OAuthProvider({
110+
apiHandlers: {
111+
'/mcp': DNSAnalyticsMCP.serve('/mcp'),
112+
'/sse': DNSAnalyticsMCP.serveSSE('/sse'),
113+
},
114+
// @ts-ignore
115+
defaultHandler: createAuthHandlers({ scopes: AnalyticsScopes, metrics }),
116+
authorizeEndpoint: '/oauth/authorize',
117+
tokenEndpoint: '/token',
118+
tokenExchangeCallback: (options) =>
119+
handleTokenExchangeCallback(
120+
options,
121+
env.CLOUDFLARE_CLIENT_ID,
122+
env.CLOUDFLARE_CLIENT_SECRET
123+
),
124+
// Cloudflare access token TTL
125+
accessTokenTTL: 3600,
126+
clientRegistrationEndpoint: '/register',
127+
}).fetch(req, env, ctx)
128+
},
129+
}

0 commit comments

Comments
 (0)