.cursorrules•8.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