đź“‹ Help shape our upcoming AI Agents course! Take our 3-minute survey and get 20% off when we launch.

Take Survey →
Go back
Tutorials • May 19, 2025

How to Build an MCP Server: Connect AI to Any External API

Build MCP servers. Integrate AI with external services. Step-by-step guide. Nikolas Barwicki Nikolas Barwicki

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:

  1. Install Claude for Desktop: Get it from the official source if you haven’t already.

  2. 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 your todoist-mcp project folder.
    {
    	"mcpServers": {
    		"todoist": {
    			"command": "node",
    			"args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/todoist-mcp/build/index.js"]
    		}
    	}
    }
  3. Restart Claude for Desktop.

  4. Look for the Hammer Icon: This UI element in Claude indicates your MCP tools are loaded.

  5. 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:

  1. User Prompt: You ask Claude something related to Todoist.
  2. Tool Selection: Claude identifies your server’s relevant tool (e.g., list-today-tasks).
  3. Execution Request: The Claude client sends a request to your MCP server via stdio.
  4. Server Action: Your Node.js server executes the tool’s logic, calling the Todoist API.
  5. API Response: The Todoist API returns data (tasks, success messages, etc.).
  6. MCP Response: Your server formats this data and sends it back to Claude.
  7. 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!