Skip to main content
Glama

Notion MCP Server

by ghubnerr
import { Client } from "@notionhq/client"; import fs from "fs"; import path from "path"; import { fetchPageContent } from "./notion-api.js"; // Directory for storing backups const BACKUP_DIR = process.env.BACKUP_DIR || path.join(process.cwd(), "backups"); /** * Creates a backup of a Notion page before modifying it * @param notion Notion client * @param pageId ID of the page to backup * @returns Object with backup details */ export async function backupPage(notion: Client, pageId: string) { try { // Create backup directory if it doesn't exist if (!fs.existsSync(BACKUP_DIR)) { fs.mkdirSync(BACKUP_DIR, { recursive: true }); } // Fetch page content const pageContent = await fetchPageContent(notion, pageId); // Generate backup filename const timestamp = new Date().toISOString().replace(/:/g, "-"); const backupFilename = `page_${pageId}_${timestamp}.json`; const backupPath = path.join(BACKUP_DIR, backupFilename); // Write backup to file fs.writeFileSync(backupPath, JSON.stringify(pageContent, null, 2)); return { success: true, backupPath, timestamp, pageId, }; } catch (error) { console.error(`Error backing up page ${pageId}:`, error); throw new Error( `Failed to backup page: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Lists all available backups for a specific page * @param pageId ID of the page to list backups for * @returns Array of backup information */ export function listPageBackups(pageId: string) { try { // Create backup directory if it doesn't exist if (!fs.existsSync(BACKUP_DIR)) { return []; } // Find all backup files for this page const files = fs.readdirSync(BACKUP_DIR); const pageBackups = files.filter( (file) => file.startsWith(`page_${pageId}_`) && file.endsWith(".json") ); // Sort backups by timestamp (most recent first) pageBackups.sort().reverse(); // Extract backup details return pageBackups .map((filename) => { const match = filename.match(/page_(.+)_(.+)\.json/); if (!match) return null; const [_, backupPageId, timestamp] = match; const backupPath = path.join(BACKUP_DIR, filename); const stats = fs.statSync(backupPath); return { pageId: backupPageId, timestamp: timestamp.replace(/-/g, ":"), filename, path: backupPath, size: stats.size, created: stats.mtime, }; }) .filter((backup) => backup !== null); } catch (error) { console.error("Error listing backups:", error); return []; } } /** * Restores a page from a backup * @param notion Notion client * @param backupFilename Filename of the backup to restore * @returns Status of the restore operation */ export async function restoreFromBackup( notion: Client, backupFilename: string ) { try { const backupPath = path.join(BACKUP_DIR, backupFilename); // Check if backup exists if (!fs.existsSync(backupPath)) { throw new Error(`Backup file not found: ${backupFilename}`); } // Read backup file const backupContent = JSON.parse(fs.readFileSync(backupPath, "utf8")); // Extract page ID and content const pageId = backupContent.page.id; const properties = backupContent.page.properties; const blocks = backupContent.blocks; // First update page properties await notion.pages.update({ page_id: pageId, properties, }); // Then clear existing blocks const existingBlocks = await notion.blocks.children.list({ block_id: pageId, }); for (const block of existingBlocks.results) { await notion.blocks.delete({ block_id: block.id, }); } // Recreate blocks (recursive structure is complex, we'll simplify here) const topLevelBlocks = blocks.map((block: any) => { // Remove id to let Notion create new IDs const { id, children, ...blockContent } = block; return blockContent; }); // Add blocks back to the page await notion.blocks.children.append({ block_id: pageId, children: topLevelBlocks, }); return { success: true, pageId, backupFile: backupFilename, message: "Page restored successfully", }; } catch (error) { console.error(`Error restoring from backup:`, error); throw new Error( `Failed to restore from backup: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Creates a backup database in Notion to store backup metadata * @param notion Notion client * @returns ID of the backup database */ export async function createBackupDatabase(notion: Client) { try { // Create a database to track backups const response = await notion.databases.create({ parent: { type: "page_id", page_id: "", // This needs to be a valid page ID // Remove the workspace type that's causing the error }, title: [ { type: "text", text: { content: "MCP Server Backups", }, }, ], properties: { "Page ID": { type: "title", title: {}, }, "Backup Time": { type: "date", date: {}, }, "Backup Path": { type: "rich_text", rich_text: {}, }, Status: { type: "select", select: { options: [ { name: "Active", color: "green" }, { name: "Restored", color: "blue" }, { name: "Failed", color: "red" }, ], }, }, Size: { type: "number", number: { format: "number", }, }, }, }); return response.id; } catch (error) { console.error("Error creating backup database:", error); throw new Error( `Failed to create backup database: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Records a backup operation in the backup tracking database * @param notion Notion client * @param backupDatabaseId ID of the backup tracking database * @param backupInfo Information about the backup * @returns ID of the created record */ export async function recordBackupInNotion( notion: Client, backupDatabaseId: string, backupInfo: any ) { try { const response = await notion.pages.create({ parent: { database_id: backupDatabaseId, }, properties: { "Page ID": { title: [ { text: { content: backupInfo.pageId, }, }, ], }, "Backup Time": { date: { start: new Date(backupInfo.timestamp).toISOString(), }, }, "Backup Path": { rich_text: [ { text: { content: backupInfo.backupPath, }, }, ], }, Status: { select: { name: "Active", }, }, Size: { number: backupInfo.size || 0, }, }, }); return response.id; } catch (error) { console.error("Error recording backup in Notion:", error); // Don't throw - this is a non-critical operation return null; } } /** * Deletes old backups to maintain storage limits * @param maxBackupsPerPage Maximum number of backups to keep per page * @param maxBackupAge Maximum age of backups in days */ export function cleanupOldBackups(maxBackupsPerPage = 5, maxBackupAge = 30) { try { // Create backup directory if it doesn't exist if (!fs.existsSync(BACKUP_DIR)) { return; } // Get all backup files const files = fs.readdirSync(BACKUP_DIR); const backupFiles = files.filter((file) => file.match(/page_.+_.+\.json/)); // Group backups by page ID const backupsByPage: Record<string, string[]> = {}; backupFiles.forEach((file) => { const match = file.match(/page_(.+)_(.+)\.json/); if (!match) return; const [_, pageId] = match; if (!backupsByPage[pageId]) { backupsByPage[pageId] = []; } backupsByPage[pageId].push(file); }); // For each page, keep only the most recent backups Object.entries(backupsByPage).forEach(([pageId, pageBackups]) => { // Sort backups by date (oldest first) pageBackups.sort(); // If we have more than the maximum, delete the oldest if (pageBackups.length > maxBackupsPerPage) { const backupsToDelete = pageBackups.slice( 0, pageBackups.length - maxBackupsPerPage ); backupsToDelete.forEach((file) => { const backupPath = path.join(BACKUP_DIR, file); fs.unlinkSync(backupPath); console.log(`Deleted old backup: ${file}`); }); } }); // Delete backups older than maxBackupAge days const now = new Date(); const maxAge = maxBackupAge * 24 * 60 * 60 * 1000; // days to ms backupFiles.forEach((file) => { const backupPath = path.join(BACKUP_DIR, file); const stats = fs.statSync(backupPath); const fileAge = now.getTime() - stats.mtime.getTime(); if (fileAge > maxAge) { fs.unlinkSync(backupPath); console.log(`Deleted expired backup: ${file}`); } }); return true; } catch (error) { console.error("Error cleaning up old backups:", error); return false; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ghubnerr/Notion-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server