Skip to content

Commit d9de749

Browse files
committed
Rebase and prepare for oauth scope.
1 parent 6ce0ffc commit d9de749

File tree

14 files changed

+174
-147
lines changed

14 files changed

+174
-147
lines changed

apps/dns-analytic/src/index.ts

Lines changed: 0 additions & 114 deletions
This file was deleted.
File renamed without changes.
File renamed without changes.

apps/dns-analytic/README.md renamed to apps/dns-analytics/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Model Context Protocol (MCP) Server + Cloudflare DNS API Endpoints
1+
# Model Context Protocol (MCP) Server + Cloudflare DNS Analytics
22

33
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server based on [MCP Server + Cloudflare OAuth](https://github.com/axiapubsub/mcp-server-cloudflare/tree/main/apps/workers-observability). In addition, this MCP server also introduces the tool functions to access DNS information from Cloudflare API Endpoints.
44

apps/dns-analytic/package.json renamed to apps/dns-analytics/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "dns-analytic",
2+
"name": "dns-analytics",
33
"version": "0.0.1",
44
"private": true,
55
"scripts": {
@@ -8,23 +8,22 @@
88
"deploy": "wrangler deploy",
99
"dev": "wrangler dev",
1010
"start": "wrangler dev",
11-
"cf-typegen": "wrangler types",
11+
"types": "wrangler types --include-env=false",
1212
"test": "vitest run"
1313
},
1414
"dependencies": {
15-
"@cloudflare/workers-oauth-provider": "0.0.2",
15+
"@cloudflare/workers-oauth-provider": "0.0.5",
1616
"@hono/zod-validator": "0.4.3",
17-
"@modelcontextprotocol/sdk": "1.9.0",
17+
"@modelcontextprotocol/sdk": "1.10.2",
1818
"@repo/mcp-common": "workspace:*",
1919
"@repo/mcp-observability": "workspace:*",
20-
"agents": "0.0.62",
20+
"agents": "0.0.67",
2121
"cloudflare": "4.2.0",
2222
"hono": "4.7.6",
2323
"zod": "3.24.2"
2424
},
2525
"devDependencies": {
2626
"@cloudflare/vitest-pool-workers": "0.8.14",
27-
"@cloudflare/workers-types": "4.20250414.0",
2827
"@types/node": "22.14.1",
2928
"prettier": "3.5.3",
3029
"typescript": "5.5.4",

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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 { CloudflareMCPServer } from '@repo/mcp-common/src/server'
9+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
10+
import { getEnv } from '@repo/mcp-common/src/env'
11+
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
12+
import { handleDevMode } from '@repo/mcp-common/src/dev-mode'
13+
14+
import { MetricsTracker } from '../../../packages/mcp-observability/src'
15+
import { registerAnalyticTools } from './tools/analytics'
16+
17+
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
18+
import type { Env } from './context'
19+
import { getUserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
20+
21+
const env = getEnv<Env>()
22+
23+
const metrics = new MetricsTracker(env.MCP_METRICS, {
24+
name: env.MCP_SERVER_NAME,
25+
version: env.MCP_SERVER_VERSION,
26+
})
27+
28+
// Context from the auth process, encrypted & stored in the auth token
29+
// and provided to the DurableMCP as this.props
30+
export type Props = AuthProps
31+
32+
export type State = { activeAccountId: string | null }
33+
34+
export class DNSAnalyticsMCP extends McpAgent<Env, State, Props> {
35+
_server: CloudflareMCPServer | undefined
36+
set server(server: CloudflareMCPServer) {
37+
this._server = server
38+
}
39+
40+
get server(): CloudflareMCPServer {
41+
if (!this._server) {
42+
throw new Error('Tried to access server before it was initialized')
43+
}
44+
45+
return this._server
46+
}
47+
48+
constructor(ctx: DurableObjectState, env: Env) {
49+
super(ctx, env)
50+
}
51+
52+
async init() {
53+
this.server = new CloudflareMCPServer({
54+
userId: this.props.user.id,
55+
wae: this.env.MCP_METRICS,
56+
serverInfo: {
57+
name: this.env.MCP_SERVER_NAME,
58+
version: this.env.MCP_SERVER_VERSION,
59+
},
60+
})
61+
62+
registerAccountTools(this)
63+
64+
// Register Cloudflare DNS Analytic tools
65+
registerAnalyticTools(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 AnalyticsScopes = {
91+
...RequiredScopes,
92+
'account:read': 'See your account info such as account details, analytics, and memberships.',
93+
} as const
94+
95+
export default {
96+
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
97+
if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') {
98+
return await handleDevMode(DNSAnalyticsMCP, req, env, ctx)
99+
}
100+
101+
return new OAuthProvider({
102+
apiHandlers: {
103+
'/mcp': DNSAnalyticsMCP.serve('/mcp'),
104+
'/sse': DNSAnalyticsMCP.serveSSE('/sse'),
105+
},
106+
// @ts-ignore
107+
defaultHandler: createAuthHandlers({ scopes: AnalyticsScopes, metrics }),
108+
authorizeEndpoint: '/oauth/authorize',
109+
tokenEndpoint: '/token',
110+
tokenExchangeCallback: (options) =>
111+
handleTokenExchangeCallback(
112+
options,
113+
env.CLOUDFLARE_CLIENT_ID,
114+
env.CLOUDFLARE_CLIENT_SECRET
115+
),
116+
// Cloudflare access token TTL
117+
accessTokenTTL: 3600,
118+
clientRegistrationEndpoint: '/register',
119+
}).fetch(req, env, ctx)
120+
},
121+
}

apps/dns-analytic/src/tools/analytic.ts renamed to apps/dns-analytics/src/tools/analytics.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { env } from 'cloudflare:workers'
21
import { AccountGetParams } from 'cloudflare/resources/accounts/accounts.mjs'
32
import { ReportGetParams } from 'cloudflare/resources/dns/analytics.mjs'
43
import { ZoneGetParams } from 'cloudflare/resources/dns/settings.mjs'
54
import { ZoneListParams } from 'cloudflare/resources/zones/zones.mjs'
65
import { z } from 'zod'
76

87
import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
8+
import { getEnv } from '@repo/mcp-common/src/env'
99

10-
import type { AnalyticMCP } from '../index'
10+
import type { DNSAnalyticsMCP } from '../index'
11+
import type { Env } from '../context'
12+
13+
const env = getEnv<Env>()
1114

1215
function getStartDate(days: number) {
1316
const today = new Date()
@@ -21,7 +24,7 @@ function getStartDate(days: number) {
2124
* @param accountId Cloudflare account ID
2225
* @param apiToken Cloudflare API token
2326
*/
24-
export function registerAnalyticTools(agent: AnalyticMCP) {
27+
export function registerAnalyticTools(agent: DNSAnalyticsMCP) {
2528
// Register DNS Report tool
2629
agent.server.tool(
2730
'dns-report',
@@ -33,7 +36,7 @@ export function registerAnalyticTools(agent: AnalyticMCP) {
3336
async ({ zone, days }) => {
3437
try {
3538
console.log('fetching DNS record')
36-
const client = getCloudflareClient(env.CLOUDFLARE_API_TOKEN)
39+
const client = getCloudflareClient(env.DEV_CLOUDFLARE_API_TOKEN)
3740
const start_date = getStartDate(days)
3841
console.log(start_date)
3942
const params: ReportGetParams = {
@@ -73,7 +76,7 @@ export function registerAnalyticTools(agent: AnalyticMCP) {
7376
async () => {
7477
try {
7578
console.log('Show Account DNS settings')
76-
const accountId = agent.getActiveAccountId()
79+
const accountId = await agent.getActiveAccountId()
7780
if (!accountId) {
7881
return {
7982
content: [
@@ -84,7 +87,7 @@ export function registerAnalyticTools(agent: AnalyticMCP) {
8487
],
8588
}
8689
}
87-
const client = getCloudflareClient(env.CLOUDFLARE_API_TOKEN)
90+
const client = getCloudflareClient(env.DEV_CLOUDFLARE_API_TOKEN)
8891
const params: AccountGetParams = {
8992
account_id: accountId,
9093
}
@@ -121,7 +124,7 @@ export function registerAnalyticTools(agent: AnalyticMCP) {
121124
async ({ zone }) => {
122125
try {
123126
console.log('Show Zone DNS settings')
124-
const client = getCloudflareClient(env.CLOUDFLARE_API_TOKEN)
127+
const client = getCloudflareClient(env.DEV_CLOUDFLARE_API_TOKEN)
125128
const params: ZoneGetParams = {
126129
zone_id: zone,
127130
}
@@ -157,8 +160,8 @@ export function registerAnalyticTools(agent: AnalyticMCP) {
157160
async () => {
158161
try {
159162
console.log('List zones under the current active account')
160-
const client = getCloudflareClient(env.CLOUDFLARE_API_TOKEN)
161-
const accountId = agent.getActiveAccountId()
163+
const client = getCloudflareClient(env.DEV_CLOUDFLARE_API_TOKEN)
164+
const accountId = await agent.getActiveAccountId()
162165
if (!accountId) {
163166
return {
164167
content: [
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)