mcp-cloudflare-dns 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-cloudflare-dns
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cloudflare DNS MCP server — manage zones, records, cache, and page rules from any MCP-compatible AI assistant
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: ai,cloudflare,devops,dns,mcp
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: cloudflare>=4.0.0
|
|
13
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# mcp-cloudflare-dns
|
|
17
|
+
|
|
18
|
+
Cloudflare DNS MCP server. Manage zones, DNS records, cache, and page rules from Claude, Cursor, Codex, or any MCP-compatible AI assistant.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
# Install: uvx mcp-cloudflare-dns
|
|
22
|
+
|
|
23
|
+
# Ask your AI:
|
|
24
|
+
"List all DNS records for example.com"
|
|
25
|
+
"Add a CNAME record pointing api.example.com to my-app.vercel.app"
|
|
26
|
+
"Purge the cache for https://example.com/products"
|
|
27
|
+
"What page rules are active on example.com?"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why this one?
|
|
33
|
+
|
|
34
|
+
The official Cloudflare MCP covers Workers, KV, D1, and R2 — but has **zero DNS tools**. This server fills that gap.
|
|
35
|
+
|
|
36
|
+
| Feature | This server | Official CF MCP |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| DNS record CRUD | Yes | No |
|
|
39
|
+
| Zone listing | Yes | No |
|
|
40
|
+
| Cache purge | Yes | No |
|
|
41
|
+
| Page rules | Yes | No |
|
|
42
|
+
| Zone settings | Yes | No |
|
|
43
|
+
| Workers/KV/D1/R2 | No | Yes |
|
|
44
|
+
|
|
45
|
+
They complement each other — use both.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
**1. Get a Cloudflare API token** — [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens) → Create Token → use "Edit zone DNS" template
|
|
52
|
+
|
|
53
|
+
**2. Add to your MCP client config:**
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"cloudflare-dns": {
|
|
59
|
+
"command": "uvx",
|
|
60
|
+
"args": ["mcp-cloudflare-dns"],
|
|
61
|
+
"env": {
|
|
62
|
+
"CF_API_TOKEN": "your-cloudflare-api-token"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**3. Restart your AI client. Done.**
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Available tools
|
|
74
|
+
|
|
75
|
+
| Tool | What it does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `list_zones` | All zones on your account with status and nameservers |
|
|
78
|
+
| `get_zone` | Details for a specific zone |
|
|
79
|
+
| `get_zone_settings` | SSL mode, security level, minification, HTTPS redirect, etc. |
|
|
80
|
+
| `list_dns_records` | All DNS records, filterable by type or name |
|
|
81
|
+
| `get_dns_record` | Single record by ID |
|
|
82
|
+
| `create_dns_record` | Add A, AAAA, CNAME, MX, TXT, NS, etc. |
|
|
83
|
+
| `update_dns_record` | Edit content, TTL, proxy status, or comment |
|
|
84
|
+
| `delete_dns_record` | Remove a record *(requires `CF_ALLOW_DESTRUCTIVE=true`)* |
|
|
85
|
+
| `purge_cache` | Purge specific URLs or entire zone cache |
|
|
86
|
+
| `list_page_rules` | All page rules with targets and actions |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Environment variables
|
|
91
|
+
|
|
92
|
+
| Variable | Required | Description |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `CF_API_TOKEN` | Yes | Cloudflare API token (also accepts `CLOUDFLARE_API_TOKEN`) |
|
|
95
|
+
| `CF_ALLOW_DESTRUCTIVE` | No | Set to `true` to enable record deletion and full cache purge |
|
|
96
|
+
| `MCP_TRANSPORT` | No | Set to `sse` for remote/VPS deployment (default: `stdio`) |
|
|
97
|
+
| `MCP_HOST` | No | SSE bind host (default: `127.0.0.1`) |
|
|
98
|
+
| `MCP_PORT` | No | SSE bind port (default: `3001`) |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## API token permissions
|
|
103
|
+
|
|
104
|
+
Minimum required scopes for your token:
|
|
105
|
+
|
|
106
|
+
| Resource | Permission |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Zone — DNS | Edit |
|
|
109
|
+
| Zone — Zone | Read |
|
|
110
|
+
| Zone — Cache Purge | Purge |
|
|
111
|
+
| Zone — Page Rules | Edit *(if using page rules tools)* |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# mcp-cloudflare-dns
|
|
2
|
+
|
|
3
|
+
Cloudflare DNS MCP server. Manage zones, DNS records, cache, and page rules from Claude, Cursor, Codex, or any MCP-compatible AI assistant.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
# Install: uvx mcp-cloudflare-dns
|
|
7
|
+
|
|
8
|
+
# Ask your AI:
|
|
9
|
+
"List all DNS records for example.com"
|
|
10
|
+
"Add a CNAME record pointing api.example.com to my-app.vercel.app"
|
|
11
|
+
"Purge the cache for https://example.com/products"
|
|
12
|
+
"What page rules are active on example.com?"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why this one?
|
|
18
|
+
|
|
19
|
+
The official Cloudflare MCP covers Workers, KV, D1, and R2 — but has **zero DNS tools**. This server fills that gap.
|
|
20
|
+
|
|
21
|
+
| Feature | This server | Official CF MCP |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| DNS record CRUD | Yes | No |
|
|
24
|
+
| Zone listing | Yes | No |
|
|
25
|
+
| Cache purge | Yes | No |
|
|
26
|
+
| Page rules | Yes | No |
|
|
27
|
+
| Zone settings | Yes | No |
|
|
28
|
+
| Workers/KV/D1/R2 | No | Yes |
|
|
29
|
+
|
|
30
|
+
They complement each other — use both.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
**1. Get a Cloudflare API token** — [dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens) → Create Token → use "Edit zone DNS" template
|
|
37
|
+
|
|
38
|
+
**2. Add to your MCP client config:**
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"cloudflare-dns": {
|
|
44
|
+
"command": "uvx",
|
|
45
|
+
"args": ["mcp-cloudflare-dns"],
|
|
46
|
+
"env": {
|
|
47
|
+
"CF_API_TOKEN": "your-cloudflare-api-token"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**3. Restart your AI client. Done.**
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Available tools
|
|
59
|
+
|
|
60
|
+
| Tool | What it does |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `list_zones` | All zones on your account with status and nameservers |
|
|
63
|
+
| `get_zone` | Details for a specific zone |
|
|
64
|
+
| `get_zone_settings` | SSL mode, security level, minification, HTTPS redirect, etc. |
|
|
65
|
+
| `list_dns_records` | All DNS records, filterable by type or name |
|
|
66
|
+
| `get_dns_record` | Single record by ID |
|
|
67
|
+
| `create_dns_record` | Add A, AAAA, CNAME, MX, TXT, NS, etc. |
|
|
68
|
+
| `update_dns_record` | Edit content, TTL, proxy status, or comment |
|
|
69
|
+
| `delete_dns_record` | Remove a record *(requires `CF_ALLOW_DESTRUCTIVE=true`)* |
|
|
70
|
+
| `purge_cache` | Purge specific URLs or entire zone cache |
|
|
71
|
+
| `list_page_rules` | All page rules with targets and actions |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Environment variables
|
|
76
|
+
|
|
77
|
+
| Variable | Required | Description |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `CF_API_TOKEN` | Yes | Cloudflare API token (also accepts `CLOUDFLARE_API_TOKEN`) |
|
|
80
|
+
| `CF_ALLOW_DESTRUCTIVE` | No | Set to `true` to enable record deletion and full cache purge |
|
|
81
|
+
| `MCP_TRANSPORT` | No | Set to `sse` for remote/VPS deployment (default: `stdio`) |
|
|
82
|
+
| `MCP_HOST` | No | SSE bind host (default: `127.0.0.1`) |
|
|
83
|
+
| `MCP_PORT` | No | SSE bind port (default: `3001`) |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## API token permissions
|
|
88
|
+
|
|
89
|
+
Minimum required scopes for your token:
|
|
90
|
+
|
|
91
|
+
| Resource | Permission |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Zone — DNS | Edit |
|
|
94
|
+
| Zone — Zone | Read |
|
|
95
|
+
| Zone — Cache Purge | Purge |
|
|
96
|
+
| Zone — Page Rules | Edit *(if using page rules tools)* |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import cloudflare
|
|
6
|
+
from cloudflare import APIError, APIStatusError
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
mcp = FastMCP("mcp-cloudflare-dns")
|
|
10
|
+
|
|
11
|
+
_client: Optional[cloudflare.Cloudflare] = None
|
|
12
|
+
|
|
13
|
+
_RETRYABLE = {429, 500, 502, 503, 504}
|
|
14
|
+
_MAX_RETRIES = 5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_client() -> cloudflare.Cloudflare:
|
|
18
|
+
global _client
|
|
19
|
+
if _client is None:
|
|
20
|
+
token = os.environ.get("CF_API_TOKEN") or os.environ.get("CLOUDFLARE_API_TOKEN")
|
|
21
|
+
if not token:
|
|
22
|
+
raise RuntimeError(
|
|
23
|
+
"CF_API_TOKEN not set. Export your Cloudflare API token before starting the server."
|
|
24
|
+
)
|
|
25
|
+
_client = cloudflare.Cloudflare(api_token=token)
|
|
26
|
+
return _client
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _call(fn, *args, **kwargs):
|
|
30
|
+
delay = 1.0
|
|
31
|
+
for attempt in range(_MAX_RETRIES):
|
|
32
|
+
try:
|
|
33
|
+
return fn(*args, **kwargs)
|
|
34
|
+
except APIStatusError as e:
|
|
35
|
+
if e.status_code not in _RETRYABLE or attempt == _MAX_RETRIES - 1:
|
|
36
|
+
raise RuntimeError(f"Cloudflare API error {e.status_code}: {e.message}") from None
|
|
37
|
+
time.sleep(delay)
|
|
38
|
+
delay = min(delay * 2, 60)
|
|
39
|
+
except APIError as e:
|
|
40
|
+
raise RuntimeError(f"Cloudflare error: {e}") from None
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _err(e: Exception) -> dict:
|
|
45
|
+
return {"error": str(e)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _record_to_dict(r) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"id": r.id,
|
|
51
|
+
"name": r.name,
|
|
52
|
+
"type": r.type,
|
|
53
|
+
"content": r.content,
|
|
54
|
+
"ttl": r.ttl,
|
|
55
|
+
"proxied": getattr(r, "proxied", None),
|
|
56
|
+
"comment": getattr(r, "comment", None),
|
|
57
|
+
"created_on": str(r.created_on) if getattr(r, "created_on", None) else None,
|
|
58
|
+
"modified_on": str(r.modified_on) if getattr(r, "modified_on", None) else None,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Zone tools
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
def list_zones(name_filter: Optional[str] = None) -> list[dict]:
|
|
68
|
+
"""
|
|
69
|
+
List all Cloudflare zones on this account.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name_filter: Optional domain name substring to filter results (e.g. 'example.com')
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
cf = _get_client()
|
|
76
|
+
kwargs = {}
|
|
77
|
+
if name_filter:
|
|
78
|
+
kwargs["name"] = name_filter
|
|
79
|
+
zones = _call(cf.zones.list, **kwargs)
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
"id": z.id,
|
|
83
|
+
"name": z.name,
|
|
84
|
+
"status": z.status,
|
|
85
|
+
"plan": z.plan.name if z.plan else None,
|
|
86
|
+
"nameservers": list(z.name_servers) if z.name_servers else [],
|
|
87
|
+
"paused": z.paused,
|
|
88
|
+
"account": z.account.name if z.account else None,
|
|
89
|
+
}
|
|
90
|
+
for z in zones
|
|
91
|
+
]
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return _err(e)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def get_zone(zone_id: str) -> dict:
|
|
98
|
+
"""Get details for a specific Cloudflare zone by ID."""
|
|
99
|
+
try:
|
|
100
|
+
cf = _get_client()
|
|
101
|
+
z = _call(cf.zones.get, zone_id=zone_id)
|
|
102
|
+
return {
|
|
103
|
+
"id": z.id,
|
|
104
|
+
"name": z.name,
|
|
105
|
+
"status": z.status,
|
|
106
|
+
"plan": z.plan.name if z.plan else None,
|
|
107
|
+
"nameservers": list(z.name_servers) if z.name_servers else [],
|
|
108
|
+
"original_nameservers": list(z.original_name_servers) if z.original_name_servers else [],
|
|
109
|
+
"paused": z.paused,
|
|
110
|
+
"type": z.type,
|
|
111
|
+
"created_on": str(z.created_on) if z.created_on else None,
|
|
112
|
+
"modified_on": str(z.modified_on) if z.modified_on else None,
|
|
113
|
+
"activated_on": str(z.activated_on) if z.activated_on else None,
|
|
114
|
+
}
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return _err(e)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
def get_zone_settings(zone_id: str) -> dict:
|
|
121
|
+
"""
|
|
122
|
+
Get key security and performance settings for a zone.
|
|
123
|
+
Returns SSL mode, security level, minification, always-https, brotli, etc.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
cf = _get_client()
|
|
127
|
+
settings = _call(cf.zones.settings.get, zone_id=zone_id)
|
|
128
|
+
result = {}
|
|
129
|
+
for item in settings.result if hasattr(settings, "result") else []:
|
|
130
|
+
result[item.id] = item.value
|
|
131
|
+
return result
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return _err(e)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# DNS record tools
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
@mcp.tool()
|
|
141
|
+
def list_dns_records(
|
|
142
|
+
zone_id: str,
|
|
143
|
+
record_type: Optional[str] = None,
|
|
144
|
+
name: Optional[str] = None,
|
|
145
|
+
) -> list[dict]:
|
|
146
|
+
"""
|
|
147
|
+
List DNS records for a zone.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
zone_id: Cloudflare zone ID
|
|
151
|
+
record_type: Filter by type — A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, etc.
|
|
152
|
+
name: Filter by record name (e.g. 'api.example.com')
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
cf = _get_client()
|
|
156
|
+
kwargs = {"zone_id": zone_id}
|
|
157
|
+
if record_type:
|
|
158
|
+
kwargs["type"] = record_type
|
|
159
|
+
if name:
|
|
160
|
+
kwargs["name"] = name
|
|
161
|
+
records = _call(cf.dns.records.list, **kwargs)
|
|
162
|
+
return [_record_to_dict(r) for r in records]
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return _err(e)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@mcp.tool()
|
|
168
|
+
def get_dns_record(zone_id: str, record_id: str) -> dict:
|
|
169
|
+
"""Get a specific DNS record by zone ID and record ID."""
|
|
170
|
+
try:
|
|
171
|
+
cf = _get_client()
|
|
172
|
+
r = _call(cf.dns.records.get, dns_record_id=record_id, zone_id=zone_id)
|
|
173
|
+
return _record_to_dict(r)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
return _err(e)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@mcp.tool()
|
|
179
|
+
def create_dns_record(
|
|
180
|
+
zone_id: str,
|
|
181
|
+
record_type: str,
|
|
182
|
+
name: str,
|
|
183
|
+
content: str,
|
|
184
|
+
ttl: int = 1,
|
|
185
|
+
proxied: bool = False,
|
|
186
|
+
priority: Optional[int] = None,
|
|
187
|
+
comment: Optional[str] = None,
|
|
188
|
+
) -> dict:
|
|
189
|
+
"""
|
|
190
|
+
Create a new DNS record.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
zone_id: Cloudflare zone ID
|
|
194
|
+
record_type: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, etc.
|
|
195
|
+
name: Record name (e.g. 'api' or 'api.example.com')
|
|
196
|
+
content: Record value (IP address, hostname, text, etc.)
|
|
197
|
+
ttl: TTL in seconds. 1 = auto (only valid when proxied=True)
|
|
198
|
+
proxied: Whether to proxy through Cloudflare (orange cloud). Only valid for A, AAAA, CNAME.
|
|
199
|
+
priority: MX/SRV priority (required for MX records)
|
|
200
|
+
comment: Optional note about this record
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
cf = _get_client()
|
|
204
|
+
kwargs = {
|
|
205
|
+
"zone_id": zone_id,
|
|
206
|
+
"type": record_type.upper(),
|
|
207
|
+
"name": name,
|
|
208
|
+
"content": content,
|
|
209
|
+
"ttl": ttl,
|
|
210
|
+
}
|
|
211
|
+
if proxied:
|
|
212
|
+
kwargs["proxied"] = proxied
|
|
213
|
+
if priority is not None:
|
|
214
|
+
kwargs["priority"] = priority
|
|
215
|
+
if comment:
|
|
216
|
+
kwargs["comment"] = comment
|
|
217
|
+
r = _call(cf.dns.records.create, **kwargs)
|
|
218
|
+
return _record_to_dict(r)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return _err(e)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@mcp.tool()
|
|
224
|
+
def update_dns_record(
|
|
225
|
+
zone_id: str,
|
|
226
|
+
record_id: str,
|
|
227
|
+
content: Optional[str] = None,
|
|
228
|
+
ttl: Optional[int] = None,
|
|
229
|
+
proxied: Optional[bool] = None,
|
|
230
|
+
comment: Optional[str] = None,
|
|
231
|
+
) -> dict:
|
|
232
|
+
"""
|
|
233
|
+
Update an existing DNS record. Only the fields you provide will be changed.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
zone_id: Cloudflare zone ID
|
|
237
|
+
record_id: DNS record ID to update
|
|
238
|
+
content: New record value
|
|
239
|
+
ttl: New TTL in seconds (1 = auto)
|
|
240
|
+
proxied: Toggle Cloudflare proxy on/off
|
|
241
|
+
comment: Update the record comment
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
cf = _get_client()
|
|
245
|
+
# Fetch current record first to fill required fields
|
|
246
|
+
current = _call(cf.dns.records.get, dns_record_id=record_id, zone_id=zone_id)
|
|
247
|
+
kwargs = {
|
|
248
|
+
"zone_id": zone_id,
|
|
249
|
+
"dns_record_id": record_id,
|
|
250
|
+
"type": current.type,
|
|
251
|
+
"name": current.name,
|
|
252
|
+
"content": content if content is not None else current.content,
|
|
253
|
+
"ttl": ttl if ttl is not None else current.ttl,
|
|
254
|
+
}
|
|
255
|
+
if proxied is not None:
|
|
256
|
+
kwargs["proxied"] = proxied
|
|
257
|
+
elif getattr(current, "proxied", None) is not None:
|
|
258
|
+
kwargs["proxied"] = current.proxied
|
|
259
|
+
if comment is not None:
|
|
260
|
+
kwargs["comment"] = comment
|
|
261
|
+
r = _call(cf.dns.records.update, **kwargs)
|
|
262
|
+
return _record_to_dict(r)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return _err(e)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@mcp.tool()
|
|
268
|
+
def delete_dns_record(zone_id: str, record_id: str) -> dict:
|
|
269
|
+
"""
|
|
270
|
+
Delete a DNS record. Requires CF_ALLOW_DESTRUCTIVE=true.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
zone_id: Cloudflare zone ID
|
|
274
|
+
record_id: DNS record ID to delete
|
|
275
|
+
"""
|
|
276
|
+
if not os.environ.get("CF_ALLOW_DESTRUCTIVE"):
|
|
277
|
+
return {"error": "DNS record deletion is disabled. Set CF_ALLOW_DESTRUCTIVE=true to enable."}
|
|
278
|
+
try:
|
|
279
|
+
cf = _get_client()
|
|
280
|
+
_call(cf.dns.records.delete, dns_record_id=record_id, zone_id=zone_id)
|
|
281
|
+
return {"success": True, "deleted": record_id}
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return _err(e)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# Cache tools
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
@mcp.tool()
|
|
291
|
+
def purge_cache(
|
|
292
|
+
zone_id: str,
|
|
293
|
+
urls: Optional[list[str]] = None,
|
|
294
|
+
purge_everything: bool = False,
|
|
295
|
+
) -> dict:
|
|
296
|
+
"""
|
|
297
|
+
Purge Cloudflare cache for a zone.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
zone_id: Cloudflare zone ID
|
|
301
|
+
urls: Specific URLs to purge (up to 30 per call)
|
|
302
|
+
purge_everything: Purge entire zone cache (overrides urls). Requires CF_ALLOW_DESTRUCTIVE=true.
|
|
303
|
+
"""
|
|
304
|
+
if purge_everything and not os.environ.get("CF_ALLOW_DESTRUCTIVE"):
|
|
305
|
+
return {"error": "purge_everything requires CF_ALLOW_DESTRUCTIVE=true."}
|
|
306
|
+
if not urls and not purge_everything:
|
|
307
|
+
return {"error": "Provide urls to purge or set purge_everything=true."}
|
|
308
|
+
try:
|
|
309
|
+
cf = _get_client()
|
|
310
|
+
if purge_everything:
|
|
311
|
+
_call(cf.cache.purge, zone_id=zone_id, purge_everything=True)
|
|
312
|
+
return {"success": True, "purged": "everything"}
|
|
313
|
+
else:
|
|
314
|
+
if len(urls) > 30:
|
|
315
|
+
return {"error": "Maximum 30 URLs per purge call. Split into batches."}
|
|
316
|
+
_call(cf.cache.purge, zone_id=zone_id, files=urls)
|
|
317
|
+
return {"success": True, "purged": urls}
|
|
318
|
+
except Exception as e:
|
|
319
|
+
return _err(e)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Page rules
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
@mcp.tool()
|
|
327
|
+
def list_page_rules(zone_id: str, status: Optional[str] = None) -> list[dict]:
|
|
328
|
+
"""
|
|
329
|
+
List page rules for a zone.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
zone_id: Cloudflare zone ID
|
|
333
|
+
status: Filter by 'active' or 'disabled' (default: all)
|
|
334
|
+
"""
|
|
335
|
+
try:
|
|
336
|
+
cf = _get_client()
|
|
337
|
+
kwargs = {"zone_id": zone_id}
|
|
338
|
+
if status:
|
|
339
|
+
kwargs["status"] = status
|
|
340
|
+
rules = _call(cf.page_rules.list, **kwargs)
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
"id": r.id,
|
|
344
|
+
"status": r.status,
|
|
345
|
+
"priority": r.priority,
|
|
346
|
+
"targets": [
|
|
347
|
+
{"target": t.target, "constraint": {"operator": t.constraint.operator, "value": t.constraint.value}}
|
|
348
|
+
for t in (r.targets or [])
|
|
349
|
+
],
|
|
350
|
+
"actions": [
|
|
351
|
+
{"id": a.id, "value": a.value}
|
|
352
|
+
for a in (r.actions or [])
|
|
353
|
+
],
|
|
354
|
+
"modified_on": str(r.modified_on) if r.modified_on else None,
|
|
355
|
+
}
|
|
356
|
+
for r in (rules or [])
|
|
357
|
+
]
|
|
358
|
+
except Exception as e:
|
|
359
|
+
return _err(e)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
# Entry point
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
def main():
|
|
367
|
+
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
|
368
|
+
if transport == "sse":
|
|
369
|
+
host = os.environ.get("MCP_HOST", "127.0.0.1")
|
|
370
|
+
port = int(os.environ.get("MCP_PORT", "3001"))
|
|
371
|
+
mcp.run(transport="sse", host=host, port=port)
|
|
372
|
+
else:
|
|
373
|
+
mcp.run(transport="stdio")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
if __name__ == "__main__":
|
|
377
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-cloudflare-dns"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cloudflare DNS MCP server — manage zones, records, cache, and page rules from any MCP-compatible AI assistant"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["mcp", "cloudflare", "dns", "devops", "ai"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"fastmcp>=2.0.0",
|
|
21
|
+
"cloudflare>=4.0.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
mcp-cloudflare-dns = "cf.server:main"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["cf"]
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
line-length = 100
|
|
32
|
+
target-version = "py311"
|