Want to supercharge AI assistants like Claude? Model Context Protocol (MCP) servers are the key. They empower AI to interact directly with external services. This tutorial guides you through building an MCP server for Todoist. Soon, Claude will manage your tasks seamlessly, all thanks to your custom server.
Prerequisites
Before we dive in, ensure you have:
- Basic TypeScript/JavaScript skills.
- Node.js familiarity.
- An active Todoist account and its API token.
See AI Agents Map
More than 500 AI agents in one place
Step 1: Setting Up Your Development Environment
First, let’s get your project foundation ready.
Initialize Your Project
Open your terminal and create a new project directory.
# Create a new directory
mkdir todoist-mcp
cd todoist-mcp
# Initialize npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk @doist/todoist-api-typescript zod
npm install -D @types/node typescript
These commands set up your workspace and install essential packages.
Define Project Structure
A clean structure helps. Create a src
directory for your code.
mkdir src
touch src/index.ts
Your main TypeScript code will live in src/index.ts
.
Configure package.json
Next, update your package.json
. Add type: "module"
for ES module support. Define bin
for command-line execution and scripts
for building.
{
"type": "module",
"bin": {
"todoist-mcp": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": ["build"]
}
This setup makes your server buildable and executable.
Set Up tsconfig.json
Create a tsconfig.json
file for TypeScript compiler options.
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
This configuration ensures modern JavaScript output and strict type checking.
Step 2: Bootstrapping the MCP Server & Todoist API
With the environment set, let’s write some code. Open src/index.ts
.
Import Dependencies & Configure Todoist API
Start by importing necessary modules. Initialize the Todoist API client.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { TodoistApi } from '@doist/todoist-api-typescript';
import { z } from 'zod';
// IMPORTANT: Replace "YOUR_API_TOKEN" with your actual Todoist API token.
// Consider using environment variables for better security.
const TODOIST_API_TOKEN = 'YOUR_API_TOKEN';
if (!TODOIST_API_TOKEN || TODOIST_API_TOKEN === 'YOUR_API_TOKEN') {
console.error(
'Error: TODOIST_API_TOKEN is not set or is still the placeholder. ' +
'Please set it to your actual Todoist API token.'
);
process.exit(1);
}
const api = new TodoistApi(TODOIST_API_TOKEN);
Security Note: Never commit your actual API tokens to public repositories. Use environment variables or a secrets manager for production.
Initialize the MCP Server
Now, create an instance of the McpServer
.
const server = new McpServer({
name: 'todoist',
version: '1.0.0',
capabilities: {
resources: {},
tools: {}
}
});
This server
object is the core of your MCP integration.
Step 3: Crafting Utility Functions
Helper functions keep your code clean. Let’s add one for date formatting.
Get Today’s Date
Todoist API often requires dates in YYYY-MM-DD
format.
// Helper function to get today's date in YYYY-MM-DD format
function getTodayDate(): string {
const today = new Date();
return today.toISOString().split('T')[0];
}
This function simplifies date handling in your tools.
Step 4: Developing MCP Tools for Todoist
Tools are actions your AI assistant can perform. Let’s create three for Todoist.
Tool 1: List Today’s Tasks
This tool fetches tasks due today from Todoist.
server.tool(
'list-today-tasks',
'List all tasks due today in Todoist',
{}, // No input parameters for this tool
async () => {
try {
const todayDate = getTodayDate();
const tasks = await api.getTasks();
// Filter tasks due today and not completed
const todayTasks = tasks.filter((task) => task.due?.date === todayDate && !task.isCompleted);
if (todayTasks.length === 0) {
return { content: [{ type: 'text', text: 'You have no tasks due today.' }] };
}
const formattedTasks = todayTasks.map(
(task) => `- [${task.isCompleted ? 'x' : ' '}] ${task.id}: ${task.content}`
);
return {
content: [
{
type: 'text',
text: `Tasks due today (${todayDate}):\n\n${formattedTasks.join('\n')}`
}
]
};
} catch (error) {
console.error('Error fetching tasks:', error);
return { content: [{ type: 'text', text: 'Failed to retrieve tasks from Todoist.' }] };
}
}
);
Claude can now ask your server, “What are my tasks for today?“.
Tool 2: Add a Task for Today
This tool allows Claude to add new tasks to Todoist, due today.
server.tool(
'add-today-task',
'Add a new task due today in Todoist',
{
// Define input parameters using Zod for validation
content: z.string().describe('The task content/description'),
priority: z
.number()
.min(1)
.max(4)
.optional()
.describe('Task priority (1-4, where 4 is highest, 1 is default/normal)')
},
async ({ content, priority }) => {
try {
const todayDate = getTodayDate();
const task = await api.addTask({
content,
dueDate: todayDate,
priority: priority || undefined // Todoist API: 1 (normal) to 4 (urgent)
});
return {
content: [
{
type: 'text',
text: `Task added successfully: ${task.content} (ID: ${task.id})`
}
]
};
} catch (error) {
console.error('Error adding task:', error);
return { content: [{ type: 'text', text: 'Failed to add task to Todoist.' }] };
}
}
);
Now you can tell Claude, “Add a task: Deploy new feature, priority 4”.
Tool 3: Complete a Task
This tool marks an existing Todoist task as complete.
server.tool(
'complete-task',
'Complete a specific task in Todoist',
{
// Input parameter: task ID
taskId: z.string().describe('The ID of the task to complete')
},
async ({ taskId }) => {
try {
const success = await api.closeTask(taskId);
if (success) {
return { content: [{ type: 'text', text: `Task ${taskId} completed successfully.` }] };
} else {
return { content: [{ type: 'text', text: `Failed to complete task ${taskId}.` }] };
}
} catch (error) {
console.error('Error completing task:', error);
return {
content: [
{
type: 'text',
text: `Failed to complete task ${taskId}. It might not exist or an API error occurred.`
}
]
};
}
}
);
Completing tasks is now as easy as “Mark task 1234567890 complete.”
Step 5: Bringing Your Server to Life
Add a main
function to start your MCP server. It will listen for requests over standard input/output (stdio).
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Todoist MCP Server running on stdio. Ready for Claude!');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});
This makes your server listen for instructions from an MCP client like Claude.
Step 6: Building and Testing Your Todoist MCP Server
Your server code is complete. Let’s build and test it.
Build Your Server
Compile your TypeScript to JavaScript using the script in package.json
.
npm run build
This creates a build
directory with your executable JavaScript.
Testing with Claude for Desktop
To integrate with Claude for Desktop:
Install Claude for Desktop: Get it from the official source if you haven’t already.
Configure Claude: Edit
~/Library/Application Support/Claude/claude_desktop_config.json
(Mac) or the equivalent on your OS.- Important: Replace
/ABSOLUTE/PATH/TO/PARENT/FOLDER/
with the actual absolute path to the directory containing yourtodoist-mcp
project folder.
{ "mcpServers": { "todoist": { "command": "node", "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/todoist-mcp/build/index.js"] } } }
- Important: Replace
Restart Claude for Desktop.
Look for the Hammer Icon: This UI element in Claude indicates your MCP tools are loaded.
Test Commands:
- “What tasks do I have due today?”
- “Add a task to buy milk with priority 3”
- “Complete task [some_task_id_from_list]” (Replace with an actual ID)
See AI Agents Map
More than 500 AI agents in one place
Step 7: Understanding the MCP Workflow
Curious how this all works? Here’s the flow:
- User Prompt: You ask Claude something related to Todoist.
- Tool Selection: Claude identifies your server’s relevant tool (e.g.,
list-today-tasks
). - Execution Request: The Claude client sends a request to your MCP server via stdio.
- Server Action: Your Node.js server executes the tool’s logic, calling the Todoist API.
- API Response: The Todoist API returns data (tasks, success messages, etc.).
- MCP Response: Your server formats this data and sends it back to Claude.
- Natural Language: Claude uses the structured data to give you a helpful, natural language answer.
This elegant protocol allows powerful, secure integrations.
Conclusion: You’ve Unlocked Todoist for Claude!
Congratulations! You’ve successfully built an MCP server. Claude can now interact with your Todoist, streamlining your task management.
This is just the beginning. Consider expanding your server:
- List tasks by project or label.
- Add tasks with custom due dates or recurrence.
- Retrieve task details or comments.
The Model Context Protocol opens vast possibilities for connecting AI to any service with an API. Keep building, keep innovating!