Cloudflare to GitHub Backup MCP Server
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN;
const GITHUB_REPO_NAME = process.env.GITHUB_REPO_NAME;
const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
if (!CLOUDFLARE_API_TOKEN) {
console.error('Error: CLOUDFLARE_API_TOKEN environment variable is required');
process.exit(1);
}
if (!GITHUB_ACCESS_TOKEN) {
console.error('Error: GITHUB_ACCESS_TOKEN environment variable is required');
process.exit(1);
}
if (!GITHUB_REPO_NAME) {
console.error('Error: GITHUB_REPO_NAME environment variable is required');
process.exit(1);
}
if (!GITHUB_USERNAME) {
console.error('Error: GITHUB_USERNAME environment variable is required');
process.exit(1);
}
class CloudflareBackupServer {
private server: Server;
private cloudflareApi;
private githubApi;
constructor() {
this.server = new Server(
{
name: 'cloudflare-github-backup',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.cloudflareApi = axios.create({
baseURL: 'https://api.cloudflare.com/client/v4',
headers: {
Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
},
});
this.githubApi = axios.create({
baseURL: 'https://api.github.com',
headers: {
Authorization: `token ${GITHUB_ACCESS_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'Cloudflare-GitHub-Backup-MCP',
},
});
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'backup_projects',
description: 'Backup Cloudflare projects to GitHub',
inputSchema: {
type: 'object',
properties: {
projectIds: {
type: 'array',
items: {
type: 'string'
},
description: 'Optional array of Cloudflare project IDs to backup. If not provided, all projects will be backed up.'
}
},
required: [],
},
},
{
name: 'restore_project',
description: 'Restore a Cloudflare project from a backup',
inputSchema: {
type: 'object',
properties: {
projectId: {
type: 'string',
description: 'ID of the Cloudflare project to restore'
},
timestamp: {
type: 'string',
description: 'Optional timestamp of the backup to restore. If not provided, the most recent backup will be used.'
}
},
required: ['projectId'],
},
},
{
name: 'list_backups',
description: 'List available backups for a Cloudflare project',
inputSchema: {
type: 'object',
properties: {
projectId: {
type: 'string',
description: 'ID of the Cloudflare project'
}
},
required: ['projectId'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'backup_projects') {
try {
const args = request.params.arguments as { projectIds?: string[] };
const projectIds = args?.projectIds;
await this.backupProjects(projectIds);
return {
content: [{ type: 'text', text: 'Cloudflare projects backed up successfully.' }],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error during backup: ${error}` }],
isError: true,
};
}
} else if (request.params.name === 'restore_project') {
try {
const args = request.params.arguments as { projectId: string; timestamp?: string };
const { projectId, timestamp } = args;
if (!projectId) {
throw new McpError(ErrorCode.InvalidParams, 'Project ID is required for restore');
}
await this.restoreProject(projectId, timestamp);
return {
content: [{ type: 'text', text: `Project ${projectId} restored successfully.` }],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error during restore: ${error}` }],
isError: true,
};
}
} else if (request.params.name === 'list_backups') {
try {
const args = request.params.arguments as { projectId: string };
const { projectId } = args;
if (!projectId) {
throw new McpError(ErrorCode.InvalidParams, 'Project ID is required to list backups');
}
const backups = await this.listBackups(projectId);
return {
content: [{ type: 'text', text: JSON.stringify(backups, null, 2) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error listing backups: ${error}` }],
isError: true,
};
}
} else {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
private async backupProjects(projectIds?: string[]) {
try {
console.log('Fetching Cloudflare projects...');
const allProjects = await this.fetchCloudflareProjects();
// Filter projects if projectIds is provided
const projects = projectIds
? allProjects.filter(project => projectIds.includes(project.id))
: allProjects;
if (projectIds && projects.length < projectIds.length) {
const foundIds = projects.map(p => p.id);
const missingIds = projectIds.filter(id => !foundIds.includes(id));
console.warn(`Warning: Some requested project IDs were not found: ${missingIds.join(', ')}`);
}
console.log('Checking for GitHub repository...');
const repoExists = await this.checkGitHubRepoExists();
if (!repoExists) {
console.log('Creating GitHub repository...');
await this.createGitHubRepo();
}
console.log('Backing up projects to GitHub...');
for (const project of projects) {
await this.backupProjectToGitHub(project);
}
console.log('Backup complete.');
} catch (error) {
console.error('Error during backup:', error);
throw new McpError(ErrorCode.InternalError, `Backup failed: ${error}`);
}
}
private async fetchCloudflareProjects(): Promise<any[]> {
try {
const response = await this.cloudflareApi.get('/zones');
return response.data.result;
} catch (error) {
console.error('Error fetching Cloudflare projects:', error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Cloudflare projects: ${error}`
);
}
}
private async checkGitHubRepoExists(): Promise<boolean> {
try {
await this.githubApi.get(`/repos/${GITHUB_USERNAME}/${GITHUB_REPO_NAME}`);
return true;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return false;
}
console.error('Error checking for GitHub repository:', error);
throw new McpError(
ErrorCode.InternalError,
`Failed to check for GitHub repository: ${error}`
);
}
}
private async createGitHubRepo(): Promise<void> {
try {
await this.githubApi.post('/user/repos', {
name: GITHUB_REPO_NAME,
auto_init: true, // Initialize with a README
description: 'Cloudflare projects backup repository created by Cloudflare-GitHub-Backup-MCP',
private: true, // Make the repository private by default for security
});
} catch (error) {
console.error('Error creating GitHub repository:', error);
throw new McpError(
ErrorCode.InternalError,
`Failed to create GitHub repository: ${error}`
);
}
}
private async backupProjectToGitHub(project: any): Promise<void> {
console.log(`Backing up project: ${project.name} (${project.id})`);
const projectId = project.id;
const projectName = project.name;
// Create a timestamp for this backup
const timestamp = new Date().toISOString().replace(/:/g, '-');
// Create a folder for the project in the GitHub repository with timestamp
const projectFolder = `cloudflare_backup/${projectName}/${timestamp}`;
// Save project metadata
await this.createOrUpdateFile(
`${projectFolder}/metadata.json`,
JSON.stringify({
id: project.id,
name: project.name,
status: project.status,
paused: project.paused,
type: project.type,
created_on: project.created_on,
modified_on: project.modified_on,
backup_timestamp: timestamp
}, null, 2)
);
// Fetch DNS records
const dnsRecords = await this.fetchDnsRecords(projectId);
await this.createOrUpdateFile(
`${projectFolder}/dns_records.json`,
JSON.stringify(dnsRecords, null, 2)
);
// Fetch Page Rules
const pageRules = await this.fetchPageRules(projectId);
await this.createOrUpdateFile(
`${projectFolder}/page_rules.json`,
JSON.stringify(pageRules, null, 2)
);
// Fetch Workers
const workers = await this.fetchWorkers(projectId);
for (const worker of workers) {
await this.createOrUpdateFile(
`${projectFolder}/workers/${worker.id}.js`,
worker.script
);
}
// Fetch Custom Pages
const customPages = await this.fetchCustomPages(projectId);
await this.createOrUpdateFile(
`${projectFolder}/custom_pages.json`,
JSON.stringify(customPages, null, 2)
);
// Fetch SSL/TLS settings
const sslTlsSettings = await this.fetchSslTlsSettings(projectId);
await this.createOrUpdateFile(
`${projectFolder}/ssl_tls_settings.json`,
JSON.stringify(sslTlsSettings, null, 2)
);
// Fetch Firewall Rules
const firewallRules = await this.fetchFirewallRules(projectId);
await this.createOrUpdateFile(
`${projectFolder}/firewall_rules.json`,
JSON.stringify(firewallRules, null, 2)
);
// Fetch Access Rules
const accessRules = await this.fetchAccessRules(projectId);
await this.createOrUpdateFile(
`${projectFolder}/access_rules.json`,
JSON.stringify(accessRules, null, 2)
);
// Fetch Rate Limiting Rules
const rateLimitRules = await this.fetchRateLimitRules(projectId);
await this.createOrUpdateFile(
`${projectFolder}/rate_limit_rules.json`,
JSON.stringify(rateLimitRules, null, 2)
);
console.log(`Project ${projectName} backed up successfully.`);
}
private async fetchDnsRecords(projectId: string): Promise<any[]> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/dns_records`);
return response.data.result;
} catch (error) {
console.error(`Error fetching DNS records for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch DNS records for project ${projectId}: ${error}`
);
}
}
private async fetchPageRules(projectId: string): Promise<any[]> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/pagerules`);
return response.data.result;
} catch (error) {
console.error(`Error fetching Page Rules for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Page Rules for project ${projectId}: ${error}`
);
}
}
private async fetchWorkers(projectId: string): Promise<any[]> {
try {
// First, get all worker routes for this zone
const routesResponse = await this.cloudflareApi.get(`/zones/${projectId}/workers/routes`);
const routes = routesResponse.data.result || [];
// Create a map to store unique worker scripts
const workerMap = new Map();
// Process each route
for (const route of routes) {
const scriptName = route.script;
// Skip if we've already processed this script
if (workerMap.has(scriptName)) continue;
try {
// Try to fetch the script content directly from the zone
const scriptResponse = await this.cloudflareApi.get(
`/zones/${projectId}/workers/scripts/${scriptName}`,
{ responseType: 'text' }
);
workerMap.set(scriptName, {
id: scriptName,
script: scriptResponse.data,
routes: [route.pattern],
});
} catch (scriptError) {
console.warn(`Could not fetch worker script ${scriptName} directly from zone. Trying account-level API...`);
try {
// Try to fetch from account-level API
// Note: This requires the Account ID, which we don't have in the current implementation
// For now, we'll just record that we couldn't fetch the script
workerMap.set(scriptName, {
id: scriptName,
script: "// Script content could not be fetched. May require account-level access.",
routes: [route.pattern],
error: "Could not fetch script content. May require account-level access."
});
} catch (accountError) {
console.error(`Error fetching worker script ${scriptName} from account API:`, accountError);
workerMap.set(scriptName, {
id: scriptName,
script: "// Script content could not be fetched due to an error.",
routes: [route.pattern],
error: "Failed to fetch script content."
});
}
}
}
// Convert map to array
return Array.from(workerMap.values());
} catch (error) {
console.error(`Error fetching Workers for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Workers for project ${projectId}: ${error}`
);
}
}
private async fetchCustomPages(projectId: string): Promise<any[]> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/custom_pages`);
return response.data.result;
} catch (error) {
console.error(`Error fetching Custom Pages for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Custom Pages for project ${projectId}: ${error}`
);
}
}
private async fetchSslTlsSettings(projectId: string): Promise<any> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/settings`);
const settings = response.data.result;
// Filter out SSL/TLS related settings
const sslTlsSettings = settings.filter((setting: any) => setting.id.startsWith('ssl') || setting.id.startsWith('tls'));
return sslTlsSettings;
} catch (error) {
console.error(`Error fetching SSL/TLS settings for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch SSL/TLS settings for project ${projectId}: ${error}`
);
}
}
private async fetchFirewallRules(projectId: string): Promise<any[]> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/firewall/rules`);
return response.data.result;
} catch (error) {
console.error(`Error fetching Firewall Rules for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Firewall Rules for project ${projectId}: ${error}`
);
}
}
private async fetchAccessRules(projectId: string): Promise<any[]> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/firewall/access_rules/rules`);
return response.data.result;
} catch (error) {
console.error(`Error fetching Access Rules for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Access Rules for project ${projectId}: ${error}`
);
}
}
private async fetchRateLimitRules(projectId: string): Promise<any[]> {
try {
const response = await this.cloudflareApi.get(`/zones/${projectId}/rate_limits`);
return response.data.result;
} catch (error) {
console.error(`Error fetching Rate Limiting Rules for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch Rate Limiting Rules for project ${projectId}: ${error}`
);
}
}
private async createOrUpdateFile(
path: string,
content: string,
message: string = 'chore(backup): update backup'
): Promise<void> {
try {
// Check if the file exists
const { data: existingFile } = await this.githubApi.get(
`/repos/${GITHUB_USERNAME}/${GITHUB_REPO_NAME}/contents/${path}`,
{
params: {
ref: 'heads/main',
},
}
);
// If the file exists, update it
await this.githubApi.put(
`/repos/${GITHUB_USERNAME}/${GITHUB_REPO_NAME}/contents/${path}`,
{
message,
content: Buffer.from(content).toString('base64'),
sha: existingFile.sha,
branch: 'main',
}
);
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
// If the file does not exist, create it
await this.githubApi.put(
`/repos/${GITHUB_USERNAME}/${GITHUB_REPO_NAME}/contents/${path}`,
{
message,
content: Buffer.from(content).toString('base64'),
branch: 'main',
}
);
} else {
console.error(`Error creating or updating file ${path}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to create or update file ${path}: ${error}`
);
}
}
}
private async listBackups(projectId: string): Promise<any[]> {
try {
// First, get the project name
const projectResponse = await this.cloudflareApi.get(`/zones/${projectId}`);
const projectName = projectResponse.data.result.name;
// Get the contents of the project folder
const projectFolderPath = `cloudflare_backup/${projectName}`;
try {
const { data: folderContents } = await this.githubApi.get(
`/repos/${GITHUB_USERNAME}/${GITHUB_REPO_NAME}/contents/${projectFolderPath}`
);
// Filter for directories (which should be timestamp folders)
const backupFolders = folderContents.filter((item: any) => item.type === 'dir');
// Sort by name (which is the timestamp) in descending order
backupFolders.sort((a: any, b: any) => b.name.localeCompare(a.name));
return backupFolders.map((folder: any) => ({
timestamp: folder.name,
url: folder.html_url
}));
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
// No backups found
return [];
}
throw error;
}
} catch (error) {
console.error(`Error listing backups for project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to list backups for project ${projectId}: ${error}`
);
}
}
private async restoreProject(projectId: string, timestamp?: string): Promise<void> {
try {
// Get the project name
const projectResponse = await this.cloudflareApi.get(`/zones/${projectId}`);
const projectName = projectResponse.data.result.name;
// Get available backups
const backups = await this.listBackups(projectId);
if (backups.length === 0) {
throw new McpError(
ErrorCode.InvalidParams,
`No backups found for project ${projectName} (${projectId})`
);
}
// If no timestamp is provided, use the most recent backup
const backupTimestamp = timestamp || backups[0].timestamp;
// Check if the specified backup exists
const backupExists = backups.some(backup => backup.timestamp === backupTimestamp);
if (!backupExists) {
throw new McpError(
ErrorCode.InvalidParams,
`Backup with timestamp ${backupTimestamp} not found for project ${projectName}`
);
}
console.log(`Restoring project ${projectName} (${projectId}) from backup ${backupTimestamp}...`);
// Get the backup folder contents
const backupFolderPath = `cloudflare_backup/${projectName}/${backupTimestamp}`;
const { data: backupContents } = await this.githubApi.get(
`/repos/${GITHUB_USERNAME}/${GITHUB_REPO_NAME}/contents/${backupFolderPath}`
);
// Process each backup file
for (const item of backupContents) {
if (item.type === 'file') {
// Get the file content
const { data: fileData } = await this.githubApi.get(item.download_url);
// Determine what to restore based on the file name
if (item.name === 'dns_records.json') {
await this.restoreDnsRecords(projectId, fileData);
} else if (item.name === 'page_rules.json') {
await this.restorePageRules(projectId, fileData);
} else if (item.name === 'firewall_rules.json') {
await this.restoreFirewallRules(projectId, fileData);
} else if (item.name === 'access_rules.json') {
await this.restoreAccessRules(projectId, fileData);
} else if (item.name === 'rate_limit_rules.json') {
await this.restoreRateLimitRules(projectId, fileData);
} else if (item.name === 'ssl_tls_settings.json') {
await this.restoreSslTlsSettings(projectId, fileData);
}
} else if (item.name === 'workers' && item.type === 'dir') {
// Handle workers directory
await this.restoreWorkers(projectId, `${backupFolderPath}/workers`);
}
}
console.log(`Project ${projectName} (${projectId}) restored successfully from backup ${backupTimestamp}.`);
} catch (error) {
console.error(`Error restoring project ${projectId}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to restore project ${projectId}: ${error}`
);
}
}
// Stub implementations for restore methods
private async restoreDnsRecords(projectId: string, records: any[]): Promise<void> {
console.log(`Restoring DNS records for project ${projectId}...`);
// Implementation would go here
console.log(`DNS records restored successfully for project ${projectId}.`);
}
private async restorePageRules(projectId: string, rules: any[]): Promise<void> {
console.log(`Restoring Page Rules for project ${projectId}...`);
// Implementation would go here
console.log(`Page Rules restored successfully for project ${projectId}.`);
}
private async restoreFirewallRules(projectId: string, rules: any[]): Promise<void> {
console.log(`Restoring Firewall Rules for project ${projectId}...`);
// Implementation would go here
console.log(`Firewall Rules restored successfully for project ${projectId}.`);
}
private async restoreAccessRules(projectId: string, rules: any[]): Promise<void> {
console.log(`Restoring Access Rules for project ${projectId}...`);
// Implementation would go here
console.log(`Access Rules restored successfully for project ${projectId}.`);
}
private async restoreRateLimitRules(projectId: string, rules: any[]): Promise<void> {
console.log(`Restoring Rate Limit Rules for project ${projectId}...`);
// Implementation would go here
console.log(`Rate Limit Rules restored successfully for project ${projectId}.`);
}
private async restoreSslTlsSettings(projectId: string, settings: any[]): Promise<void> {
console.log(`Restoring SSL/TLS settings for project ${projectId}...`);
// Implementation would go here
console.log(`SSL/TLS settings restored successfully for project ${projectId}.`);
}
private async restoreWorkers(projectId: string, workersPath: string): Promise<void> {
console.log(`Restoring Workers for project ${projectId}...`);
// Implementation would go here
console.log(`Workers restored successfully for project ${projectId}.`);
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Cloudflare Backup MCP server running on stdio');
}
}
const server = new CloudflareBackupServer();
server.run().catch(console.error);