Skip to main content
Glama

Todoist MCP Server

by bkotos
.cursorrules8.32 kB
# Todoist MCP Server - Development Standards ## TDD Approach **ALWAYS write tests first:** 1. **Red** - Write a failing test 2. **PAUSE** - Ask user to pause and read the tests before proceeding to green 3. **Green** - Write minimal code to make it pass 4. **Refactor** - Improve code while keeping tests green ## Architecture - Use functional programming approach with pure functions - Separate concerns: client logic in `client.ts`, business logic in `todoist.ts` - Use Vitest module mocking for testing (not dependency injection) - Keep functions small and focused on single responsibility ## Code Style - Use 2 spaces for indentation (configured in .editorconfig) - Prefer const over let, avoid var - Use arrow functions for consistency - Use template literals for string interpolation - Use destructuring for cleaner code ## Testing - Write tests first (TDD approach) - Use descriptive test names that explain the behavior - Mock modules at the top of test files with `vi.mock('./module')` - **Import Vitest types only, not runtime functions:** ```typescript // ✅ Good - import types only import type { MockedFunction, Mocked } from 'vitest'; // ❌ Bad - import runtime functions import { vi, describe, it, expect } from 'vitest'; ``` - Test both success and error cases - Keep tests focused and isolated - **Tests are sufficient validation - do NOT start/run the project manually** - **Running `npm test` is the only validation needed for new features** - **Do NOT use `npm run dev`, `npx tsx src/index.ts`, or any server startup commands** - **Do NOT check server logs or verify server is running** - **Focus on test coverage and test results only** ## AAA Test Pattern **ALWAYS follow this exact structure:** ```typescript it('should do something', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: mockData }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await functionName(); // assert expect(result).toContain('expected output'); expect(mockClient.get).toHaveBeenCalledWith('/endpoint'); }); ``` **Rules:** - ✅ **Separate blocks**: Always use distinct `// arrange`, `// act`, `// assert` blocks - ✅ **No blank lines within blocks**: All statements in each block must be consecutive - ✅ **Blank lines between blocks**: Only use blank lines between AAA blocks - ❌ **Never combine**: No `// act & assert` - always separate blocks - ❌ **No internal spacing**: No blank lines within arrange/act/assert blocks ## Adding New Tools ### Step 1: Write Tests First Create or update `src/services/todoist.spec.ts`: ```typescript describe('newTool', () => { it('should handle success case', async () => { // arrange const mockClient = { get: vi.fn().mockResolvedValue({ data: mockData }), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const result = await newTool(); // assert expect(result).toEqual(mockData); expect(mockClient.get).toHaveBeenCalledWith('/endpoint'); }); it('should handle error case', async () => { // arrange const mockClient = { get: vi.fn().mockRejectedValue(new Error('API Error')), }; mockGetTodoistClient.mockReturnValue(mockClient); // act const promise = newTool(); // assert await expect(promise).rejects.toThrow('Failed to...'); }); }); ``` ### Step 2: Implement Function Add to `src/services/todoist.ts`: ```typescript export async function newTool(): Promise<ResponseType[]> { const client = getTodoistClient(); try { const response = await client.get<ResponseType[]>('/endpoint'); return response.data; } catch (error) { throw new Error(`Failed to newTool: ${getErrorMessage(error)}`); } } ``` ### Step 3: Add to MCP Server Update `src/index.ts`: ```typescript // Add to tools array { name: 'new_tool', description: 'Clear description of what this tool does', inputSchema: { type: 'object', properties: { param1: { type: 'string', description: 'Description of parameter' } }, required: ['param1'] } } // Add to switch statement case 'new_tool': return { content: [ { type: 'text', text: await newTool() } ] }; ``` ## Common Patterns ### Error Handling Pattern ```typescript function getErrorMessage(error: any): string { if (axios.isAxiosError(error)) { return error.response?.data?.error || error.message; } return error instanceof Error ? error.message : 'Unknown error'; } ``` ### URL Encoding Pattern **For complex query strings in tests, use constants and `encodeURIComponent`:** ```typescript // Define complex filters as constants for readability const COMPLEX_FILTER = '(today | overdue) & !##Tickler & !##Someday'; // In tests, use encodeURIComponent for URL assertions expect(mockClient.get).toHaveBeenCalledWith( `/tasks?filter=${encodeURIComponent(COMPLEX_FILTER)}` ); // This makes tests more readable and maintainable ``` ### JSON Return Pattern **Services should return raw JSON data, not formatted strings:** ```typescript // ✅ Good - return raw data export async function getChoresDueToday(): Promise<TodoistTask[]> { const client = getTodoistClient(); try { const filter = '(today | overdue) & ##Chores'; const response = await client.get<TodoistTask[]>( `/tasks?filter=${encodeURIComponent(filter)}` ); return response.data; } catch (error) { throw new Error( `Failed to get chores due today: ${getErrorMessage(error)}` ); } } // ❌ Bad - return formatted strings export async function getChoresDueToday(): Promise<string> { // ... implementation return `Found ${tasks.length} chore(s):\n\n${formattedTasks}`; } ``` **Benefits:** - Easier to test and assert - More flexible for consumers - Follows separation of concerns - Formatting can be handled at the presentation layer - Allows the LLM using the MCP server to use raw JSON and format it how it likes, rather than us guessing on how the data should be presented in the LLM's response to the user ### Tool Handler Pattern **Tool handlers should return text with JSON strings for MCP compatibility:** ```typescript // ✅ Good - tool handler returns text with JSON string export const getChoresDueTodayHandler = async () => { console.error('Executing get_chores_due_today...'); const result = await getChoresDueToday(); console.error('get_chores_due_today completed successfully'); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }; // ❌ Bad - tool handler returns raw data export const getChoresDueTodayHandler = async () => { const result = await getChoresDueToday(); return result; // This breaks MCP server expectations }; ``` **Why this pattern:** - MCP server expects content with text type - JSON.stringify preserves data structure while making it text - LLM can still parse the JSON and format as needed - Maintains compatibility with MCP protocol ### Formatting Pattern ```typescript function formatList(items: any[], itemName: string): string { if (items.length === 0) { return `No ${itemName} found.`; } const formattedItems = items.map((item) => formatItem(item)).join('\n\n'); return `Found ${items.length} ${itemName}(s):\n\n${formattedItems}`; } ``` ## Common Todoist API Endpoints - `GET /projects` - List all projects - `GET /tasks` - List all tasks - `POST /tasks` - Create a new task - `POST /tasks/{id}` - Update a task - `POST /tasks/{id}/close` - Close a task ## File Organization - Co-locate tests with source files (`.spec.ts` next to `.ts`) - Use clear, descriptive file names - Group related functions in the same file - Export types for testing when needed ## Error Handling - Use descriptive error messages - Throw errors with context about what failed - Handle API errors gracefully with proper error messages ## MCP Server - Keep tool definitions simple and clear - Use descriptive tool names and descriptions - Validate required parameters in tool schemas - Return structured responses with proper content types ## Git Workflow 1. Write tests first 2. Implement functionality 3. Ensure all tests pass 4. Update documentation

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/bkotos/todoist-mcp'

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