Skip to main content
Glama

Todo Markdown MCP Server

by danjdewhurst
todoManager.ts7.63 kB
import { readFile, writeFile, access } from 'fs/promises'; import { constants } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; import type { TodoItem, AddTodoRequest, UpdateTodoRequest, DeleteTodoRequest, ListTodosResponse, } from './types.js'; export class TodoManager { private todoFilePath: string; constructor(customPath?: string) { // Priority: custom path > environment variable > current working directory if (customPath) { // If customPath ends with .md, treat it as a file path, otherwise as a directory if (customPath.endsWith('.md')) { this.todoFilePath = customPath; } else { this.todoFilePath = join(customPath, 'todo.md'); } } else { const envPath = process.env.TODO_FILE_PATH; if (envPath) { this.todoFilePath = envPath; } else { this.todoFilePath = join(process.cwd(), 'todo.md'); } } } private async fileExists(): Promise<boolean> { try { await access(this.todoFilePath, constants.F_OK); return true; } catch { return false; } } private parseTodoMarkdown(content: string): TodoItem[] { const lines = content.split('\n'); const todos: TodoItem[] = []; for (const line of lines) { const trimmed = line.trim(); // Match markdown checkbox format: - [ ] text or - [x] text const checkboxMatch = trimmed.match(/^-\s*\[([x\s])\]\s*(.+)$/i); if (checkboxMatch) { const [, checkbox, text] = checkboxMatch; if (!checkbox || !text) continue; const completed = checkbox.toLowerCase() === 'x'; // Extract ID from text if it exists (format: text <!-- id:uuid -->) const idMatch = text.match(/^(.*?)\s*<!--\s*id:([a-f0-9-]+)\s*-->$/); let todoText: string; let id: string; if (idMatch) { todoText = idMatch[1]?.trim() || ''; id = idMatch[2] || randomUUID(); } else { todoText = text.trim(); id = randomUUID(); } todos.push({ id, text: todoText, completed, createdAt: new Date(), ...(completed && { completedAt: new Date() }), }); } } return todos; } private formatTodoMarkdown(todos: TodoItem[]): string { const lines = todos.map((todo) => { const checkbox = todo.completed ? '[x]' : '[ ]'; return `- ${checkbox} ${todo.text} <!-- id:${todo.id} -->`; }); const header = '# Todo List\n\n'; const footer = '\n<!-- Generated by MCP Todo Server -->\n'; return header + lines.join('\n') + footer; } async listTodos(): Promise<ListTodosResponse> { if (!(await this.fileExists())) { return { todos: [], total: 0, completed: 0, pending: 0, }; } const content = await readFile(this.todoFilePath, 'utf-8'); const todos = this.parseTodoMarkdown(content); const completed = todos.filter((todo) => todo.completed).length; const pending = todos.length - completed; return { todos, total: todos.length, completed, pending, }; } async addTodo(request: AddTodoRequest): Promise<TodoItem> { const todos = (await this.listTodos()).todos; const newTodo: TodoItem = { id: randomUUID(), text: request.text.trim(), completed: false, createdAt: new Date(), }; todos.push(newTodo); const markdown = this.formatTodoMarkdown(todos); try { await writeFile(this.todoFilePath, markdown, 'utf-8'); } catch (error) { if (error instanceof Error && error.message.includes('EACCES')) { throw new Error( `Permission denied: Cannot write to ${this.todoFilePath}. Check file permissions or set TODO_FILE_PATH environment variable to a writable location.` ); } else if (error instanceof Error && error.message.includes('EROFS')) { throw new Error( `Read-only file system: Cannot write to ${this.todoFilePath}. Set TODO_FILE_PATH environment variable to a writable location.` ); } throw error; } return newTodo; } async updateTodo(request: UpdateTodoRequest): Promise<TodoItem> { const todos = (await this.listTodos()).todos; const todoIndex = todos.findIndex((todo) => todo.id === request.id); if (todoIndex === -1) { throw new Error(`Todo with id ${request.id} not found`); } const todo = todos[todoIndex]!; if (request.text !== undefined) { todo.text = request.text.trim(); } if (request.completed !== undefined) { todo.completed = request.completed; if (request.completed && !todo.completedAt) { todo.completedAt = new Date(); } else if (!request.completed) { delete todo.completedAt; } } todos[todoIndex] = todo; const markdown = this.formatTodoMarkdown(todos); try { await writeFile(this.todoFilePath, markdown, 'utf-8'); } catch (error) { if (error instanceof Error && error.message.includes('EACCES')) { throw new Error( `Permission denied: Cannot write to ${this.todoFilePath}. Check file permissions or set TODO_FILE_PATH environment variable to a writable location.` ); } else if (error instanceof Error && error.message.includes('EROFS')) { throw new Error( `Read-only file system: Cannot write to ${this.todoFilePath}. Set TODO_FILE_PATH environment variable to a writable location.` ); } throw error; } return todo; } async deleteTodo(request: DeleteTodoRequest): Promise<void> { const todos = (await this.listTodos()).todos; const todoIndex = todos.findIndex((todo) => todo.id === request.id); if (todoIndex === -1) { throw new Error(`Todo with id ${request.id} not found`); } todos.splice(todoIndex, 1); const markdown = this.formatTodoMarkdown(todos); try { await writeFile(this.todoFilePath, markdown, 'utf-8'); } catch (error) { if (error instanceof Error && error.message.includes('EACCES')) { throw new Error( `Permission denied: Cannot write to ${this.todoFilePath}. Check file permissions or set TODO_FILE_PATH environment variable to a writable location.` ); } else if (error instanceof Error && error.message.includes('EROFS')) { throw new Error( `Read-only file system: Cannot write to ${this.todoFilePath}. Set TODO_FILE_PATH environment variable to a writable location.` ); } throw error; } } async clearCompleted(): Promise<number> { const todos = (await this.listTodos()).todos; const completedCount = todos.filter((todo) => todo.completed).length; const remainingTodos = todos.filter((todo) => !todo.completed); const markdown = this.formatTodoMarkdown(remainingTodos); try { await writeFile(this.todoFilePath, markdown, 'utf-8'); } catch (error) { if (error instanceof Error && error.message.includes('EACCES')) { throw new Error( `Permission denied: Cannot write to ${this.todoFilePath}. Check file permissions or set TODO_FILE_PATH environment variable to a writable location.` ); } else if (error instanceof Error && error.message.includes('EROFS')) { throw new Error( `Read-only file system: Cannot write to ${this.todoFilePath}. Set TODO_FILE_PATH environment variable to a writable location.` ); } throw error; } return completedCount; } }

Latest Blog Posts

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/danjdewhurst/todo-md-mcp'

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