Skip to content

Commit 0de0dd7

Browse files
committed
create radar mcp server
1 parent 3ee9cf4 commit 0de0dd7

File tree

14 files changed

+6036
-11
lines changed

14 files changed

+6036
-11
lines changed

apps/radar/.dev.vars.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CLOUDFLARE_CLIENT_ID=
2+
CLOUDFLARE_CLIENT_SECRET=

apps/radar/.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/radar/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Model Context Protocol (MCP) Server + Cloudflare Radar
2+
3+
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP connections, with Cloudflare OAuth built-in.
4+
5+
## Getting Started
6+
7+
- Set secrets via Wrangler (ask in the `Cloudflare's Own MCP Servers` internal channel to get credentials)
8+
9+
```bash
10+
wrangler secret put CLOUDFLARE_CLIENT_ID
11+
wrangler secret put CLOUDFLARE_CLIENT_SECRET
12+
```
13+
14+
#### Set up a KV namespace
15+
16+
- Create the KV namespace:
17+
`wrangler kv:namespace create "OAUTH_KV"`
18+
- Update the Wrangler file with the KV ID
19+
20+
#### Deploy & Test
21+
22+
Deploy the MCP server to make it available on your workers.dev domain
23+
` wrangler deploy`
24+
25+
Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
26+
27+
```
28+
npx @modelcontextprotocol/inspector@latest
29+
```

apps/radar/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "radar-mcp-server",
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+
"cf-typegen": "wrangler types",
12+
"test": "vitest run"
13+
},
14+
"dependencies": {
15+
"@cloudflare/workers-oauth-provider": "0.0.2",
16+
"@hono/zod-validator": "0.4.3",
17+
"@modelcontextprotocol/sdk": "1.9.0",
18+
"@repo/mcp-common": "workspace:*",
19+
"agents": "0.0.62",
20+
"cloudflare": "4.2.0",
21+
"hono": "4.7.6",
22+
"zod": "3.24.2"
23+
},
24+
"devDependencies": {
25+
"@cloudflare/vitest-pool-workers": "0.8.14",
26+
"@cloudflare/workers-types": "4.20250410.0",
27+
"prettier": "3.5.3",
28+
"typescript": "5.5.4",
29+
"vitest": "3.0.9",
30+
"wrangler": "4.10.0"
31+
}
32+
}

apps/radar/src/index.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import OAuthProvider from '@cloudflare/workers-oauth-provider'
2+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
3+
import { McpAgent } from 'agents/mcp'
4+
import { env } from 'cloudflare:workers'
5+
6+
import {
7+
createAuthHandlers,
8+
handleTokenExchangeCallback,
9+
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
10+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
11+
12+
import { registerRadarTools } from './tools/radar'
13+
14+
import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler'
15+
16+
// Context from the auth process, encrypted & stored in the auth token
17+
// and provided to the DurableMCP as this.props
18+
export type Props = {
19+
accessToken: string
20+
user: UserSchema['result']
21+
accounts: AccountSchema['result']
22+
}
23+
24+
export type State = { activeAccountId: string | null }
25+
26+
export class RadarMCP extends McpAgent<Env, State, Props> {
27+
server = new McpServer({
28+
name: 'Remote MCP Server with Cloudflare Radar Data',
29+
version: '1.0.0',
30+
})
31+
32+
initialState: State = {
33+
activeAccountId: null,
34+
}
35+
36+
async init() {
37+
registerAccountTools(this)
38+
39+
registerRadarTools(this)
40+
}
41+
42+
getActiveAccountId() {
43+
// TODO: Figure out why this fail sometimes, and why we need to wrap this in a try catch
44+
try {
45+
return this.state.activeAccountId ?? null
46+
} catch (e) {
47+
return null
48+
}
49+
}
50+
51+
setActiveAccountId(accountId: string) {
52+
// TODO: Figure out why this fail sometimes, and why we need to wrap this in a try catch
53+
try {
54+
this.setState({
55+
...this.state,
56+
activeAccountId: accountId,
57+
})
58+
} catch (e) {
59+
return null
60+
}
61+
}
62+
}
63+
64+
const RadarScopes = {
65+
'account:read': 'See your account info such as account details, analytics, and memberships.',
66+
'user:read': 'See your user info such as name, email address, and account memberships.',
67+
offline_access: 'Grants refresh tokens for long-lived access.',
68+
} as const
69+
70+
export default new OAuthProvider({
71+
apiRoute: '/sse',
72+
// @ts-ignore
73+
apiHandler: RadarMCP.mount('/sse'),
74+
// @ts-ignore
75+
defaultHandler: createAuthHandlers({ scopes: RadarScopes }),
76+
authorizeEndpoint: '/oauth/authorize',
77+
tokenEndpoint: '/token',
78+
tokenExchangeCallback: (options) =>
79+
handleTokenExchangeCallback(options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET),
80+
// Cloudflare access token TTL
81+
accessTokenTTL: 3600,
82+
clientRegistrationEndpoint: '/register',
83+
})

packages/mcp-common/src/tools/radar.ts renamed to apps/radar/src/tools/radar.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import { getCloudflareClient } from '../cloudflare-api'
2-
import { type CloudflareMcpAgent } from '../types/cloudflare-mcp-agent'
3-
import { AsnParam, AsOrderByParam, DateRangeParam, IpParam, SingleLocationParam } from '../types/radar'
4-
import { PaginationLimitParam, PaginationOffsetParam } from '../types/shared'
1+
import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
2+
import { type CloudflareMcpAgent } from '@repo/mcp-common/src/types/cloudflare-mcp-agent'
3+
import { PaginationLimitParam, PaginationOffsetParam } from '@repo/mcp-common/src/types/shared'
4+
5+
import {
6+
AsnParam,
7+
AsOrderByParam,
8+
DateRangeParam,
9+
IpParam,
10+
SingleLocationParam,
11+
} from '../types/radar'
512

613
export function registerRadarTools(agent: CloudflareMcpAgent) {
714
agent.server.tool(
@@ -122,7 +129,7 @@ export function registerRadarTools(agent: CloudflareMcpAgent) {
122129
offset: PaginationOffsetParam,
123130
asn: AsnParam.optional(),
124131
location: SingleLocationParam,
125-
dateRange: DateRangeParam
132+
dateRange: DateRangeParam,
126133
},
127134
async ({ limit, offset, asn, location, dateRange }) => {
128135
try {
@@ -133,7 +140,7 @@ export function registerRadarTools(agent: CloudflareMcpAgent) {
133140
asn: asn ?? undefined,
134141
location: location ?? undefined,
135142
dateRange: dateRange ?? undefined,
136-
status: "VERIFIED"
143+
status: 'VERIFIED',
137144
})
138145

139146
return {

packages/mcp-common/src/types/radar.ts renamed to apps/radar/src/types/radar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const DateRangeParam: z.ZodType<TrafficAnomalyGetParams['dateRange']> = z
1515
.toLowerCase()
1616
.regex(
1717
/^((([1-9]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|36[0-4])[d](control)?)|(([1-9]|[1-4][0-9]|5[0-2])[w](control)?))$/,
18-
'Invalid Date Range',
18+
'Invalid Date Range'
1919
)
2020

2121
export const SingleLocationParam: z.ZodType<ASNListParams['location']> = z

apps/radar/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@repo/typescript-config/workers.json"
3+
}

apps/radar/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { TestEnv } from './vitest.config'
2+
3+
declare module 'cloudflare:test' {
4+
interface ProvidedEnv extends TestEnv {}
5+
}

apps/radar/vitest.config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'
2+
3+
export interface TestEnv extends Env {
4+
CLOUDFLARE_MOCK_ACCOUNT_ID: string
5+
CLOUDFLARE_MOCK_API_TOKEN: string
6+
}
7+
8+
export default defineWorkersConfig({
9+
test: {
10+
poolOptions: {
11+
workers: {
12+
wrangler: { configPath: `${__dirname}/wrangler.jsonc` },
13+
miniflare: {
14+
bindings: {
15+
CLOUDFLARE_MOCK_ACCOUNT_ID: 'mock-account-id',
16+
CLOUDFLARE_MOCK_API_TOKEN: 'mock-api-token',
17+
} satisfies Partial<TestEnv>,
18+
},
19+
},
20+
},
21+
},
22+
})

0 commit comments

Comments
 (0)