TaskWarrior MCP Server

#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; import os from 'os'; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { diffLines, createTwoFilesPatch } from 'diff'; import { minimatch } from 'minimatch'; import { execSync } from 'child_process'; // Schema definitions // Base task schema that covers common TaskWarrior fields const taskSchema = z.object({ uuid: z.string().uuid(), description: z.string(), status: z.enum(["pending", "completed", "deleted", "waiting", "recurring"]), entry: z.string().datetime(), // ISO timestamp modified: z.string().datetime().optional(), // ISO timestamp due: z.string().optional(), // ISO timestamp priority: z.enum(["H", "M", "L"]).optional(), project: z.string().regex(/^[a-z.]+$/).optional(), tags: z.array(z.string().regex(/^a-z$/)).optional(), }); // Request schemas for different operations const listPendingTasksRequest = z.object({ project: z.string().regex(/^[a-z.]+$/).optional(), tags: z.array(z.string().regex(/^a-z$/)).optional(), }); const listTasksRequest = z.object({ status: z.enum(["pending", "completed", "deleted", "waiting", "recurring"]).optional(), project: z.string().regex(/^[a-z.]+$/).optional(), tags: z.array(z.string().regex(/^a-z$/)).optional(), }); const getTaskRequest = z.object({ identifier: z.string(), }); const markTaskDoneRequest = z.object({ identifier: z.string(), }); const addTaskRequest = z.object({ description: z.string(), // Optional fields that can be set when adding due: z.string().optional(), // ISO timestamp priority: z.enum(["H", "M", "L"]).optional(), project: z.string().regex(/^[a-z.]+$/).optional(), tags: z.array(z.string().regex(/^a-z$/)).optional(), }); // Response schemas const listTasksResponse = z.array(taskSchema); const getTaskResponse = taskSchema; const markTaskDoneResponse = taskSchema; const addTaskResponse = taskSchema; // Error schema const errorResponse = z.object({ error: z.string(), code: z.number(), }); // Server setup const server = new Server( { name: "taskwarrior-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); // Tool handlers const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = z.infer<typeof ToolInputSchema>; server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_next_tasks", description: "Get a list of all pending tasks", inputSchema: zodToJsonSchema(listPendingTasksRequest) as ToolInput, }, { name: "mark_task_done", description: "Mark a task as done (completed)", inputSchema: zodToJsonSchema(markTaskDoneRequest) as ToolInput, }, { name: "add_task", description: "Add a new task", inputSchema: zodToJsonSchema(addTaskRequest) as ToolInput, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case "get_next_tasks": { const parsed = listPendingTasksRequest.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for get_next_tasks: ${parsed.error}`); } let task_args = []; if (parsed.data.tags) { for(let tag of parsed.data.tags) { task_args.push(`+${tag}`); } } if (parsed.data.project) { task_args.push(`project:${parsed.data.project}`); } const content = execSync(`task limit: ${task_args.join(" ")} next`, { maxBuffer: 1024 * 1024 * 10 }).toString().trim(); return { content: [{ type: "text", text: content }], }; } case "mark_task_done": { const parsed = markTaskDoneRequest.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for mark_task_done: ${parsed.error}`); } const content = execSync(`task ${parsed.data.identifier} done`, { maxBuffer: 1024 * 1024 * 10 }).toString().trim(); return { content: [{ type: "text", text: content }], }; } case "add_task": { const parsed = addTaskRequest.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for add_task: ${parsed.error}`); } let task_args = [parsed.data.description]; if (parsed.data.due) { task_args.push(`due:${parsed.data.due}`); } if (parsed.data.priority) { task_args.push(`priority:${parsed.data.priority}`); } if (parsed.data.project) { task_args.push(`project:${parsed.data.project}`); } if (parsed.data.tags) { for (let tag of parsed.data.tags) { task_args.push(`+${tag}`); } } const content = execSync(`task add ${task_args.join(" ")}`, { maxBuffer: 1024 * 1024 * 10 }).toString().trim(); return { content: [{ type: "text", text: content }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } }); // Start server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP TaskWarrior Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });