#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Data storage
const DATA_FILE = path.join(__dirname, "todopomo-data.json");
interface Task {
id: string;
title: string;
description?: string;
status: "pending" | "in-progress" | "completed";
priority: "low" | "medium" | "high";
estimatedPomodoros: number;
completedPomodoros: number;
createdAt: string;
completedAt?: string;
}
interface PomodoroSession {
id: string;
taskId?: string;
duration: number; // in minutes
type: "work" | "short-break" | "long-break";
startTime: string;
endTime?: string;
completed: boolean;
}
interface TodoPomoData {
tasks: Task[];
sessions: PomodoroSession[];
settings: {
workDuration: number;
shortBreakDuration: number;
longBreakDuration: number;
pomodorosBeforeLongBreak: number;
};
}
// Initialize data
function loadData(): TodoPomoData {
if (fs.existsSync(DATA_FILE)) {
const data = fs.readFileSync(DATA_FILE, "utf-8");
return JSON.parse(data);
}
return {
tasks: [],
sessions: [],
settings: {
workDuration: 25,
shortBreakDuration: 5,
longBreakDuration: 15,
pomodorosBeforeLongBreak: 4,
},
};
}
function saveData(data: TodoPomoData): void {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
// Server setup
const server = new Server(
{
name: "todopomo-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Tools definition
const TOOLS: Tool[] = [
{
name: "add_task",
description: "Add a new task to the todo list",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Task title" },
description: { type: "string", description: "Task description" },
priority: {
type: "string",
enum: ["low", "medium", "high"],
description: "Task priority",
},
estimatedPomodoros: {
type: "number",
description: "Estimated number of pomodoros needed",
},
},
required: ["title", "priority", "estimatedPomodoros"],
},
},
{
name: "list_tasks",
description: "List all tasks with optional filtering",
inputSchema: {
type: "object",
properties: {
status: {
type: "string",
enum: ["pending", "in-progress", "completed", "all"],
description: "Filter by status",
},
priority: {
type: "string",
enum: ["low", "medium", "high", "all"],
description: "Filter by priority",
},
},
},
},
{
name: "update_task",
description: "Update an existing task",
inputSchema: {
type: "object",
properties: {
taskId: { type: "string", description: "Task ID" },
title: { type: "string", description: "New title" },
description: { type: "string", description: "New description" },
status: {
type: "string",
enum: ["pending", "in-progress", "completed"],
description: "New status",
},
priority: {
type: "string",
enum: ["low", "medium", "high"],
description: "New priority",
},
},
required: ["taskId"],
},
},
{
name: "delete_task",
description: "Delete a task",
inputSchema: {
type: "object",
properties: {
taskId: { type: "string", description: "Task ID to delete" },
},
required: ["taskId"],
},
},
{
name: "start_pomodoro",
description: "Start a new pomodoro session",
inputSchema: {
type: "object",
properties: {
taskId: {
type: "string",
description: "Task ID to work on (optional)",
},
duration: {
type: "number",
description: "Custom duration in minutes (optional)",
},
type: {
type: "string",
enum: ["work", "short-break", "long-break"],
description: "Type of session",
},
},
required: ["type"],
},
},
{
name: "complete_pomodoro",
description: "Mark a pomodoro session as completed",
inputSchema: {
type: "object",
properties: {
sessionId: { type: "string", description: "Session ID to complete" },
},
required: ["sessionId"],
},
},
{
name: "get_pomodoro_stats",
description: "Get statistics about pomodoro sessions",
inputSchema: {
type: "object",
properties: {
period: {
type: "string",
enum: ["today", "week", "month", "all"],
description: "Time period for stats",
},
},
},
},
{
name: "update_settings",
description: "Update pomodoro timer settings",
inputSchema: {
type: "object",
properties: {
workDuration: { type: "number", description: "Work duration in minutes" },
shortBreakDuration: {
type: "number",
description: "Short break duration in minutes",
},
longBreakDuration: {
type: "number",
description: "Long break duration in minutes",
},
pomodorosBeforeLongBreak: {
type: "number",
description: "Number of pomodoros before long break",
},
},
},
},
{
name: "get_active_session",
description: "Get the currently active pomodoro session",
inputSchema: {
type: "object",
properties: {},
},
},
];
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
const data = loadData();
try {
switch (name) {
case "add_task": {
const task: Task = {
id: Date.now().toString(),
title: args.title as string,
description: args.description as string,
status: "pending",
priority: args.priority as "low" | "medium" | "high",
estimatedPomodoros: args.estimatedPomodoros as number,
completedPomodoros: 0,
createdAt: new Date().toISOString(),
};
data.tasks.push(task);
saveData(data);
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, task, message: "Task added successfully" },
null,
2
),
},
],
};
}
case "list_tasks": {
let filteredTasks = data.tasks;
if (args.status && args.status !== "all") {
filteredTasks = filteredTasks.filter((t) => t.status === args.status);
}
if (args.priority && args.priority !== "all") {
filteredTasks = filteredTasks.filter(
(t) => t.priority === args.priority
);
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, tasks: filteredTasks, count: filteredTasks.length },
null,
2
),
},
],
};
}
case "update_task": {
const taskIndex = data.tasks.findIndex((t) => t.id === args.taskId);
if (taskIndex === -1) {
return {
content: [
{
type: "text",
text: JSON.stringify({ success: false, error: "Task not found" }),
},
],
};
}
const task = data.tasks[taskIndex];
if (args.title) task.title = args.title as string;
if (args.description) task.description = args.description as string;
if (args.status) {
task.status = args.status as "pending" | "in-progress" | "completed";
if (args.status === "completed") {
task.completedAt = new Date().toISOString();
}
}
if (args.priority)
task.priority = args.priority as "low" | "medium" | "high";
saveData(data);
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, task, message: "Task updated successfully" },
null,
2
),
},
],
};
}
case "delete_task": {
const taskIndex = data.tasks.findIndex((t) => t.id === args.taskId);
if (taskIndex === -1) {
return {
content: [
{
type: "text",
text: JSON.stringify({ success: false, error: "Task not found" }),
},
],
};
}
data.tasks.splice(taskIndex, 1);
saveData(data);
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, message: "Task deleted successfully" },
null,
2
),
},
],
};
}
case "start_pomodoro": {
const type = args.type as "work" | "short-break" | "long-break";
let duration: number;
if (args.duration) {
duration = args.duration as number;
} else {
switch (type) {
case "work":
duration = data.settings.workDuration;
break;
case "short-break":
duration = data.settings.shortBreakDuration;
break;
case "long-break":
duration = data.settings.longBreakDuration;
break;
}
}
const session: PomodoroSession = {
id: Date.now().toString(),
taskId: args.taskId as string,
duration,
type,
startTime: new Date().toISOString(),
completed: false,
};
data.sessions.push(session);
saveData(data);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
session,
message: `${type} session started for ${duration} minutes`,
},
null,
2
),
},
],
};
}
case "complete_pomodoro": {
const sessionIndex = data.sessions.findIndex(
(s) => s.id === args.sessionId
);
if (sessionIndex === -1) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: "Session not found",
}),
},
],
};
}
const session = data.sessions[sessionIndex];
session.completed = true;
session.endTime = new Date().toISOString();
// Update task if this was a work session
if (session.type === "work" && session.taskId) {
const task = data.tasks.find((t) => t.id === session.taskId);
if (task) {
task.completedPomodoros++;
if (task.status === "pending") {
task.status = "in-progress";
}
}
}
saveData(data);
return {
content: [
{
type: "text",
text: JSON.stringify(
{ success: true, session, message: "Pomodoro completed!" },
null,
2
),
},
],
};
}
case "get_pomodoro_stats": {
const period = (args.period as string) || "all";
let filteredSessions = data.sessions.filter((s) => s.completed);
const now = new Date();
if (period === "today") {
const today = new Date(now.setHours(0, 0, 0, 0));
filteredSessions = filteredSessions.filter(
(s) => new Date(s.startTime) >= today
);
} else if (period === "week") {
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
filteredSessions = filteredSessions.filter(
(s) => new Date(s.startTime) >= weekAgo
);
} else if (period === "month") {
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
filteredSessions = filteredSessions.filter(
(s) => new Date(s.startTime) >= monthAgo
);
}
const stats = {
totalSessions: filteredSessions.length,
workSessions: filteredSessions.filter((s) => s.type === "work").length,
breakSessions: filteredSessions.filter((s) => s.type !== "work").length,
totalMinutes: filteredSessions.reduce((sum, s) => sum + s.duration, 0),
period,
};
return {
content: [
{ type: "text", text: JSON.stringify({ success: true, stats }, null, 2) },
],
};
}
case "update_settings": {
if (args.workDuration)
data.settings.workDuration = args.workDuration as number;
if (args.shortBreakDuration)
data.settings.shortBreakDuration = args.shortBreakDuration as number;
if (args.longBreakDuration)
data.settings.longBreakDuration = args.longBreakDuration as number;
if (args.pomodorosBeforeLongBreak)
data.settings.pomodorosBeforeLongBreak =
args.pomodorosBeforeLongBreak as number;
saveData(data);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
settings: data.settings,
message: "Settings updated successfully",
},
null,
2
),
},
],
};
}
case "get_active_session": {
const activeSession = data.sessions.find((s) => !s.completed);
if (!activeSession) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
activeSession: null,
message: "No active session",
}),
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify({ success: true, activeSession }, null, 2),
},
],
};
}
default:
return {
content: [
{ type: "text", text: JSON.stringify({ error: "Unknown tool" }) },
],
};
}
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: (error as Error).message }),
},
],
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("TodoPomo MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});