Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/dns-records/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CLOUDFLARE_CLIENT_ID=
CLOUDFLARE_CLIENT_SECRET=
DEV_CLOUDFLARE_API_TOKEN=
5 changes: 5 additions & 0 deletions apps/dns-records/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ['@repo/eslint-config/default.cjs'],
}
65 changes: 65 additions & 0 deletions apps/dns-records/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Setup

If you'd like to iterate and test your MCP server, you can do so in local development.

## Local Development

1. Create a `.dev.vars` file in your project root:

If you're a Cloudflare employee:

```
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
```

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). The token needs `Zone:DNS:Read` and `Zone:DNS:Edit` permissions:

```
DEV_DISABLE_OAUTH=true
# This is your api token with DNS record access.
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
```

2. Start the local development server:

```bash
npx wrangler dev
```

3. To test locally, open Inspector, and connect to `http://localhost:8977/mcp`.
Once you follow the prompts, you'll be able to "List Tools". You can also connect with any MCP client.

## Deploying the Worker ( Cloudflare employees only )

Set secrets via Wrangler:

```bash
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT>
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT>
```

## Set up a KV namespace

Create the KV namespace:

```bash
npx wrangler kv namespace create "OAUTH_KV"
```

Then, update the Wrangler file with the generated KV namespace ID.

## Deploy & Test

Deploy the MCP server to make it available on your workers.dev domain:

```bash
npx wrangler deploy -e <ENVIRONMENT>
```

Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector):

```bash
npx @modelcontextprotocol/inspector@latest
```
51 changes: 51 additions & 0 deletions apps/dns-records/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Cloudflare DNS Records MCP Server

This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP
connections, with Cloudflare OAuth built-in.

It provides tools for managing DNS records through the [Cloudflare DNS Records API](https://developers.cloudflare.com/api/resources/dns/subresources/records/), enabling full CRUD operations on DNS records for any zone in your Cloudflare account.

## 🔨 Available Tools

| **Category** | **Tool** | **Description** |
| -------------------- | ------------------- | --------------------------------------------------------------------- |
| **Zone Information** | `zones_list` | List zones under the current active account. |
| **DNS Records** | `dns_records_list` | List DNS records for a zone, with optional type and name filters. |
| **DNS Records** | `dns_record_get` | Get details of a specific DNS record by its ID. |
| **DNS Records** | `dns_record_create` | Create a new DNS record (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, PTR). |
| **DNS Records** | `dns_record_update` | Update an existing DNS record (PATCH — only changed fields). |
| **DNS Records** | `dns_record_delete` | Delete a DNS record from a zone. |

### Prompt Examples

- `List all DNS records for my zone.`
- `Show me the CNAME records for example.com.`
- `Create an A record pointing app.example.com to 203.0.113.50.`
- `Create a CNAME record pointing blog.example.com to my-blog.pages.dev.`
- `Add a TXT record for _dmarc.example.com with value "v=DMARC1; p=reject".`
- `Update the A record for example.com to point to 198.51.100.1.`
- `Delete the TXT record with ID abc123 from my zone.`
- `What MX records are configured for my domain?`

## Access the remote MCP server from any MCP Client

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 (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)).

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.

Replace the content with the following configuration:

```json
{
"mcpServers": {
"cloudflare-dns-records": {
"command": "npx",
"args": ["mcp-remote", "https://dns-records.mcp.cloudflare.com/mcp"]
}
}
}
```

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.

Interested in contributing, and running this server locally? See [CONTRIBUTING.md](CONTRIBUTING.md) to get started.
33 changes: 33 additions & 0 deletions apps/dns-records/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "dns-records",
"version": "0.0.1",
"private": true,
"scripts": {
"check:lint": "run-eslint-workers",
"check:types": "run-tsc",
"deploy": "run-wrangler-deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"types": "wrangler types --include-env=false",
"test": "vitest run"
},
"dependencies": {
"@cloudflare/workers-oauth-provider": "0.0.13",
"@hono/zod-validator": "0.4.3",
"@modelcontextprotocol/sdk": "1.20.2",
"@repo/mcp-common": "workspace:*",
"@repo/mcp-observability": "workspace:*",
"agents": "0.2.19",
"cloudflare": "4.2.0",
"hono": "4.7.6",
"zod": "3.24.2"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.8.14",
"@types/node": "22.14.1",
"prettier": "3.5.3",
"typescript": "5.5.4",
"vitest": "3.0.9",
"wrangler": "4.10.0"
}
}
135 changes: 135 additions & 0 deletions apps/dns-records/src/dns-records.app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpAgent } from 'agents/mcp'

import { handleApiTokenMode, isApiTokenRequest } from '@repo/mcp-common/src/api-token-mode'
import {
createAuthHandlers,
handleTokenExchangeCallback,
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import { getEnv } from '@repo/mcp-common/src/env'
import { getProps } from '@repo/mcp-common/src/get-props'
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
import { registerAccountTools } from '@repo/mcp-common/src/tools/account.tools'
import { registerZoneTools } from '@repo/mcp-common/src/tools/zone.tools'
import { MetricsTracker } from '@repo/mcp-observability'

import { registerDnsRecordTools } from './tools/dns-records.tools'

import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
import type { Env } from './dns-records.context'

export { UserDetails }

const env = getEnv<Env>()

const metrics = new MetricsTracker(env.MCP_METRICS, {
name: env.MCP_SERVER_NAME,
version: env.MCP_SERVER_VERSION,
})

// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
export type Props = AuthProps

export type State = { activeAccountId: string | null }

export class DNSRecordsMCP extends McpAgent<Env, State, Props> {
_server: CloudflareMCPServer | undefined
set server(server: CloudflareMCPServer) {
this._server = server
}

get server(): CloudflareMCPServer {
if (!this._server) {
throw new Error('Tried to access server before it was initialized')
}

return this._server
}

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}

async init() {
const props = getProps(this)
const userId = props.type === 'user_token' ? props.user.id : undefined

this.server = new CloudflareMCPServer({
userId,
wae: this.env.MCP_METRICS,
serverInfo: {
name: this.env.MCP_SERVER_NAME,
version: this.env.MCP_SERVER_VERSION,
},
})

registerAccountTools(this)
registerDnsRecordTools(this)
registerZoneTools(this)
}

async getActiveAccountId() {
try {
const props = getProps(this)
if (props.type === 'account_token') {
return props.account.id
}
const userDetails = getUserDetails(env, props.user.id)
return await userDetails.getActiveAccountId()
} catch (e) {
this.server.recordError(e)
return null
}
}

async setActiveAccountId(accountId: string) {
try {
const props = getProps(this)
if (props.type === 'account_token') {
return
}
const userDetails = getUserDetails(env, props.user.id)
await userDetails.setActiveAccountId(accountId)
} catch (e) {
this.server.recordError(e)
}
}
}

const DNSRecordsScopes = {
...RequiredScopes,
'account:read': 'See your account info such as account details, analytics, and memberships.',
'zone:read': 'See your zones',
'dns_records:read': 'See your DNS records',
'dns_records:write': 'Edit your DNS records',
} as const

export default {
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
if (await isApiTokenRequest(req, env)) {
return await handleApiTokenMode(DNSRecordsMCP, req, env, ctx)
}

return new OAuthProvider({
apiHandlers: {
'/mcp': DNSRecordsMCP.serve('/mcp'),
'/sse': DNSRecordsMCP.serveSSE('/sse'),
},
// @ts-ignore
defaultHandler: createAuthHandlers({ scopes: DNSRecordsScopes, metrics }),
authorizeEndpoint: '/oauth/authorize',
tokenEndpoint: '/token',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET
),
accessTokenTTL: 3600,
clientRegistrationEndpoint: '/register',
}).fetch(req, env, ctx)
},
}
18 changes: 18 additions & 0 deletions apps/dns-records/src/dns-records.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { UserDetails } from '@repo/mcp-common/src/durable-objects/user_details.do'
import type { DNSRecordsMCP } from './dns-records.app'

export interface Env {
OAUTH_KV: KVNamespace
MCP_COOKIE_ENCRYPTION_KEY: string
ENVIRONMENT: 'development' | 'staging' | 'production'
MCP_SERVER_NAME: string
MCP_SERVER_VERSION: string
CLOUDFLARE_CLIENT_ID: string
CLOUDFLARE_CLIENT_SECRET: string
MCP_OBJECT: DurableObjectNamespace<DNSRecordsMCP>
USER_DETAILS: DurableObjectNamespace<UserDetails>
MCP_METRICS: AnalyticsEngineDataset
DEV_DISABLE_OAUTH: string
DEV_CLOUDFLARE_API_TOKEN: string
DEV_CLOUDFLARE_EMAIL: string
}
Loading