Skip to main content
Glama
index.ts12.4 kB
#!/usr/bin/env node import 'dotenv/config'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { TickTickClient } from './ticktick-client.js'; import { TickTickConfig } from './types.js'; class TickTickMCPServer { private server: Server; private ticktickClient: TickTickClient | null = null; constructor() { this.server = new Server( { name: 'ticktick-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_tasks', description: 'Get all tasks or tasks from a specific project', inputSchema: { type: 'object', properties: { projectId: { type: 'string', description: 'Optional project ID to filter tasks', }, }, }, }, { name: 'get_overdue_tasks', description: 'Get all overdue tasks (incomplete tasks past their due date)', inputSchema: { type: 'object', properties: { projectId: { type: 'string', description: 'Optional project ID to filter overdue tasks', }, timezoneOffsetHours: { type: 'number', description: 'Timezone offset in hours from UTC (e.g., 8 for UTC+8). Defaults to 8', }, }, }, }, { name: 'get_todays_tasks', description: 'Get tasks that are specifically due today', inputSchema: { type: 'object', properties: { projectId: { type: 'string', description: 'Optional project ID to filter today\'s tasks', }, }, }, }, { name: 'get_projects', description: 'Get all projects from TickTick', inputSchema: { type: 'object', properties: {}, }, }, { name: 'create_task', description: 'Create a new task in TickTick', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Task title (required)', }, content: { type: 'string', description: 'Task description/content', }, projectId: { type: 'string', description: 'Project ID where the task should be created', }, dueDate: { type: 'string', description: 'Due date in ISO format', }, priority: { type: 'number', description: 'Task priority (0=None, 1=Low, 3=Medium, 5=High)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Task tags', }, }, required: ['title'], }, }, { name: 'update_task', description: 'Update an existing task', inputSchema: { type: 'object', properties: { taskId: { type: 'string', description: 'Task ID to update (required)', }, title: { type: 'string', description: 'New task title', }, content: { type: 'string', description: 'New task description/content', }, dueDate: { type: 'string', description: 'New due date in ISO format', }, priority: { type: 'number', description: 'New task priority (0=None, 1=Low, 3=Medium, 5=High)', }, tags: { type: 'array', items: { type: 'string' }, description: 'New task tags', }, }, required: ['taskId'], }, }, { name: 'delete_task', description: 'Delete a task', inputSchema: { type: 'object', properties: { taskId: { type: 'string', description: 'Task ID to delete (required)', }, projectId: { type: 'string', description: 'Project ID containing the task (required)', }, }, required: ['taskId', 'projectId'], }, }, { name: 'complete_task', description: 'Mark a task as completed', inputSchema: { type: 'object', properties: { taskId: { type: 'string', description: 'Task ID to complete (required)', }, projectId: { type: 'string', description: 'Project ID containing the task (required)', }, }, required: ['taskId', 'projectId'], }, }, { name: 'get_task', description: 'Get a specific task by ID', inputSchema: { type: 'object', properties: { taskId: { type: 'string', description: 'Task ID to retrieve (required)', }, projectId: { type: 'string', description: 'Project ID containing the task (required)', }, }, required: ['taskId', 'projectId'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { await this.ensureClientInitialized(); switch (name) { case 'get_tasks': const tasks = await this.ticktickClient!.getTasks(args?.projectId as string); return { content: [ { type: 'text', text: JSON.stringify(tasks, null, 2), }, ], }; case 'get_overdue_tasks': const timezoneOffsetHours = args?.timezoneOffsetHours as number || 8; const overdueTasks = await this.ticktickClient!.getOverdueTasks(args?.projectId as string, timezoneOffsetHours); return { content: [ { type: 'text', text: JSON.stringify(overdueTasks, null, 2), }, ], }; case 'get_todays_tasks': const todaysTasks = await this.ticktickClient!.getTodaysTasks(args?.projectId as string); return { content: [ { type: 'text', text: JSON.stringify(todaysTasks, null, 2), }, ], }; case 'get_projects': const projects = await this.ticktickClient!.getProjects(); return { content: [ { type: 'text', text: JSON.stringify(projects, null, 2), }, ], }; case 'create_task': if (!args?.title) { throw new McpError(ErrorCode.InvalidParams, 'Title is required'); } const newTask = await this.ticktickClient!.createTask(args); return { content: [ { type: 'text', text: `Task created successfully: ${JSON.stringify(newTask, null, 2)}`, }, ], }; case 'update_task': if (!args?.taskId) { throw new McpError(ErrorCode.InvalidParams, 'Task ID is required'); } const updatedTask = await this.ticktickClient!.updateTask(args.taskId as string, args); return { content: [ { type: 'text', text: `Task updated successfully: ${JSON.stringify(updatedTask, null, 2)}`, }, ], }; case 'delete_task': if (!args?.taskId || !args?.projectId) { throw new McpError(ErrorCode.InvalidParams, 'Task ID and Project ID are required'); } await this.ticktickClient!.deleteTask(args.taskId as string, args.projectId as string); return { content: [ { type: 'text', text: 'Task deleted successfully', }, ], }; case 'complete_task': if (!args?.taskId || !args?.projectId) { throw new McpError(ErrorCode.InvalidParams, 'Task ID and Project ID are required'); } const completedTask = await this.ticktickClient!.completeTask(args.taskId as string, args.projectId as string); return { content: [ { type: 'text', text: `Task completed successfully: ${JSON.stringify(completedTask, null, 2)}`, }, ], }; case 'get_task': if (!args?.taskId || !args?.projectId) { throw new McpError(ErrorCode.InvalidParams, 'Task ID and Project ID are required'); } const task = await this.ticktickClient!.getTaskById(args.taskId as string, args.projectId as string); return { content: [ { type: 'text', text: JSON.stringify(task, null, 2), }, ], }; default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { throw new McpError( ErrorCode.InternalError, `Error executing tool ${name}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); } private async ensureClientInitialized(): Promise<void> { if (!this.ticktickClient) { const accessToken = process.env.TICKTICK_ACCESS_TOKEN; const username = process.env.TICKTICK_USERNAME; const password = process.env.TICKTICK_PASSWORD; const clientId = process.env.TICKTICK_CLIENT_ID; const clientSecret = process.env.TICKTICK_CLIENT_SECRET; const refreshToken = process.env.TICKTICK_REFRESH_TOKEN; if (!accessToken && (!username || !password)) { throw new McpError( ErrorCode.InvalidParams, 'Either TICKTICK_ACCESS_TOKEN or TICKTICK_USERNAME/TICKTICK_PASSWORD environment variables are required' ); } const config: TickTickConfig = { accessToken, username, password, clientId, clientSecret, refreshToken }; this.ticktickClient = new TickTickClient(config); } } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('TickTick MCP server running on stdio'); } } const server = new TickTickMCPServer(); server.run().catch((error) => { console.error('Server error:', error); process.exit(1); });

Latest Blog Posts

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/rafliruslan/ticktick-mcp-server'

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