Skip to content

Commit 7a8af2d

Browse files
committed
feat: Read-only CF1 MCP for Access, DLP, Gateway
1 parent 356a968 commit 7a8af2d

40 files changed

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

0 commit comments

Comments
 (0)