import { Request, Response } from "express"; import { storage } from "./storage"; import { User } from "../shared/schema"; import { randomBytes } from "crypto"; interface JsonRpcRequest { jsonrpc: "2.0"; method: string; params?: any; id: number | string; } interface JsonRpcResponse { jsonrpc: "2.0"; result?: any; error?: { code: number; message: string; data?: any; }; id: number | string | null; } export class McpServer { private clients: Map = new Map(); async authenticate(req: Request): Promise { let key = req.headers["x-api-key"] as string; if (!key && req.query.apiKey) { key = req.query.apiKey as string; } if (!key && req.headers["authorization"]) { const auth = req.headers["authorization"]; if (auth.startsWith("Bearer ")) { key = auth.substring(7); } } if (!key) return null; return (await storage.getUserByApiKey(key)) || null; } async handleSse(req: Request, res: Response) { const user = await this.authenticate(req); if (!user) { res.status(401).send("Unauthorized: Invalid API Key"); return; } res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*", }); const sessionId = randomBytes(8).toString("hex"); this.clients.set(sessionId, res); const endpoint = `/api/mcp/messages`; res.write(`event: endpoint\ndata: ${endpoint}\n\n`); const interval = setInterval(() => { res.write(": keepalive\n\n"); }, 15000); req.on("close", () => { clearInterval(interval); this.clients.delete(sessionId); }); } async handleMessage(req: Request, res: Response) { const user = await this.authenticate(req); if (!user) { res.status(401).json({ error: "Unauthorized" }); return; } const body = req.body as JsonRpcRequest; try { const result = await this.processRequest(body, user); res.json({ jsonrpc: "2.0", result, id: body.id }); } catch (err: any) { res.json({ jsonrpc: "2.0", error: { code: -32000, message: err.message || "Internal Server Error" }, id: body.id }); } } private async processRequest(req: JsonRpcRequest, user: User): Promise { switch (req.method) { case "initialize": return { protocolVersion: "2024-11-05", capabilities: { tools: {}, resources: {} }, serverInfo: { name: "TaskFlow MCP", version: "2.0.0" } }; case "tools/list": case "listTools": return { tools: this.getToolDefinitions() }; case "tools/call": case "callTool": return await this.handleToolCall(req.params.name, req.params.arguments, user); case "notifications/initialized": return true; default: throw new Error(`Method ${req.method} not found`); } } private getToolDefinitions() { return [ // ===== TASK MANAGEMENT ===== { name: "list_tasks", description: "List all tasks. Optional filters: status, priority, labelId, limit.", inputSchema: { type: "object", properties: { status: { type: "string", enum: ["todo", "inProgress", "done"], description: "Filter by status" }, priority: { type: "string", enum: ["low", "medium", "high"], description: "Filter by priority" }, labelId: { type: "string", description: "Filter by label ID" }, limit: { type: "number", description: "Max results" }, includeCompleted: { type: "boolean", description: "Include done tasks (default: false)" } } } }, { name: "get_task", description: "Get a single task by ID with full details.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Task ID" } }, required: ["id"] } }, { name: "search_tasks", description: "Search tasks by title/description text.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search text" } }, required: ["query"] } }, { name: "create_task", description: "Create a new task. Returns the created task with ID.", inputSchema: { type: "object", properties: { title: { type: "string", description: "Task title" }, description: { type: "string", description: "Task description (markdown supported)" }, priority: { type: "string", enum: ["low", "medium", "high"], description: "Priority level" }, status: { type: "string", enum: ["todo", "inProgress", "done"], description: "Initial status" }, dueDate: { type: "string", description: "Due date (ISO 8601)" }, labelId: { type: "string", description: "Label/category ID" }, estimatedDuration: { type: "number", description: "Estimated minutes" }, energyLevel: { type: "string", enum: ["low", "medium", "high"], description: "Required energy level" }, parentTaskId: { type: "string", description: "Parent task ID (for subtasks)" }, notes: { type: "string", description: "Additional notes" } }, required: ["title"] } }, { name: "update_task", description: "Update an existing task. Only provided fields are changed.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Task ID" }, title: { type: "string" }, description: { type: "string" }, priority: { type: "string", enum: ["low", "medium", "high"] }, status: { type: "string", enum: ["todo", "inProgress", "done"] }, dueDate: { type: "string", description: "Due date (ISO 8601) or null to clear" }, labelId: { type: "string", description: "Label ID or null to clear" }, estimatedDuration: { type: "number" }, energyLevel: { type: "string", enum: ["low", "medium", "high"] }, notes: { type: "string" } }, required: ["id"] } }, { name: "complete_task", description: "Mark a task as done.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Task ID" } }, required: ["id"] } }, { name: "delete_task", description: "Permanently delete a task.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Task ID" } }, required: ["id"] } }, // ===== LABELS ===== { name: "list_labels", description: "List all available labels/categories.", inputSchema: { type: "object", properties: {} } }, { name: "create_label", description: "Create a new label/category.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Label name" }, color: { type: "string", description: "Hex color (e.g. #ff5733)" }, domain: { type: "string", enum: ["work", "personal", "neutral"], description: "Domain" } }, required: ["name", "color"] } }, // ===== USER & STATS ===== { name: "get_user_stats", description: "Get current user stats: XP, level, streak, task counts.", inputSchema: { type: "object", properties: {} } }, { name: "get_dashboard", description: "Get a dashboard overview: active tasks, upcoming due, stats.", inputSchema: { type: "object", properties: {} } }, // ===== BULK OPERATIONS ===== { name: "bulk_create_tasks", description: "Create multiple tasks at once. Returns array of created tasks.", inputSchema: { type: "object", properties: { tasks: { type: "array", items: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, priority: { type: "string", enum: ["low", "medium", "high"] }, dueDate: { type: "string" }, labelId: { type: "string" }, estimatedDuration: { type: "number" } }, required: ["title"] }, description: "Array of task objects" } }, required: ["tasks"] } }, { name: "bulk_update_tasks", description: "Update multiple tasks. Each item needs an id + fields to change.", inputSchema: { type: "object", properties: { updates: { type: "array", items: { type: "object", properties: { id: { type: "string" }, status: { type: "string" }, priority: { type: "string" }, dueDate: { type: "string" } }, required: ["id"] } } }, required: ["updates"] } } ]; } private async handleToolCall(name: string, args: any, user: User) { switch (name) { case "list_tasks": { let tasks = await storage.getTasksForUser(user.id); if (!args?.includeCompleted) { tasks = tasks.filter(t => t.status !== "done"); } if (args?.status) tasks = tasks.filter(t => t.status === args.status); if (args?.priority) tasks = tasks.filter(t => t.priority === args.priority); if (args?.labelId) tasks = tasks.filter(t => t.labelId === args.labelId); if (args?.limit) tasks = tasks.slice(0, args.limit); // Return compact format const compact = tasks.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, dueDate: t.dueDate, labelId: t.labelId, estimatedDuration: t.estimatedDuration, timeTracked: t.timeTracked, energyLevel: t.energyLevel })); return { content: [{ type: "text", text: JSON.stringify(compact, null, 2) }] }; } case "get_task": { if (!args.id) throw new Error("Task ID is required"); const task = await storage.getTask(args.id); if (!task || task.userId !== user.id) throw new Error("Task not found"); return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] }; } case "search_tasks": { if (!args.query) throw new Error("Search query is required"); const tasks = await storage.searchTasks(args.query, user.id); const compact = tasks.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, dueDate: t.dueDate })); return { content: [{ type: "text", text: JSON.stringify(compact, null, 2) }] }; } case "create_task": { if (!args.title) throw new Error("Title is required"); const task = await storage.createTask({ title: args.title, description: args.description || "", priority: args.priority || "medium", status: args.status || "todo", isTracking: false, timeTracked: 0, energyLevel: args.energyLevel || "medium", estimatedDuration: args.estimatedDuration || 15, dueDate: args.dueDate || null, notes: args.notes || "", labelId: args.labelId || null, parentTaskId: args.parentTaskId || null, userId: user.id }); await storage.createAuditLog({ userId: user.id, action: "CREATE", entityType: "TASK", entityId: task.id, details: { title: task.title, source: "MCP/API" }, source: "API" }); return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] }; } case "update_task": { if (!args.id) throw new Error("Task ID is required"); const task = await storage.getTask(args.id); if (!task || task.userId !== user.id) throw new Error("Task not found"); const { id, ...updates } = args; const updated = await storage.updateTask(id, updates); await storage.createAuditLog({ userId: user.id, action: "UPDATE", entityType: "TASK", entityId: id, details: { ...updates, source: "MCP/API" }, source: "API" }); return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] }; } case "complete_task": { if (!args.id) throw new Error("Task ID is required"); const task = await storage.getTask(args.id); if (!task || task.userId !== user.id) throw new Error("Task not found"); const updated = await storage.updateTask(args.id, { status: "done" }); await storage.createAuditLog({ userId: user.id, action: "UPDATE", entityType: "TASK", entityId: args.id, details: { status: "done", source: "MCP/API" }, source: "API" }); return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] }; } case "delete_task": { if (!args.id) throw new Error("Task ID is required"); const task = await storage.getTask(args.id); if (!task || task.userId !== user.id) throw new Error("Task not found"); await storage.deleteTask(args.id); await storage.createAuditLog({ userId: user.id, action: "DELETE", entityType: "TASK", entityId: args.id, details: { title: task.title, source: "MCP/API" }, source: "API" }); return { content: [{ type: "text", text: `Task "${task.title}" deleted.` }] }; } case "list_labels": { const labels = await storage.getAllLabels(); const visible = labels.filter(l => l.creatorId === user.id || l.creatorId === null ); return { content: [{ type: "text", text: JSON.stringify(visible, null, 2) }] }; } case "create_label": { if (!args.name || !args.color) throw new Error("Name and color are required"); const label = await storage.createLabel({ name: args.name, color: args.color, domain: args.domain || "neutral", creatorId: user.id }); return { content: [{ type: "text", text: JSON.stringify(label, null, 2) }] }; } case "get_user_stats": { const freshUser = await storage.getUser(user.id); if (!freshUser) throw new Error("User not found"); const tasks = await storage.getTasksForUser(user.id); const active = tasks.filter(t => t.status !== "done"); const done = tasks.filter(t => t.status === "done"); const overdue = active.filter(t => t.dueDate && new Date(t.dueDate) < new Date()); return { content: [{ type: "text", text: JSON.stringify({ username: freshUser.username, xp: freshUser.xp, level: freshUser.level, streak: freshUser.currentStreak, tasks: { total: tasks.length, active: active.length, completed: done.length, overdue: overdue.length, byPriority: { high: active.filter(t => t.priority === "high").length, medium: active.filter(t => t.priority === "medium").length, low: active.filter(t => t.priority === "low").length } } }, null, 2) }] }; } case "get_dashboard": { const tasks = await storage.getTasksForUser(user.id); const labels = await storage.getAllLabels(); const now = new Date(); const in48h = new Date(now.getTime() + 48 * 60 * 60 * 1000); const active = tasks.filter(t => t.status !== "done"); const upcoming = active .filter(t => t.dueDate && new Date(t.dueDate) <= in48h) .sort((a, b) => new Date(a.dueDate!).getTime() - new Date(b.dueDate!).getTime()); const overdue = active.filter(t => t.dueDate && new Date(t.dueDate) < now); const highPriority = active.filter(t => t.priority === "high"); const labelMap = new Map(labels.map(l => [l.id, l.name])); return { content: [{ type: "text", text: JSON.stringify({ summary: { activeTasks: active.length, overdue: overdue.length, dueSoon: upcoming.length, highPriority: highPriority.length }, overdueTasks: overdue.map(t => ({ id: t.id, title: t.title, dueDate: t.dueDate, priority: t.priority, label: labelMap.get(t.labelId || "") || null })), upcomingTasks: upcoming.slice(0, 10).map(t => ({ id: t.id, title: t.title, dueDate: t.dueDate, priority: t.priority, label: labelMap.get(t.labelId || "") || null })), highPriorityTasks: highPriority.map(t => ({ id: t.id, title: t.title, dueDate: t.dueDate, label: labelMap.get(t.labelId || "") || null })) }, null, 2) }] }; } case "bulk_create_tasks": { if (!args.tasks || !Array.isArray(args.tasks)) throw new Error("tasks array is required"); const created = []; for (const t of args.tasks) { if (!t.title) continue; const task = await storage.createTask({ title: t.title, description: t.description || "", priority: t.priority || "medium", status: "todo", isTracking: false, timeTracked: 0, energyLevel: t.energyLevel || "medium", estimatedDuration: t.estimatedDuration || 15, dueDate: t.dueDate || null, notes: "", labelId: t.labelId || null, userId: user.id }); created.push({ id: task.id, title: task.title }); } return { content: [{ type: "text", text: JSON.stringify({ created: created.length, tasks: created }, null, 2) }] }; } case "bulk_update_tasks": { if (!args.updates || !Array.isArray(args.updates)) throw new Error("updates array is required"); const results = []; for (const u of args.updates) { if (!u.id) continue; const task = await storage.getTask(u.id); if (!task || task.userId !== user.id) continue; const { id, ...updates } = u; const updated = await storage.updateTask(id, updates); results.push({ id: updated?.id, title: updated?.title, status: updated?.status }); } return { content: [{ type: "text", text: JSON.stringify({ updated: results.length, tasks: results }, null, 2) }] }; } default: throw new Error(`Tool ${name} not implemented`); } } } export const mcpServer = new McpServer();