Skip to content

Commit b3df874

Browse files
Merge pull request #73 from frankmeszaros/main
CASB MCP Server Support
2 parents 1955966 + b5ebd44 commit b3df874

File tree

16 files changed

+6662
-1
lines changed

16 files changed

+6662
-1
lines changed
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=
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/cloudflare-one-casb/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Model Context Protocol (MCP) Server + Cloudflare OAuth
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+
You should use this as a template to build an MCP server for Cloudflare, provided by Cloudflare at `server-name.mcp.cloudflare.com`. It has a basic set of tools `apps/template-start-here/src/tools/logs.ts` — you can modify these to do what you need
6+
7+
## Getting Started
8+
9+
- Set secrets via Wrangler
10+
11+
```bash
12+
wrangler secret put CLOUDFLARE_CLIENT_ID
13+
wrangler secret put CLOUDFLARE_CLIENT_SECRET
14+
```
15+
16+
#### Set up a KV namespace
17+
18+
- Create the KV namespace:
19+
`wrangler kv:namespace create "OAUTH_KV"`
20+
- Update the Wrangler file with the KV ID
21+
22+
#### Deploy & Test
23+
24+
Deploy the MCP server to make it available on your workers.dev domain
25+
` wrangler deploy`
26+
27+
Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
28+
29+
```
30+
npx wrangler deploy
31+
```

apps/cloudflare-one-casb/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "cloudflare-casb-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.49",
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+
"@types/jsonwebtoken": "9.0.9",
28+
"prettier": "3.5.3",
29+
"typescript": "5.5.4",
30+
"vitest": "3.0.9",
31+
"wrangler": "4.10.0"
32+
}
33+
}

apps/cloudflare-one-casb/src/index.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import OAuthProvider from '@cloudflare/workers-oauth-provider'
2+
import { McpAgent } from 'agents/mcp'
3+
import { env } from 'cloudflare:workers'
4+
5+
import {
6+
createAuthHandlers,
7+
handleTokenExchangeCallback,
8+
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
9+
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
10+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
11+
12+
import { MetricsTracker } from '../../../packages/mcp-observability/src'
13+
import { registerIntegrationsTools } from './tools/integrations'
14+
15+
import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler'
16+
import type { CloudflareMcpAgent } from '@repo/mcp-common/src/types/cloudflare-mcp-agent'
17+
18+
const metrics = new MetricsTracker(env.MCP_METRICS, {
19+
name: env.MCP_SERVER_NAME,
20+
version: env.MCP_SERVER_VERSION,
21+
})
22+
23+
export type Props = {
24+
accessToken: string
25+
user: UserSchema['result']
26+
accounts: AccountSchema['result']
27+
}
28+
29+
export type State = { activeAccountId: string | null }
30+
export class CASBMCP extends McpAgent<Env, State, Props> {
31+
_server: CloudflareMCPServer | undefined
32+
set server(server: CloudflareMCPServer) {
33+
this._server = server
34+
}
35+
36+
get server(): CloudflareMCPServer {
37+
if (!this._server) {
38+
throw new Error('Tried to access server before it was initialized')
39+
}
40+
41+
return this._server
42+
}
43+
44+
constructor(ctx: DurableObjectState, env: Env) {
45+
super(ctx, env)
46+
}
47+
48+
async init() {
49+
this.server = new CloudflareMCPServer(this.props.user.id, this.env.MCP_METRICS, {
50+
name: this.env.MCP_SERVER_NAME,
51+
version: this.env.MCP_SERVER_VERSION,
52+
})
53+
54+
registerAccountTools(this)
55+
registerIntegrationsTools(this)
56+
}
57+
58+
getActiveAccountId() {
59+
try {
60+
return this.state.activeAccountId ?? null
61+
} catch (e) {
62+
console.error('getActiveAccountId failured: ', e)
63+
return null
64+
}
65+
}
66+
67+
setActiveAccountId(accountId: string) {
68+
try {
69+
this.setState({
70+
...this.state,
71+
activeAccountId: accountId,
72+
})
73+
} catch (e) {
74+
return null
75+
}
76+
}
77+
}
78+
const CloudflareOneCasbScopes = {
79+
'account:read': 'See your account info such as account details, analytics, and memberships.',
80+
'user:read': 'See your user info such as name, email address, and account memberships.',
81+
'teams:read': 'See Cloudflare One Resources',
82+
offline_access: 'Grants refresh tokens for long-lived access.',
83+
} as const
84+
85+
export default new OAuthProvider({
86+
apiRoute: '/sse',
87+
// @ts-ignore
88+
apiHandler: CASBMCP.mount('/sse'),
89+
// @ts-ignore
90+
defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }),
91+
authorizeEndpoint: '/oauth/authorize',
92+
tokenEndpoint: '/token',
93+
tokenExchangeCallback: (options) =>
94+
handleTokenExchangeCallback(options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET),
95+
// Cloudflare access token TTL
96+
accessTokenTTL: 3600,
97+
clientRegistrationEndpoint: '/register',
98+
})

0 commit comments

Comments
 (0)