Todoist MCP Server
by abhiz123
- src
#!/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 { TodoistApi } from "@doist/todoist-api-typescript";
// Define tools
const CREATE_TASK_TOOL: Tool = {
name: "todoist_create_task",
description: "Create a new task in Todoist with optional description, due date, and priority",
inputSchema: {
type: "object",
properties: {
content: {
type: "string",
description: "The content/title of the task"
},
description: {
type: "string",
description: "Detailed description of the task (optional)"
},
due_string: {
type: "string",
description: "Natural language due date like 'tomorrow', 'next Monday', 'Jan 23' (optional)"
},
priority: {
type: "number",
description: "Task priority from 1 (normal) to 4 (urgent) (optional)",
enum: [1, 2, 3, 4]
}
},
required: ["content"]
}
};
const GET_TASKS_TOOL: Tool = {
name: "todoist_get_tasks",
description: "Get a list of tasks from Todoist with various filters",
inputSchema: {
type: "object",
properties: {
project_id: {
type: "string",
description: "Filter tasks by project ID (optional)"
},
filter: {
type: "string",
description: "Natural language filter like 'today', 'tomorrow', 'next week', 'priority 1', 'overdue' (optional)"
},
priority: {
type: "number",
description: "Filter by priority level (1-4) (optional)",
enum: [1, 2, 3, 4]
},
limit: {
type: "number",
description: "Maximum number of tasks to return (optional)",
default: 10
}
}
}
};
const UPDATE_TASK_TOOL: Tool = {
name: "todoist_update_task",
description: "Update an existing task in Todoist by searching for it by name and then updating it",
inputSchema: {
type: "object",
properties: {
task_name: {
type: "string",
description: "Name/content of the task to search for and update"
},
content: {
type: "string",
description: "New content/title for the task (optional)"
},
description: {
type: "string",
description: "New description for the task (optional)"
},
due_string: {
type: "string",
description: "New due date in natural language like 'tomorrow', 'next Monday' (optional)"
},
priority: {
type: "number",
description: "New priority level from 1 (normal) to 4 (urgent) (optional)",
enum: [1, 2, 3, 4]
}
},
required: ["task_name"]
}
};
const DELETE_TASK_TOOL: Tool = {
name: "todoist_delete_task",
description: "Delete a task from Todoist by searching for it by name",
inputSchema: {
type: "object",
properties: {
task_name: {
type: "string",
description: "Name/content of the task to search for and delete"
}
},
required: ["task_name"]
}
};
const COMPLETE_TASK_TOOL: Tool = {
name: "todoist_complete_task",
description: "Mark a task as complete by searching for it by name",
inputSchema: {
type: "object",
properties: {
task_name: {
type: "string",
description: "Name/content of the task to search for and complete"
}
},
required: ["task_name"]
}
};
// Server implementation
const server = new Server(
{
name: "todoist-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
},
);
// Check for API token
const TODOIST_API_TOKEN = process.env.TODOIST_API_TOKEN!;
if (!TODOIST_API_TOKEN) {
console.error("Error: TODOIST_API_TOKEN environment variable is required");
process.exit(1);
}
// Initialize Todoist client
const todoistClient = new TodoistApi(TODOIST_API_TOKEN);
// Type guards for arguments
function isCreateTaskArgs(args: unknown): args is {
content: string;
description?: string;
due_string?: string;
priority?: number;
} {
return (
typeof args === "object" &&
args !== null &&
"content" in args &&
typeof (args as { content: string }).content === "string"
);
}
function isGetTasksArgs(args: unknown): args is {
project_id?: string;
filter?: string;
priority?: number;
limit?: number;
} {
return (
typeof args === "object" &&
args !== null
);
}
function isUpdateTaskArgs(args: unknown): args is {
task_name: string;
content?: string;
description?: string;
due_string?: string;
priority?: number;
} {
return (
typeof args === "object" &&
args !== null &&
"task_name" in args &&
typeof (args as { task_name: string }).task_name === "string"
);
}
function isDeleteTaskArgs(args: unknown): args is {
task_name: string;
} {
return (
typeof args === "object" &&
args !== null &&
"task_name" in args &&
typeof (args as { task_name: string }).task_name === "string"
);
}
function isCompleteTaskArgs(args: unknown): args is {
task_name: string;
} {
return (
typeof args === "object" &&
args !== null &&
"task_name" in args &&
typeof (args as { task_name: string }).task_name === "string"
);
}
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [CREATE_TASK_TOOL, GET_TASKS_TOOL, UPDATE_TASK_TOOL, DELETE_TASK_TOOL, COMPLETE_TASK_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("No arguments provided");
}
if (name === "todoist_create_task") {
if (!isCreateTaskArgs(args)) {
throw new Error("Invalid arguments for todoist_create_task");
}
const task = await todoistClient.addTask({
content: args.content,
description: args.description,
dueString: args.due_string,
priority: args.priority
});
return {
content: [{
type: "text",
text: `Task created:\nTitle: ${task.content}${task.description ? `\nDescription: ${task.description}` : ''}${task.due ? `\nDue: ${task.due.string}` : ''}${task.priority ? `\nPriority: ${task.priority}` : ''}`
}],
isError: false,
};
}
if (name === "todoist_get_tasks") {
if (!isGetTasksArgs(args)) {
throw new Error("Invalid arguments for todoist_get_tasks");
}
const tasks = await todoistClient.getTasks({
projectId: args.project_id,
filter: args.filter
});
// Apply additional filters
let filteredTasks = tasks;
if (args.priority) {
filteredTasks = filteredTasks.filter(task => task.priority === args.priority);
}
// Apply limit
if (args.limit && args.limit > 0) {
filteredTasks = filteredTasks.slice(0, args.limit);
}
const taskList = filteredTasks.map(task =>
`- ${task.content}${task.description ? `\n Description: ${task.description}` : ''}${task.due ? `\n Due: ${task.due.string}` : ''}${task.priority ? `\n Priority: ${task.priority}` : ''}`
).join('\n\n');
return {
content: [{
type: "text",
text: filteredTasks.length > 0 ? taskList : "No tasks found matching the criteria"
}],
isError: false,
};
}
if (name === "todoist_update_task") {
if (!isUpdateTaskArgs(args)) {
throw new Error("Invalid arguments for todoist_update_task");
}
// First, search for the task
const tasks = await todoistClient.getTasks();
const matchingTask = tasks.find(task =>
task.content.toLowerCase().includes(args.task_name.toLowerCase())
);
if (!matchingTask) {
return {
content: [{
type: "text",
text: `Could not find a task matching "${args.task_name}"`
}],
isError: true,
};
}
// Build update data
const updateData: any = {};
if (args.content) updateData.content = args.content;
if (args.description) updateData.description = args.description;
if (args.due_string) updateData.dueString = args.due_string;
if (args.priority) updateData.priority = args.priority;
const updatedTask = await todoistClient.updateTask(matchingTask.id, updateData);
return {
content: [{
type: "text",
text: `Task "${matchingTask.content}" updated:\nNew Title: ${updatedTask.content}${updatedTask.description ? `\nNew Description: ${updatedTask.description}` : ''}${updatedTask.due ? `\nNew Due Date: ${updatedTask.due.string}` : ''}${updatedTask.priority ? `\nNew Priority: ${updatedTask.priority}` : ''}`
}],
isError: false,
};
}
if (name === "todoist_delete_task") {
if (!isDeleteTaskArgs(args)) {
throw new Error("Invalid arguments for todoist_delete_task");
}
// First, search for the task
const tasks = await todoistClient.getTasks();
const matchingTask = tasks.find(task =>
task.content.toLowerCase().includes(args.task_name.toLowerCase())
);
if (!matchingTask) {
return {
content: [{
type: "text",
text: `Could not find a task matching "${args.task_name}"`
}],
isError: true,
};
}
// Delete the task
await todoistClient.deleteTask(matchingTask.id);
return {
content: [{
type: "text",
text: `Successfully deleted task: "${matchingTask.content}"`
}],
isError: false,
};
}
if (name === "todoist_complete_task") {
if (!isCompleteTaskArgs(args)) {
throw new Error("Invalid arguments for todoist_complete_task");
}
// First, search for the task
const tasks = await todoistClient.getTasks();
const matchingTask = tasks.find(task =>
task.content.toLowerCase().includes(args.task_name.toLowerCase())
);
if (!matchingTask) {
return {
content: [{
type: "text",
text: `Could not find a task matching "${args.task_name}"`
}],
isError: true,
};
}
// Complete the task
await todoistClient.closeTask(matchingTask.id);
return {
content: [{
type: "text",
text: `Successfully completed task: "${matchingTask.content}"`
}],
isError: false,
};
}
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Todoist MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error running server:", error);
process.exit(1);
});