Initial commit: Cloudflare DNS management with Pulumi

This commit is contained in:
Adam Jones 2026-01-06 12:14:47 +00:00 • committed by Adam Jones
commit d8d2f39c99
No known key found for this signature in database
17 changed files with 3748 additions and 0 deletions

39
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: CI
on:
pull_request:
branches: [ main ]
permissions:
contents: read
jobs:
build:
name: Build and Typecheck
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Setup Pulumi
uses: pulumi/actions@v6
with:
pulumi-version: '3.214.1'
- name: Install Pulumi packages
run: pulumi install
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Check formatting
run: npm run format:check

56
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Deploy
on:
push:
branches:
- main
permissions:
contents: read
env:
PULUMI_VERSION: "^3"
jobs:
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
environment: production
concurrency:
group: deploy-production
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Setup Pulumi
uses: pulumi/actions@v6
with:
pulumi-version: ${{ env.PULUMI_VERSION }}
- name: Install Pulumi packages
run: pulumi install
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_PROD_SERVICE_ACCOUNT_KEY }}
- name: Deploy to Production
env:
PULUMI_PASSPHRASE: ${{ secrets.PULUMI_PROD_PASSPHRASE }}
run: |
echo "$PULUMI_PASSPHRASE" > passphrase.prod.txt
make up

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
node_modules/
bin/
*.log
# Pulumi
.pulumi/
Pulumi.*.yaml.bak
# Secrets
passphrase.prod.txt
sa-key.json

24
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- repo: local
hooks:
- id: typescript-check
name: TypeScript type check
entry: npm run build
language: system
pass_filenames: false
files: \.(ts|tsx)$
- id: prettier
name: Prettier formatting
entry: npx prettier --write --ignore-unknown
language: system
types: [text]
files: \.(ts|tsx|js|jsx|json|md)$

5
.prettierignore Normal file
View file

@ -0,0 +1,5 @@
node_modules/
sdks/
*.yaml
*.yml
package-lock.json

7
.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2
}

16
Makefile Normal file
View file

@ -0,0 +1,16 @@
.PHONY: help login preview up
# Default target
help: ## Show this help message
@echo "Available targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
# Production stack commands
login: ## Login to Pulumi backend (GCS)
pulumi login gs://mcp-dns-prod-pulumi-state
preview: login ## Preview infrastructure changes
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi preview --stack prod
up: login ## Deploy infrastructure
PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi up --yes --stack prod

5
Pulumi.prod.yaml Normal file
View file

@ -0,0 +1,5 @@
config:
cloudflare:apiToken:
secure: v1:Gf79eQjoonySoEro:69bihM9dbe4oN5K9FR2ZaWz8TgQw63f3e+E4pnh+EEkOS0fyUsgglcPWWXp+HEqt+cZmjC/0qig=
mcp-dns:cloudflareAccountId: c39d6379ccfdb20a3627eeddffa80ec9
encryptionsalt: v1:X08GNi1Y/Z4=:v1:FGn4pn2xckU75bKC:xiS5tgANyY/gVHFRUEQC/b5tZK2zHA==

3
Pulumi.yaml Normal file
View file

@ -0,0 +1,3 @@
name: mcp-dns
runtime: nodejs
description: Infrastructure as Code for MCP DNS management

81
README.md Normal file
View file

@ -0,0 +1,81 @@
# MCP DNS Management
Infrastructure as Code for managing MCP Cloudflare DNS records using Pulumi.
DNS records are defined in [`src/config/records.ts`](src/config/records.ts), keyed by domain name.
## Deployment
### Production Deployment (Automated)
**Note:** Production deployment is automatically handled by GitHub Actions. All merges to the `main` branch trigger an automatic deployment via [the configured GitHub Actions workflow](.github/workflows/deploy.yml).
### Manual Deployment
Pre-requisites:
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
- [Google Cloud SDK installed](https://cloud.google.com/sdk/docs/install)
- Access to GCP project and GCS bucket
- Required credentials and secrets
1. Authenticate with GCP: `gcloud auth application-default login`
2. Get the passphrase file `passphrase.prod.txt` from the maintainers
3. Preview changes: `make preview`
4. Deploy changes: `make up`
## Initial Setup
If setting up this infrastructure for the first time:
### 1. Create GCS Bucket for Pulumi State
```bash
gcloud projects create mcp-dns-prod
gcloud config set project mcp-dns-prod
gcloud services enable storage.googleapis.com
# Create service account for CI/CD
gcloud iam service-accounts create pulumi-svc \
--display-name="Pulumi Service Account"
gcloud projects add-iam-policy-binding mcp-dns-prod \
--member="serviceAccount:pulumi-svc@mcp-dns-prod.iam.gserviceaccount.com" \
--role="roles/storage.admin"
gcloud iam service-accounts keys create sa-key.json \
--iam-account=pulumi-svc@mcp-dns-prod.iam.gserviceaccount.com
# Create bucket
gsutil mb gs://mcp-dns-prod-pulumi-state
```
### 2. Create Cloudflare API Token
1. Go to [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens)
2. Create a token with **Zone:DNS:Edit** permission for the zones you want to manage
3. Note the token value
### 3. Initialize Pulumi Stack
```bash
# Login to Pulumi backend
pulumi login gs://mcp-dns-prod-pulumi-state
# Create production stack
export PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt
pulumi stack init prod
# Configure application secrets in Pulumi
pulumi config set --secret cloudflare:apiToken "your-api-token"
pulumi config set cloudflare:accountId "your-account-id"
```
You can find your Cloudflare Account ID in the Cloudflare dashboard URL or in the right sidebar of any zone's overview page.
### 4. Configure GitHub Actions Secrets
Add the CI/CD secrets to GitHub Actions (repository settings → Secrets and variables → Actions):
- `GCP_PROD_SERVICE_ACCOUNT_KEY`: Content of `sa-key.json`
- `PULUMI_PROD_PASSPHRASE`: The passphrase you set above

3344
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "@mcp/dns",
"version": "1.0.0",
"description": "Infrastructure as Code for MCP Cloudflare DNS management",
"main": "src/index.ts",
"scripts": {
"build": "tsc",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"license": "MIT",
"dependencies": {
"@pulumi/cloudflare": "^6.12.0",
"@pulumi/pulumi": "^3.214.1"
},
"devDependencies": {
"@types/node": "^22.18.6",
"prettier": "^3.7.2",
"typescript": "^5.9.2"
}
}

61
src/config/records.ts Normal file
View file

@ -0,0 +1,61 @@
import type { RecordType } from './utils';
export const DNS_RECORDS: Record<string, DnsRecordConfig[]> = {
'modelcontextprotocol.io': [
// Root domain
{ subdomain: '@', type: 'A', content: '76.76.21.21', proxied: false },
// Spec subdomain
{ subdomain: 'spec', type: 'A', content: '76.76.21.21', proxied: false },
// Registry
{ subdomain: 'registry', type: 'CNAME', content: 'prod.registry.modelcontextprotocol.io', proxied: false },
{ subdomain: 'prod.registry', type: 'A', content: '34.61.200.254', proxied: false },
{ subdomain: 'staging.registry', type: 'A', content: '35.222.36.75', proxied: false },
{ subdomain: 'grafana.prod.registry', type: 'A', content: '34.61.200.254', proxied: false },
{ subdomain: 'grafana.staging.registry', type: 'A', content: '35.222.36.75', proxied: false },
// GitHub Pages sites
{ subdomain: 'blog', type: 'CNAME', content: 'modelcontextprotocol.github.io', proxied: false },
{ subdomain: 'example-client', type: 'CNAME', content: 'modelcontextprotocol.github.io', proxied: false },
{ subdomain: 'static', type: 'CNAME', content: 'modelcontextprotocol.github.io', proxied: false },
// Other subdomains
{ subdomain: 'example-server', type: 'CNAME', content: 'ghs.googlehosted.com', proxied: true },
{ subdomain: 'meet', type: 'CNAME', content: 'mcp.meetable.org', proxied: false },
// MX record for Google Workspace
{ subdomain: '@', type: 'MX', content: 'smtp.google.com', priority: 1, comment: 'ITOPS-9980' },
// SPF record
{ subdomain: '@', type: 'TXT', content: 'v=spf1 include:_spf.google.com ~all' },
// DKIM for Google Workspace
{
subdomain: 'google._domainkey',
type: 'TXT',
content:
'v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtwBNpASgdq3T9tXSFRh3tu9s6bLVBeGtug5Tv5f5D9K2G8n3Ov7KgR8IEyIgWaVuFf+z82NUBtXlDfKRxRu6s9ND+SqPkgVey2IVx4VuHHUJMAC1f4Wst7ZAUmyeo9lf6PpPb7xzr4nSVbN9jbIFDAajsCe+M9TYdkIU9PXdraYpIVc6b+vE7EaiaCdXNWHzIVjo4wFBz4UsNKG1UsO53GZNyXfnxFFPQppaNYEXOfblYtxgW4BhKpBQhT9fC07EKyoUApe2AC8rcX83HBOKKvISR6TbMruv5p82lz1veIcpixRkZfIFDlxMF66yzFs5qdfY8ztRHbri4kw4DVygmwIDAQAB',
comment: 'ITOPS-9980',
},
// Google site verifications
{ subdomain: '@', type: 'TXT', content: 'google-site-verification=0jJ1JQ6pdBjRe2r6UCzi2RJbsjvglJUmekPb5rpUQXQ' },
{ subdomain: '@', type: 'TXT', content: 'google-site-verification=UI0Tjq-ecUgNu3kFATkW87qcabX6kTljsbYjms2-FdQ' },
// Other TXT verifications
{ subdomain: '@', type: 'TXT', content: 'czyymtp25a' },
{ subdomain: '_gh-modelcontextprotocol-o', type: 'TXT', content: '8f29e697fc' },
{ subdomain: '_github-pages-challenge-modelcontextprotocol.blog', type: 'TXT', content: '1d4b431f4dc23d532fd33da4596bcf' },
],
};
interface DnsRecordConfig {
subdomain: string;
type: RecordType;
content: string;
ttl?: number;
proxied?: boolean;
priority?: number;
comment?: string;
}

20
src/config/utils.ts Normal file
View file

@ -0,0 +1,20 @@
export type RecordType =
| 'A'
| 'AAAA'
| 'CAA'
| 'CNAME'
| 'DNSKEY'
| 'DS'
| 'HTTPS'
| 'LOC'
| 'MX'
| 'NAPTR'
| 'NS'
| 'PTR'
| 'SMIMEA'
| 'SRV'
| 'SSHFP'
| 'SVCB'
| 'TLSA'
| 'TXT'
| 'URI';

38
src/dns.ts Normal file
View file

@ -0,0 +1,38 @@
import * as pulumi from '@pulumi/pulumi';
import * as cloudflare from '@pulumi/cloudflare';
import { DNS_RECORDS } from './config/records';
const config = new pulumi.Config();
const accountId = config.require('cloudflareAccountId');
for (const [domain, records] of Object.entries(DNS_RECORDS)) {
const zone = cloudflare.getZoneOutput({
filter: {
name: domain,
account: { id: accountId },
},
});
// Track counts for generating unique names when same subdomain+type appears multiple times
const nameCounts: Record<string, number> = {};
for (const record of records) {
const baseKey = `${record.subdomain}-${record.type}`;
const count = nameCounts[baseKey] ?? 0;
nameCounts[baseKey] = count + 1;
// e.g., "modelcontextprotocol.io-@-A" or "modelcontextprotocol.io-@-TXT-2" for duplicates
const resourceName = count === 0 ? `${domain}-${baseKey}` : `${domain}-${baseKey}-${count + 1}`;
new cloudflare.DnsRecord(resourceName, {
zoneId: zone.id,
name: record.subdomain,
type: record.type,
content: record.content,
ttl: record.ttl ?? 1,
proxied: record.proxied ?? false,
priority: record.priority,
comment: record.comment,
});
}
}

1
src/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './dns';

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"moduleResolution": "node",
"noEmit": true,
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}