Skip to main content
Glama

NotePlan MCP Server

by bscott
noteService.ts11.2 kB
/** * Service for handling NotePlan notes interactions */ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; interface Note { id: string; title: string; content: string; created: string; modified: string; folder: string; filePath?: string; filename?: string; type?: string; } interface CreateNoteParams { title: string; content?: string; folder?: string; } interface CreateDailyNoteParams { date?: string; content?: string; } interface UpdateNoteParams { title?: string; content?: string; folder?: string; } // NotePlan data directory path const NOTEPLAN_BASE_PATH = path.join( os.homedir(), 'Library/Containers/co.noteplan.NotePlan3/Data/Library/Application Support/co.noteplan.NotePlan3' ); const CALENDAR_PATH = path.join(NOTEPLAN_BASE_PATH, 'Calendar'); const NOTES_PATH = path.join(NOTEPLAN_BASE_PATH, 'Notes'); // Cache for notes data let notesCache: Note[] = []; let lastCacheUpdate: number = 0; const CACHE_DURATION = 5000; // 5 seconds // Mock database for notes - fallback if NotePlan directory not found const notesDb: Note[] = [ { id: 'note1', title: 'Sample Note 1', content: 'This is a sample note', created: '2023-01-01T12:00:00Z', modified: '2023-01-02T14:30:00Z', folder: 'Notes' }, { id: 'note2', title: 'Sample Note 2', content: 'This is another sample note', created: '2023-02-15T09:45:00Z', modified: '2023-02-16T11:20:00Z', folder: 'Notes' }, { id: 'note3', title: 'Project Ideas', content: '- Build a note-taking app\n- Learn a new language\n- Write a book', created: '2023-03-10T16:15:00Z', modified: '2023-03-12T08:00:00Z', folder: 'Projects' } ]; /** * Check if NotePlan directory exists */ function isNotePlanAvailable(): boolean { try { return fs.existsSync(NOTEPLAN_BASE_PATH) && fs.existsSync(CALENDAR_PATH) && fs.existsSync(NOTES_PATH); } catch (error) { return false; } } /** * Parse markdown file to extract metadata */ function parseMarkdownFile(filePath: string, folder: string): Note | null { try { const content = fs.readFileSync(filePath, 'utf8'); const stats = fs.statSync(filePath); const filename = path.basename(filePath, '.md'); // Extract title from first line or use filename const lines = content.split('\n'); let title = filename; // Look for markdown title (# Title) for (const line of lines) { if (line.startsWith('# ') && line.length > 2) { title = line.substring(2).trim(); break; } } // Generate ID from filename const id = folder === 'Calendar' ? `calendar-${filename}` : `note-${filename}`; return { id, title, content, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), folder, filePath, filename }; } catch (error) { console.error(`Error parsing file ${filePath}:`, error); return null; } } /** * Scan directory for markdown files */ function scanNotesDirectory(dirPath: string, folder: string): Note[] { const notes: Note[] = []; try { if (!fs.existsSync(dirPath)) { return notes; } const items = fs.readdirSync(dirPath, { withFileTypes: true }); for (const item of items) { if (item.isFile() && item.name.endsWith('.md')) { const filePath = path.join(dirPath, item.name); const note = parseMarkdownFile(filePath, folder); if (note) { notes.push(note); } } else if (item.isDirectory() && !item.name.startsWith('.') && !item.name.startsWith('@')) { // Recursively scan subdirectories (but skip hidden and special folders) const subNotes = scanNotesDirectory( path.join(dirPath, item.name), `${folder}/${item.name}` ); notes.push(...subNotes); } } } catch (error) { console.error(`Error scanning directory ${dirPath}:`, error); } return notes; } /** * Load all notes from NotePlan directories */ function loadNotesFromFileSystem(): Note[] { if (!isNotePlanAvailable()) { console.warn('NotePlan directory not found, using mock data'); return notesDb; } const notes: Note[] = []; // Load calendar notes const calendarNotes = scanNotesDirectory(CALENDAR_PATH, 'Calendar'); notes.push(...calendarNotes); // Load regular notes const regularNotes = scanNotesDirectory(NOTES_PATH, 'Notes'); notes.push(...regularNotes); return notes.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()); } /** * Get all notes with caching */ function getAllNotes(): Note[] { const now = Date.now(); // Use cache if still valid if (notesCache.length > 0 && (now - lastCacheUpdate) < CACHE_DURATION) { return notesCache; } // Refresh cache notesCache = loadNotesFromFileSystem(); lastCacheUpdate = now; return notesCache; } /** * Get a note by ID */ function getNoteById(id: string): Note | null { const notes = getAllNotes(); return notes.find(note => note.id === id) || null; } /** * Search notes by title or content */ function searchNotes(query: string): Note[] { const notes = getAllNotes(); const lowerQuery = query.toLowerCase(); return notes.filter(note => note.title.toLowerCase().includes(lowerQuery) || note.content.toLowerCase().includes(lowerQuery) ); } /** * Get notes by folder */ function getNotesByFolder(folder: string): Note[] { const notes = getAllNotes(); return notes.filter(note => note.folder === folder || note.folder.startsWith(folder + '/')); } /** * Create a daily note with today's date */ function createDailyNote(options: CreateDailyNoteParams = {}): Note { const noteDate = options.date ? new Date(options.date) : new Date(); const dateStr = noteDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format const noteId = `calendar-${dateStr}`; // Check if daily note already exists const existingNote = getNoteById(noteId); if (existingNote) { throw new Error(`Daily note for ${dateStr} already exists`); } const defaultTemplate = `# ${dateStr} ## Today's Plan - [ ] ## Notes ## Reflection --- Created: ${noteDate.toISOString()}`; const content = options.content || defaultTemplate; if (isNotePlanAvailable()) { // Write to actual NotePlan directory const filePath = path.join(CALENDAR_PATH, `${dateStr}.md`); try { fs.writeFileSync(filePath, content, 'utf8'); // Clear cache to force refresh notesCache = []; lastCacheUpdate = 0; // Return the newly created note return parseMarkdownFile(filePath, 'Calendar')!; } catch (error) { throw new Error(`Failed to create daily note: ${(error as Error).message}`); } } else { // Fallback to mock database const newNote: Note = { id: noteId, title: `Daily Note - ${dateStr}`, content, created: noteDate.toISOString(), modified: noteDate.toISOString(), folder: 'Calendar', type: 'daily' }; notesDb.push(newNote); return newNote; } } /** * Create a new note */ function createNote(noteData: CreateNoteParams): Note { if (!noteData.title) { throw new Error('Note title is required'); } const now = new Date(); const timestamp = now.toISOString(); // Generate filename from title (sanitize for filesystem) const sanitizedTitle = noteData.title .replace(/[^a-zA-Z0-9\s-_]/g, '') .replace(/\s+/g, '-') .substring(0, 50); const filename = `${sanitizedTitle}-${Date.now()}`; const noteId = `note-${filename}`; // Prepare content with title as markdown header const content = noteData.content ? `# ${noteData.title}\n\n${noteData.content}` : `# ${noteData.title}\n\n`; if (isNotePlanAvailable()) { // Determine target directory const targetFolder = noteData.folder || 'Notes'; let targetPath = NOTES_PATH; if (targetFolder !== 'Notes' && targetFolder !== 'Calendar') { targetPath = path.join(NOTES_PATH, targetFolder); // Create subfolder if it doesn't exist if (!fs.existsSync(targetPath)) { fs.mkdirSync(targetPath, { recursive: true }); } } const filePath = path.join(targetPath, `${filename}.md`); try { fs.writeFileSync(filePath, content, 'utf8'); // Clear cache to force refresh notesCache = []; lastCacheUpdate = 0; // Return the newly created note return parseMarkdownFile(filePath, targetFolder)!; } catch (error) { throw new Error(`Failed to create note: ${(error as Error).message}`); } } else { // Fallback to mock database const newNote: Note = { id: noteId, title: noteData.title, content, created: timestamp, modified: timestamp, folder: noteData.folder || 'Notes' }; notesDb.push(newNote); return newNote; } } /** * Update an existing note */ function updateNote(id: string, updates: UpdateNoteParams): Note { const existingNote = getNoteById(id); if (!existingNote) { throw new Error(`Note with id ${id} not found`); } if (isNotePlanAvailable() && existingNote.filePath) { try { // Update the file content let newContent = existingNote.content; if (updates.content !== undefined) { newContent = updates.content; } // If title is being updated, update the first markdown header if (updates.title !== undefined) { const lines = newContent.split('\n'); let headerUpdated = false; for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('# ')) { lines[i] = `# ${updates.title}`; headerUpdated = true; break; } } // If no header found, add one at the beginning if (!headerUpdated) { lines.unshift(`# ${updates.title}`, ''); } newContent = lines.join('\n'); } // Write updated content to file fs.writeFileSync(existingNote.filePath, newContent, 'utf8'); // Clear cache to force refresh notesCache = []; lastCacheUpdate = 0; // Return updated note return parseMarkdownFile(existingNote.filePath, existingNote.folder)!; } catch (error) { throw new Error(`Failed to update note: ${(error as Error).message}`); } } else { // Fallback to mock database const noteIndex = notesDb.findIndex(note => note.id === id); if (noteIndex === -1) { throw new Error(`Note with id ${id} not found`); } const note = notesDb[noteIndex]; const updatedNote: Note = { ...note, ...updates, modified: new Date().toISOString() }; notesDb[noteIndex] = updatedNote; return updatedNote; } } export const noteService = { getAllNotes, getNoteById, searchNotes, getNotesByFolder, createDailyNote, createNote, updateNote };

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/bscott/noteplan-mcp'

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