├── src ├── utils │ ├── index.ts │ ├── errors.ts │ └── logger.ts ├── services │ ├── index.ts │ ├── ProjectService.ts │ └── TaskService.ts ├── types │ ├── index.ts │ ├── exampleServiceTypes.ts │ └── taskTypes.ts ├── tools │ ├── deleteProjectParams.ts │ ├── getNextTaskParams.ts │ ├── showTaskParams.ts │ ├── exportProjectParams.ts │ ├── createProjectParams.ts │ ├── deleteTaskParams.ts │ ├── importProjectParams.ts │ ├── listTasksParams.ts │ ├── setTaskStatusParams.ts │ ├── expandTaskParams.ts │ ├── addTaskParams.ts │ ├── exportProjectTool.ts │ ├── showTaskTool.ts │ ├── listTasksTool.ts │ ├── getNextTaskTool.ts │ ├── setTaskStatusTool.ts │ ├── importProjectTool.ts │ ├── deleteTaskTool.ts │ ├── deleteProjectTool.ts │ ├── createProjectTool.ts │ ├── expandTaskTool.ts │ ├── updateTaskParams.ts │ ├── addTaskTool.ts │ ├── updateTaskTool.ts │ └── index.ts ├── server.ts ├── createServer.ts ├── db │ ├── schema.sql │ └── DatabaseManager.ts ├── repositories │ ├── ProjectRepository.ts │ └── TaskRepository.ts └── config │ └── ConfigurationManager.ts ├── .prettierrc.json ├── .eslintrc.json ├── package.json ├── tsconfig.json ├── tasks.md ├── public └── images │ └── mcp-task-manager-logo.svg ├── .gitignore ├── README.md └── LICENSE /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger.js'; 2 | export * from './errors.js'; 3 | // Add other util exports here 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": false, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProjectService.js'; 2 | export * from './TaskService.js'; // Added TaskService export 3 | // Remove or comment out ExampleService if it's not being used 4 | // export * from './ExampleService.js'; 5 | // Add other service exports here 6 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Export all types and interfaces from this barrel file 2 | export * from './exampleServiceTypes.js'; 3 | export * from './taskTypes.js'; // Added export for task types 4 | // export * from './yourServiceTypes.js'; // Add new type exports here 5 | 6 | // Define common types used across services/tools if any 7 | export interface CommonContext { 8 | sessionId?: string; 9 | userId?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/exampleServiceTypes.ts: -------------------------------------------------------------------------------- 1 | // Types specific to the ExampleService 2 | 3 | /** 4 | * Configuration options for ExampleService. 5 | */ 6 | export interface ExampleServiceConfig { 7 | greeting: string; 8 | enableDetailedLogs: boolean; 9 | } 10 | 11 | /** 12 | * Data structure handled by ExampleService. 13 | */ 14 | export interface ExampleServiceData { 15 | name: string; 16 | message: string; 17 | processedTimestamp: string; 18 | metrics?: ExampleServiceMetrics; 19 | } 20 | 21 | /** 22 | * Metrics collected during ExampleService processing. 23 | */ 24 | export interface ExampleServiceMetrics { 25 | processingTimeMs: number; 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint", 6 | "prettier" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier" // Make sure this is last 12 | ], 13 | "rules": { 14 | "prettier/prettier": "warn", 15 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], 16 | "@typescript-eslint/no-explicit-any": "warn", // Use warn instead of error initially 17 | "no-console": "off" // Allow console logging for server apps, or configure properly 18 | }, 19 | "env": { 20 | "node": true, 21 | "es2022": true 22 | }, 23 | "parserOptions": { 24 | "ecmaVersion": "latest", 25 | "sourceType": "module" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tools/deleteProjectParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "deleteProject"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Permanently deletes a project and ALL associated tasks and dependencies. 7 | Requires the project ID. This is a highly destructive operation and cannot be undone. 8 | Returns a success confirmation upon completion. 9 | `; 10 | 11 | // Zod schema for the parameters, matching FR-013 12 | export const TOOL_PARAMS = z.object({ 13 | project_id: z.string() 14 | .uuid("The project_id must be a valid UUID.") 15 | .describe("The unique identifier (UUID) of the project to permanently delete. This project must exist."), // Required, UUID format 16 | 17 | }); 18 | 19 | // Define the expected type for arguments based on the Zod schema 20 | export type DeleteProjectArgs = z.infer; 21 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "./createServer.js"; 2 | import { logger } from "./utils/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | // import { WebSocketServerTransport } from "@modelcontextprotocol/sdk/server/ws.js"; // Example for WebSocket 5 | 6 | const main = async () => { 7 | try { 8 | const server = createServer(); 9 | logger.info("Starting MCP server"); 10 | 11 | // Choose your transport 12 | const transport = new StdioServerTransport(); 13 | // const transport = new WebSocketServerTransport({ port: 8080 }); // Example 14 | 15 | logger.info("Connecting transport", { transport: transport.constructor.name }); 16 | await server.connect(transport); 17 | 18 | logger.info("MCP Server connected and listening"); 19 | 20 | } catch (error) { 21 | logger.error("Failed to start server", error); 22 | process.exit(1); // Exit if server fails to start 23 | } 24 | }; 25 | 26 | main(); 27 | -------------------------------------------------------------------------------- /src/createServer.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { ConfigurationManager } from "./config/ConfigurationManager.js"; 3 | import { registerTools } from "./tools/index.js"; 4 | import { logger } from "./utils/index.js"; 5 | 6 | /** 7 | * Creates and configures an MCP server instance. 8 | * This is the central function for server creation and tool registration. 9 | * @returns {McpServer} The configured MCP server instance 10 | */ 11 | export function createServer(): McpServer { 12 | logger.info("Creating MCP server instance"); 13 | 14 | // Initialize the server 15 | const server = new McpServer({ 16 | name: "mcp-server", 17 | version: "1.0.0", 18 | description: "MCP Server based on recommended practices" 19 | }); 20 | 21 | // Get configuration 22 | const configManager = ConfigurationManager.getInstance(); 23 | 24 | // Register all tools 25 | registerTools(server); 26 | 27 | logger.info("MCP server instance created successfully"); 28 | return server; 29 | } 30 | -------------------------------------------------------------------------------- /src/tools/getNextTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "getNextTask"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Identifies and returns the next actionable task within a specified project. 7 | A task is considered actionable if its status is 'todo' and all its dependencies (if any) have a status of 'done'. 8 | If multiple tasks are ready, the one with the highest priority ('high' > 'medium' > 'low') is chosen. 9 | If priorities are equal, the task created earliest is chosen. 10 | Returns the full details of the next task, or null if no task is currently ready. 11 | `; 12 | 13 | // Zod schema for the parameters, matching FR-007 and getNextTaskTool.md spec 14 | export const TOOL_PARAMS = z.object({ 15 | project_id: z.string() 16 | .uuid("The project_id must be a valid UUID.") 17 | .describe("The unique identifier (UUID) of the project to find the next task for."), // Required, UUID format 18 | }); 19 | 20 | // Define the expected type for arguments based on the Zod schema 21 | export type GetNextTaskArgs = z.infer; 22 | -------------------------------------------------------------------------------- /src/tools/showTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "showTask"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Retrieves the full details of a single, specific task, including its dependencies and direct subtasks. 7 | Requires the project ID and the task ID. 8 | Returns a task object containing all details if found. 9 | `; 10 | 11 | // Zod schema for the parameters, matching FR-004 and showTaskTool.md spec 12 | export const TOOL_PARAMS = z.object({ 13 | project_id: z.string() 14 | .uuid("The project_id must be a valid UUID.") 15 | .describe("The unique identifier (UUID) of the project the task belongs to."), // Required, UUID format 16 | 17 | task_id: z.string() 18 | // Add .uuid() if task IDs are also UUIDs, otherwise keep as string 19 | .min(1, "Task ID cannot be empty.") 20 | .describe("The unique identifier of the task to retrieve details for."), // Required, string (or UUID) 21 | }); 22 | 23 | // Define the expected type for arguments based on the Zod schema 24 | export type ShowTaskArgs = z.infer; 25 | -------------------------------------------------------------------------------- /src/tools/exportProjectParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "exportProject"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Exports the complete data set for a specified project as a JSON string. 7 | This includes project metadata, all tasks (hierarchically structured), and their dependencies. 8 | Requires the project ID. The format is fixed to JSON for V1. 9 | Returns the JSON string representing the project data. 10 | `; 11 | 12 | // Zod schema for the parameters, matching FR-009 and exportProjectTool.md spec 13 | export const TOOL_PARAMS = z.object({ 14 | project_id: z.string() 15 | .uuid("The project_id must be a valid UUID.") 16 | .describe("The unique identifier (UUID) of the project to export."), // Required, UUID format 17 | 18 | format: z.literal('json') // Only allow 'json' for V1 19 | .optional() 20 | .default('json') 21 | .describe("Optional format for the export. Currently only 'json' is supported (default)."), // Optional, enum (fixed), default 22 | }); 23 | 24 | // Define the expected type for arguments based on the Zod schema 25 | export type ExportProjectArgs = z.infer; 26 | -------------------------------------------------------------------------------- /src/tools/createProjectParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "createProject"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Creates a new, empty project entry in the Task Management Server database. 7 | This tool is used by clients (e.g., AI agents) to initiate a new workspace for tasks. 8 | It returns the unique identifier (UUID) assigned to the newly created project. 9 | An optional name can be provided; otherwise, a default name including a timestamp will be generated. 10 | `; 11 | 12 | // Define the shape of the parameters for the server.tool method 13 | export const TOOL_PARAMS = { 14 | projectName: z.string() 15 | .max(255, "Project name cannot exceed 255 characters.") // Max length constraint 16 | .optional() // Optional parameter 17 | .describe("Optional human-readable name for the new project (max 255 chars). If omitted, a default name like 'New Project [timestamp]' will be used."), // Detailed description 18 | }; 19 | 20 | // Create a Zod schema object from the shape for validation and type inference 21 | const toolParamsSchema = z.object(TOOL_PARAMS); 22 | 23 | // Define the expected type for arguments based on the Zod schema 24 | export type CreateProjectArgs = z.infer; 25 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom error types for the Task Management Server. 3 | * These can be caught in the service layer and mapped to specific 4 | * McpError codes in the tool layer. 5 | */ 6 | 7 | // Example: Base service error 8 | export class ServiceError extends Error { 9 | constructor(message: string, public details?: any) { 10 | super(message); 11 | this.name = 'ServiceError'; 12 | } 13 | } 14 | 15 | // Example: Validation specific error 16 | export class ValidationError extends ServiceError { 17 | constructor(message: string, details?: any) { 18 | super(message, details); 19 | this.name = 'ValidationError'; 20 | } 21 | } 22 | 23 | // Example: Not found specific error 24 | export class NotFoundError extends ServiceError { 25 | constructor(message: string = "Resource not found", details?: any) { 26 | super(message, details); 27 | this.name = 'NotFoundError'; 28 | } 29 | } 30 | 31 | // Example: Conflict specific error (e.g., trying to create something that exists) 32 | export class ConflictError extends ServiceError { 33 | constructor(message: string = "Resource conflict", details?: any) { 34 | super(message, details); 35 | this.name = 'ConflictError'; 36 | } 37 | } 38 | 39 | // Add other custom error types as needed 40 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { pino, Logger } from 'pino'; // Try named import for the function 2 | 3 | /** 4 | * Pino logger instance configured for structured JSON logging to stderr. 5 | * MCP servers typically use stdout for protocol messages, so logs go to stderr. 6 | */ 7 | export const logger: Logger = pino( 8 | { 9 | level: process.env.LOG_LEVEL || 'info', // Default to 'info', configurable via env var 10 | formatters: { 11 | level: (label: string) => { // Add type for label 12 | // Standardize level labels if desired, e.g., uppercase 13 | return { level: label.toUpperCase() }; 14 | }, 15 | // bindings: (bindings) => { 16 | // // Add custom bindings if needed, e.g., hostname, pid 17 | // return { pid: bindings.pid, hostname: bindings.hostname }; 18 | // }, 19 | }, 20 | timestamp: pino.stdTimeFunctions.isoTime, // Use ISO 8601 timestamps 21 | }, 22 | pino.destination(2) // Direct output to stderr (file descriptor 2) 23 | ); 24 | 25 | // Example usage (replace console.log/error calls throughout the app): 26 | // logger.info('Server starting...'); 27 | // logger.debug({ userId: '123' }, 'User logged in'); 28 | // logger.error(new Error('Something failed'), 'Failed to process request'); 29 | -------------------------------------------------------------------------------- /src/tools/deleteTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "deleteTask"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Deletes one or more tasks within a specified project. 7 | Requires the project ID and an array of task IDs to delete. 8 | Note: Deleting a task also deletes its subtasks and dependency links due to database cascade rules. 9 | Returns the count of successfully deleted tasks. 10 | `; 11 | 12 | // Zod schema for the parameters, matching FR-012 13 | export const TOOL_PARAMS = z.object({ 14 | project_id: z.string() 15 | .uuid("The project_id must be a valid UUID.") 16 | .describe("The unique identifier (UUID) of the project containing the tasks to delete. This project must exist."), // Required, UUID format 17 | 18 | task_ids: z.array( 19 | z.string() 20 | .uuid("Each task ID must be a valid UUID.") 21 | .describe("A unique identifier (UUID) of a task to delete.") 22 | ) 23 | .min(1, "At least one task ID must be provided.") 24 | .max(100, "Cannot delete more than 100 tasks per call.") 25 | .describe("An array of task IDs (UUIDs, 1-100) to be deleted from the specified project."), // Required, array of UUID strings, limits 26 | 27 | }); 28 | 29 | // Define the expected type for arguments based on the Zod schema 30 | export type DeleteTaskArgs = z.infer; 31 | -------------------------------------------------------------------------------- /src/tools/importProjectParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "importProject"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Creates a *new* project by importing data from a JSON string. 7 | The JSON data must conform to the structure previously generated by the 'exportProject' tool. 8 | Performs validation on the input data (parsing, basic structure, size limit). 9 | Returns the unique project_id of the newly created project upon success. 10 | `; 11 | 12 | // Zod schema for the parameters, matching FR-010 and importProjectTool.md spec 13 | export const TOOL_PARAMS = z.object({ 14 | project_data: z.string() 15 | .min(1, "Project data cannot be empty.") 16 | // Size validation happens in the service layer before parsing 17 | .describe("Required. A JSON string containing the full project data, conforming to the export structure. Max size e.g., 10MB."), // Required, string 18 | 19 | new_project_name: z.string() 20 | .max(255, "New project name cannot exceed 255 characters.") 21 | .optional() 22 | .describe("Optional name for the newly created project (max 255 chars). If omitted, a name based on the original project name and import timestamp will be used."), // Optional, string, max length 23 | }); 24 | 25 | // Define the expected type for arguments based on the Zod schema 26 | export type ImportProjectArgs = z.infer; 27 | -------------------------------------------------------------------------------- /src/tools/listTasksParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "listTasks"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Retrieves a list of tasks for a specified project. 7 | Allows optional filtering by task status ('todo', 'in-progress', 'review', 'done'). 8 | Provides an option to include nested subtasks directly within their parent task objects in the response. 9 | Returns an array of task objects. 10 | `; 11 | 12 | // Re-use enum from addTaskParams or define locally if preferred 13 | const TaskStatusEnum = z.enum(['todo', 'in-progress', 'review', 'done']); 14 | 15 | // Zod schema for the parameters, matching FR-003 and listTasksTool.md spec 16 | export const TOOL_PARAMS = z.object({ 17 | project_id: z.string() 18 | .uuid("The project_id must be a valid UUID.") 19 | .describe("The unique identifier (UUID) of the project whose tasks are to be listed. This project must exist."), // Required, UUID format 20 | 21 | status: TaskStatusEnum 22 | .optional() 23 | .describe("Optional filter to return only tasks matching the specified status."), // Optional, enum 24 | 25 | include_subtasks: z.boolean() 26 | .optional() 27 | .default(false) // Default value 28 | .describe("Optional flag (default false). If true, the response will include subtasks nested within their parent tasks."), // Optional, boolean, default 29 | }); 30 | 31 | // Define the expected type for arguments based on the Zod schema 32 | export type ListTasksArgs = z.infer; 33 | -------------------------------------------------------------------------------- /src/tools/setTaskStatusParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "setTaskStatus"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Updates the status ('todo', 'in-progress', 'review', 'done') for one or more tasks within a specified project. 7 | Requires the project ID, an array of task IDs (1-100), and the target status. 8 | Verifies all tasks exist in the project before updating. Returns the count of updated tasks. 9 | `; 10 | 11 | // Re-use enum from other param files 12 | const TaskStatusEnum = z.enum(['todo', 'in-progress', 'review', 'done']); 13 | 14 | // Zod schema for the parameters, matching FR-005 and setTaskStatusTool.md spec 15 | export const TOOL_PARAMS = z.object({ 16 | project_id: z.string() 17 | .uuid("The project_id must be a valid UUID.") 18 | .describe("The unique identifier (UUID) of the project containing the tasks."), // Required, UUID format 19 | 20 | task_ids: z.array( 21 | z.string().min(1, "Task ID cannot be empty.") 22 | // Add .uuid() if task IDs are UUIDs 23 | .describe("A unique identifier of a task to update.") 24 | ) 25 | .min(1, "At least one task ID must be provided.") 26 | .max(100, "Cannot update more than 100 tasks per call.") 27 | .describe("An array of task IDs (1-100) whose status should be updated."), // Required, array of strings, limits 28 | 29 | status: TaskStatusEnum 30 | .describe("The target status to set for the specified tasks."), // Required, enum 31 | }); 32 | 33 | // Define the expected type for arguments based on the Zod schema 34 | export type SetTaskStatusArgs = z.infer; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-task-manager-server", 3 | "version": "0.1.0", 4 | "description": "My new MCP Server", 5 | "main": "dist/server.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node dist/server.js", 9 | "build": "tsc && copyfiles -f src/db/*.sql dist/db", 10 | "dev": "nodemon --watch src --ext ts --exec \"node --loader ts-node/esm src/server.ts\"", 11 | "lint": "eslint . --ext .ts", 12 | "format": "prettier --write \"src/**/*.ts\"", 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "prepare": "husky install || true" 15 | }, 16 | "keywords": [ 17 | "mcp", 18 | "model-context-protocol" 19 | ], 20 | "license": "ISC", 21 | "dependencies": { 22 | "@modelcontextprotocol/sdk": "^1.9.0", 23 | "@types/better-sqlite3": "^7.6.13", 24 | "@types/inquirer": "^9.0.7", 25 | "@types/uuid": "^10.0.0", 26 | "better-sqlite3": "^11.9.1", 27 | "chalk": "^5.3.0", 28 | "inquirer": "^12.5.0", 29 | "pino": "^9.6.0", 30 | "uuid": "^11.1.0", 31 | "zod": "^3.23.8" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20.14.2", 35 | "@typescript-eslint/eslint-plugin": "^7.13.0", 36 | "@typescript-eslint/parser": "^7.13.0", 37 | "copyfiles": "^2.4.1", 38 | "eslint": "^8.57.0", 39 | "eslint-config-prettier": "^9.1.0", 40 | "eslint-plugin-prettier": "^5.1.3", 41 | "husky": "^9.0.11", 42 | "lint-staged": "^15.2.5", 43 | "nodemon": "^3.1.3", 44 | "prettier": "^3.3.2", 45 | "ts-node": "^10.9.2", 46 | "typescript": "^5.4.5" 47 | }, 48 | "lint-staged": { 49 | "*.ts": [ 50 | "eslint --fix", 51 | "prettier --write" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | "module": "NodeNext", /* Specify what module code is generated. */ 5 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 7 | "rootDir": "./src", /* Specify the root folder within your source files. */ 8 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 9 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 10 | "strict": true, /* Enable all strict type-checking options. */ 11 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 12 | "resolveJsonModule": true, /* Enable importing .json files */ 13 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 14 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 15 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 16 | "allowJs": true, /* Allow JavaScript files to be a part of your program. */ 17 | }, 18 | "ts-node": { /* ts-node specific options */ 19 | "transpileOnly": true, /* Skip type checking for faster execution */ 20 | "files": true /* Include files in tsconfig.json */ 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], /* Specifies an array of filenames or patterns to include in the program */ 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ] /* Specifies an array of filenames or patterns that should be skipped when resolving include */ 29 | } -------------------------------------------------------------------------------- /tasks.md: -------------------------------------------------------------------------------- 1 | # Task Manager Server - Development Tasks 2 | 3 | This file tracks the implementation progress based on the defined milestones. 4 | 5 | ## Milestone 1: Core Setup & `createProject` Tool 6 | 7 | - [x] **Create `tasks.md`:** Initial file creation. 8 | - [x] **Define DB Schema:** Create `src/db/schema.sql` with tables and indexes. 9 | - [x] **Implement DB Manager:** Create `src/db/DatabaseManager.ts` for connection, init, WAL. 10 | - [x] **Update Config:** Ensure `src/config/ConfigurationManager.ts` handles DB path. 11 | - [x] **Implement Project Repo:** Create `src/repositories/ProjectRepository.ts` with `create` method. 12 | - [x] **Implement Project Service:** Create `src/services/ProjectService.ts` with `createProject` method. 13 | - [x] **Implement `createProject` Params:** Create `src/tools/createProjectParams.ts`. 14 | - [x] **Implement `createProject` Tool:** Create `src/tools/createProjectTool.ts`. 15 | - [x] **Implement Utilities:** Create/update `src/utils/logger.ts`, `src/utils/errors.ts`, `src/utils/index.ts`. 16 | - [x] **Update Server Setup:** Modify `src/server.ts`, `src/createServer.ts`, `src/tools/index.ts`, `src/services/index.ts`. 17 | - [ ] **Write Tests:** Unit test `ProjectService`, Integration test `createProject` tool. *(Skipped/Deferred)* 18 | 19 | ## Milestone 2: Core Task Management Tools 20 | 21 | - [x] Implement `addTask` tool (FR-002) 22 | - [x] Implement `listTasks` tool (FR-003) 23 | - [x] Implement `showTask` tool (FR-004) 24 | - [x] Implement `setTaskStatus` tool (FR-005) 25 | 26 | ## Milestone 3: Advanced & I/O Tools 27 | 28 | - [x] Implement `expandTask` tool (FR-006) 29 | - [x] Implement `getNextTask` tool (FR-007) 30 | - [x] Implement `exportProject` tool (FR-009) 31 | - [x] Implement `importProject` tool (FR-010) 32 | - [x] Implement structured logging (NFR-006). 33 | - [x] Finalize documentation (README, tool descriptions). 34 | -------------------------------------------------------------------------------- /public/images/mcp-task-manager-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | MCP TASKS 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/types/taskTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the possible status values for a task. 3 | * Using string literal union as per .clinerules (no enums). 4 | */ 5 | export type TaskStatus = 'todo' | 'in-progress' | 'review' | 'done'; 6 | 7 | /** 8 | * Represents the possible priority levels for a task. 9 | * Using string literal union as per .clinerules (no enums). 10 | */ 11 | export type TaskPriority = 'high' | 'medium' | 'low'; 12 | 13 | /** 14 | * Interface representing a Task object as returned by the API. 15 | */ 16 | export interface Task { 17 | task_id: string; // UUID format 18 | project_id: string; // UUID format 19 | parent_task_id: string | null; // UUID format or null 20 | description: string; 21 | status: TaskStatus; 22 | priority: TaskPriority; 23 | created_at: string; // ISO8601 format 24 | updated_at: string; // ISO8601 format 25 | dependencies?: string[]; // Array of task_ids this task depends on 26 | subtasks?: Task[]; // Array of subtasks (populated if requested, e.g., listTasks with include_subtasks=true) 27 | } 28 | 29 | /** 30 | * Interface representing the payload for updating a task (FR-011). 31 | * All fields are optional, but at least one must be provided for an update. 32 | */ 33 | export interface TaskUpdatePayload { 34 | description?: string; 35 | priority?: TaskPriority; 36 | dependencies?: string[]; // Represents the complete new list of dependencies 37 | } 38 | 39 | /** 40 | * Interface representing the structure of a Task as stored in the database. 41 | * May differ slightly from the API representation (e.g., no nested subtasks/dependencies). 42 | */ 43 | export interface TaskDbObject { 44 | task_id: string; 45 | project_id: string; 46 | parent_task_id: string | null; 47 | description: string; 48 | status: TaskStatus; 49 | priority: TaskPriority; 50 | created_at: string; 51 | updated_at: string; 52 | } 53 | 54 | /** 55 | * Interface representing a record in the task_dependencies table. 56 | */ 57 | export interface TaskDependencyDbObject { 58 | task_id: string; 59 | depends_on_task_id: string; 60 | } 61 | -------------------------------------------------------------------------------- /src/tools/expandTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "expandTask"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Breaks down a specified parent task into multiple subtasks based on provided descriptions. 7 | Requires the project ID, the parent task ID, and an array of descriptions for the new subtasks. 8 | Optionally allows forcing the replacement of existing subtasks using the 'force' flag. 9 | Returns the updated parent task details, including the newly created subtasks. 10 | `; 11 | 12 | // Zod schema for the parameters, matching FR-006 and expandTaskTool.md spec 13 | export const TOOL_PARAMS = z.object({ 14 | project_id: z.string() 15 | .uuid("The project_id must be a valid UUID.") 16 | .describe("The unique identifier (UUID) of the project containing the parent task."), // Required, UUID format 17 | 18 | task_id: z.string() 19 | // Add .uuid() if task IDs are also UUIDs 20 | .min(1, "Parent task ID cannot be empty.") 21 | .describe("The unique identifier of the parent task to be expanded."), // Required, string (or UUID) 22 | 23 | subtask_descriptions: z.array( 24 | z.string() 25 | .min(1, "Subtask description cannot be empty.") 26 | .max(512, "Subtask description cannot exceed 512 characters.") 27 | .describe("A textual description for one of the new subtasks (1-512 characters).") 28 | ) 29 | .min(1, "At least one subtask description must be provided.") 30 | .max(20, "Cannot create more than 20 subtasks per call.") 31 | .describe("An array of descriptions (1-20) for the new subtasks to be created under the parent task."), // Required, array of strings, limits 32 | 33 | force: z.boolean() 34 | .optional() 35 | .default(false) 36 | .describe("Optional flag (default false). If true, any existing subtasks of the parent task will be deleted before creating the new ones. If false and subtasks exist, the operation will fail."), // Optional, boolean, default 37 | }); 38 | 39 | // Define the expected type for arguments based on the Zod schema 40 | export type ExpandTaskArgs = z.infer; 41 | -------------------------------------------------------------------------------- /src/tools/addTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const TOOL_NAME = "addTask"; 4 | 5 | export const TOOL_DESCRIPTION = ` 6 | Adds a new task to a specified project within the Task Management Server. 7 | Requires the project ID and a description for the task. 8 | Optionally accepts a list of dependency task IDs, a priority level, and an initial status. 9 | Returns the full details of the newly created task upon success. 10 | `; 11 | 12 | // Allowed enum values for status and priority 13 | const TaskStatusEnum = z.enum(['todo', 'in-progress', 'review', 'done']); 14 | const TaskPriorityEnum = z.enum(['high', 'medium', 'low']); 15 | 16 | // Zod schema for the parameters, matching FR-002 and addTaskTool.md spec 17 | export const TOOL_PARAMS = z.object({ 18 | project_id: z.string() 19 | .uuid("The project_id must be a valid UUID.") 20 | .describe("The unique identifier (UUID) of the project to add the task to. This project must already exist."), // Required, UUID format 21 | 22 | description: z.string() 23 | .min(1, "Task description cannot be empty.") 24 | .max(1024, "Task description cannot exceed 1024 characters.") 25 | .describe("The textual description of the task to be performed (1-1024 characters)."), // Required, length limits 26 | 27 | dependencies: z.array(z.string().describe("A task ID that this new task depends on.")) // Allow any string for now, existence checked in service (or deferred) 28 | .max(50, "A task cannot have more than 50 dependencies.") 29 | .optional() 30 | .describe("An optional list of task IDs (strings) that must be completed before this task can start (max 50)."), // Optional, array of strings, count limit 31 | 32 | priority: TaskPriorityEnum 33 | .optional() 34 | .default('medium') // Default value 35 | .describe("Optional task priority. Defaults to 'medium' if not specified."), // Optional, enum, default 36 | 37 | status: TaskStatusEnum 38 | .optional() 39 | .default('todo') // Default value 40 | .describe("Optional initial status of the task. Defaults to 'todo' if not specified."), // Optional, enum, default 41 | }); 42 | 43 | // Define the expected type for arguments based on the Zod schema 44 | export type AddTaskArgs = z.infer; 45 | -------------------------------------------------------------------------------- /src/tools/exportProjectTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, ExportProjectArgs } from "./exportProjectParams.js"; 4 | import { ProjectService } from "../services/ProjectService.js"; // Assuming ProjectService is exported via services/index.js 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; 7 | 8 | /** 9 | * Registers the exportProject tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param projectService - An instance of the ProjectService. 13 | */ 14 | export const exportProjectTool = (server: McpServer, projectService: ProjectService): void => { 15 | 16 | const processRequest = async (args: ExportProjectArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 18 | try { 19 | // Zod schema ensures format is 'json' if provided, or defaults to 'json' 20 | const jsonString = await projectService.exportProject(args.project_id); 21 | 22 | // Format the successful response 23 | logger.info(`[${TOOL_NAME}] Successfully exported project ${args.project_id}`); 24 | return { 25 | content: [{ 26 | type: "text" as const, 27 | text: jsonString // Return the JSON string directly 28 | }] 29 | }; 30 | } catch (error: unknown) { 31 | // Handle potential errors 32 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 33 | 34 | if (error instanceof NotFoundError) { 35 | // Project not found 36 | throw new McpError(ErrorCode.InvalidParams, error.message); 37 | } else { 38 | // Generic internal error 39 | const message = error instanceof Error ? error.message : 'An unknown error occurred while exporting the project.'; 40 | throw new McpError(ErrorCode.InternalError, message); 41 | } 42 | } 43 | }; 44 | 45 | // Register the tool with the server 46 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 47 | 48 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 49 | }; 50 | -------------------------------------------------------------------------------- /src/tools/showTaskTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, ShowTaskArgs } from "./showTaskParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; 7 | 8 | /** 9 | * Registers the showTask tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const showTaskTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | const processRequest = async (args: ShowTaskArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 18 | try { 19 | // Call the service method to get the task details 20 | const task = await taskService.getTaskById(args.project_id, args.task_id); 21 | 22 | // Format the successful response 23 | logger.info(`[${TOOL_NAME}] Found task ${args.task_id} in project ${args.project_id}`); 24 | return { 25 | content: [{ 26 | type: "text" as const, 27 | text: JSON.stringify(task) // Return the full task object 28 | }] 29 | }; 30 | } catch (error: unknown) { 31 | // Handle potential errors 32 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 33 | 34 | if (error instanceof NotFoundError) { 35 | // Specific error if the project or task wasn't found 36 | // Map to InvalidParams as the provided ID(s) are invalid in this context 37 | throw new McpError(ErrorCode.InvalidParams, error.message); 38 | } else { 39 | // Generic internal error 40 | const message = error instanceof Error ? error.message : 'An unknown error occurred while retrieving the task.'; 41 | throw new McpError(ErrorCode.InternalError, message); 42 | } 43 | } 44 | }; 45 | 46 | // Register the tool with the server 47 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 48 | 49 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 50 | }; 51 | -------------------------------------------------------------------------------- /src/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Database schema for the MCP Task Manager Server 2 | -- Based on RFC-2025-001 3 | 4 | -- Enable foreign key support 5 | PRAGMA foreign_keys = ON; 6 | 7 | -- Use Write-Ahead Logging for better concurrency 8 | PRAGMA journal_mode = WAL; 9 | 10 | -- Table: projects 11 | -- Stores project metadata 12 | CREATE TABLE IF NOT EXISTS projects ( 13 | project_id TEXT PRIMARY KEY NOT NULL, -- UUID format 14 | name TEXT NOT NULL, 15 | created_at TEXT NOT NULL -- ISO8601 format (e.g., YYYY-MM-DDTHH:MM:SS.SSSZ) 16 | ); 17 | 18 | -- Table: tasks 19 | -- Stores individual task details 20 | CREATE TABLE IF NOT EXISTS tasks ( 21 | task_id TEXT PRIMARY KEY NOT NULL, -- UUID format 22 | project_id TEXT NOT NULL, 23 | parent_task_id TEXT NULL, -- For subtasks 24 | description TEXT NOT NULL, 25 | status TEXT NOT NULL CHECK(status IN ('todo', 'in-progress', 'review', 'done')), 26 | priority TEXT NOT NULL CHECK(priority IN ('high', 'medium', 'low')), 27 | created_at TEXT NOT NULL, -- ISO8601 format 28 | updated_at TEXT NOT NULL, -- ISO8601 format 29 | FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE, 30 | FOREIGN KEY (parent_task_id) REFERENCES tasks(task_id) ON DELETE CASCADE 31 | ); 32 | 33 | -- Table: task_dependencies 34 | -- Stores prerequisite relationships between tasks 35 | CREATE TABLE IF NOT EXISTS task_dependencies ( 36 | task_id TEXT NOT NULL, -- The task that depends on another 37 | depends_on_task_id TEXT NOT NULL, -- The task that must be completed first 38 | PRIMARY KEY (task_id, depends_on_task_id), 39 | FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE, 40 | FOREIGN KEY (depends_on_task_id) REFERENCES tasks(task_id) ON DELETE CASCADE 41 | ); 42 | 43 | -- Indexes for performance optimization 44 | 45 | -- Index on tasks table 46 | CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id); 47 | CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); 48 | CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority); 49 | CREATE INDEX IF NOT EXISTS idx_tasks_parent_task_id ON tasks(parent_task_id); 50 | CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at); 51 | 52 | -- Indexes on task_dependencies table 53 | CREATE INDEX IF NOT EXISTS idx_task_dependencies_task_id ON task_dependencies(task_id); 54 | CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on_task_id ON task_dependencies(depends_on_task_id); 55 | -------------------------------------------------------------------------------- /src/tools/listTasksTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, ListTasksArgs } from "./listTasksParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; 7 | 8 | /** 9 | * Registers the listTasks tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const listTasksTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | const processRequest = async (args: ListTasksArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 18 | try { 19 | // Call the service method to list tasks 20 | const tasks = await taskService.listTasks({ 21 | project_id: args.project_id, 22 | status: args.status, 23 | include_subtasks: args.include_subtasks, 24 | }); 25 | 26 | // Format the successful response 27 | logger.info(`[${TOOL_NAME}] Found ${tasks.length} tasks for project ${args.project_id}`); 28 | return { 29 | content: [{ 30 | type: "text" as const, 31 | text: JSON.stringify(tasks) // Return the array of task objects 32 | }] 33 | }; 34 | } catch (error: unknown) { 35 | // Handle potential errors 36 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 37 | 38 | if (error instanceof NotFoundError) { 39 | // Specific error if the project wasn't found 40 | throw new McpError(ErrorCode.InvalidParams, error.message); // Map NotFound to InvalidParams for project_id 41 | } else { 42 | // Generic internal error 43 | const message = error instanceof Error ? error.message : 'An unknown error occurred while listing tasks.'; 44 | throw new McpError(ErrorCode.InternalError, message); 45 | } 46 | } 47 | }; 48 | 49 | // Register the tool with the server 50 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 51 | 52 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 53 | }; 54 | -------------------------------------------------------------------------------- /src/tools/getNextTaskTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, GetNextTaskArgs } from "./getNextTaskParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; 7 | 8 | /** 9 | * Registers the getNextTask tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const getNextTaskTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | const processRequest = async (args: GetNextTaskArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 18 | try { 19 | // Call the service method to get the next task 20 | const nextTask = await taskService.getNextTask(args.project_id); 21 | 22 | // Format the successful response 23 | if (nextTask) { 24 | logger.info(`[${TOOL_NAME}] Next task found: ${nextTask.task_id} in project ${args.project_id}`); 25 | } else { 26 | logger.info(`[${TOOL_NAME}] No ready task found for project ${args.project_id}`); 27 | } 28 | 29 | return { 30 | content: [{ 31 | type: "text" as const, 32 | // Return the full task object or null 33 | text: JSON.stringify(nextTask) 34 | }] 35 | }; 36 | } catch (error: unknown) { 37 | // Handle potential errors 38 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 39 | 40 | if (error instanceof NotFoundError) { 41 | // Project not found 42 | throw new McpError(ErrorCode.InvalidParams, error.message); 43 | } else { 44 | // Generic internal error 45 | const message = error instanceof Error ? error.message : 'An unknown error occurred while getting the next task.'; 46 | throw new McpError(ErrorCode.InternalError, message); 47 | } 48 | } 49 | }; 50 | 51 | // Register the tool with the server 52 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 53 | 54 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 55 | }; 56 | -------------------------------------------------------------------------------- /src/tools/setTaskStatusTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, SetTaskStatusArgs } from "./setTaskStatusParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; 7 | 8 | /** 9 | * Registers the setTaskStatus tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const setTaskStatusTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | const processRequest = async (args: SetTaskStatusArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 18 | try { 19 | // Call the service method to update the status 20 | const updatedCount = await taskService.setTaskStatus( 21 | args.project_id, 22 | args.task_ids, 23 | args.status 24 | ); 25 | 26 | // Format the successful response 27 | const responsePayload = { success: true, updated_count: updatedCount }; 28 | logger.info(`[${TOOL_NAME}] Updated status for ${updatedCount} tasks in project ${args.project_id}`); 29 | return { 30 | content: [{ 31 | type: "text" as const, 32 | text: JSON.stringify(responsePayload) 33 | }] 34 | }; 35 | } catch (error: unknown) { 36 | // Handle potential errors 37 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 38 | 39 | if (error instanceof NotFoundError) { 40 | // Specific error if the project or any task wasn't found 41 | // Map to InvalidParams as the provided ID(s) are invalid 42 | throw new McpError(ErrorCode.InvalidParams, error.message); 43 | } else { 44 | // Generic internal error 45 | const message = error instanceof Error ? error.message : 'An unknown error occurred while setting task status.'; 46 | throw new McpError(ErrorCode.InternalError, message); 47 | } 48 | } 49 | }; 50 | 51 | // Register the tool with the server 52 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 53 | 54 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 55 | }; 56 | -------------------------------------------------------------------------------- /src/tools/importProjectTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, ImportProjectArgs } from "./importProjectParams.js"; 4 | import { ProjectService } from "../services/ProjectService.js"; 5 | import { logger } from '../utils/logger.js'; 6 | import { ValidationError } from "../utils/errors.js"; // Import specific errors 7 | 8 | /** 9 | * Registers the importProject tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param projectService - An instance of the ProjectService. 13 | */ 14 | export const importProjectTool = (server: McpServer, projectService: ProjectService): void => { 15 | 16 | const processRequest = async (args: ImportProjectArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request (project name: ${args.new_project_name || 'Default'})`); 18 | try { 19 | // Call the service method to import the project 20 | const result = await projectService.importProject( 21 | args.project_data, 22 | args.new_project_name 23 | ); 24 | 25 | // Format the successful response 26 | const responsePayload = { project_id: result.project_id }; 27 | logger.info(`[${TOOL_NAME}] Successfully imported project. New ID: ${result.project_id}`); 28 | return { 29 | content: [{ 30 | type: "text" as const, 31 | text: JSON.stringify(responsePayload) 32 | }] 33 | }; 34 | } catch (error: unknown) { 35 | // Handle potential errors 36 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 37 | 38 | if (error instanceof ValidationError) { 39 | // JSON parsing, schema validation, size limit, or other data issues 40 | throw new McpError(ErrorCode.InvalidParams, error.message); 41 | } else { 42 | // Generic internal error (likely database related from the transaction) 43 | const message = error instanceof Error ? error.message : 'An unknown error occurred during project import.'; 44 | throw new McpError(ErrorCode.InternalError, message); 45 | } 46 | } 47 | }; 48 | 49 | // Register the tool with the server 50 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 51 | 52 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 53 | }; 54 | -------------------------------------------------------------------------------- /src/tools/deleteTaskTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, DeleteTaskArgs } from "./deleteTaskParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; // Assuming TaskService is exported from index 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; // Import custom errors 7 | 8 | /** 9 | * Registers the deleteTask tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const deleteTaskTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | const processRequest = async (args: DeleteTaskArgs): Promise<{ content: { type: 'text', text: string }[] }> => { 17 | logger.info(`[${TOOL_NAME}] Received request to delete ${args.task_ids.length} tasks from project ${args.project_id}`); 18 | try { 19 | // Call the service method to delete the tasks 20 | const deletedCount = await taskService.deleteTasks(args.project_id, args.task_ids); 21 | 22 | // Format the successful response 23 | logger.info(`[${TOOL_NAME}] Successfully deleted ${deletedCount} tasks from project ${args.project_id}`); 24 | return { 25 | content: [{ 26 | type: "text" as const, 27 | text: JSON.stringify({ success: true, deleted_count: deletedCount }) 28 | }] 29 | }; 30 | } catch (error: unknown) { 31 | // Handle potential errors according to systemPatterns.md mapping 32 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 33 | 34 | if (error instanceof NotFoundError) { 35 | // Project or one/more tasks not found - Map to InvalidParams as per convention 36 | throw new McpError(ErrorCode.InvalidParams, error.message); 37 | } else { 38 | // Generic internal error 39 | const message = error instanceof Error ? error.message : 'An unknown error occurred while deleting tasks.'; 40 | throw new McpError(ErrorCode.InternalError, message); 41 | } 42 | } 43 | }; 44 | 45 | // Register the tool with the server 46 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); // Using .shape as this schema doesn't use .refine() 47 | 48 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 49 | }; 50 | -------------------------------------------------------------------------------- /src/tools/deleteProjectTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, DeleteProjectArgs } from "./deleteProjectParams.js"; 4 | import { ProjectService } from "../services/ProjectService.js"; // Assuming ProjectService is exported from index 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError } from "../utils/errors.js"; // Import custom errors 7 | 8 | /** 9 | * Registers the deleteProject tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param projectService - An instance of the ProjectService. 13 | */ 14 | export const deleteProjectTool = (server: McpServer, projectService: ProjectService): void => { 15 | 16 | const processRequest = async (args: DeleteProjectArgs): Promise<{ content: { type: 'text', text: string }[] }> => { 17 | logger.warn(`[${TOOL_NAME}] Received request to DELETE project ${args.project_id}. This is a destructive operation.`); // Log deletion intent clearly 18 | try { 19 | // Call the service method to delete the project 20 | const success = await projectService.deleteProject(args.project_id); 21 | 22 | // Format the successful response 23 | logger.info(`[${TOOL_NAME}] Successfully deleted project ${args.project_id}`); 24 | return { 25 | content: [{ 26 | type: "text" as const, 27 | text: JSON.stringify({ success: success }) // Return true if deleted 28 | }] 29 | }; 30 | } catch (error: unknown) { 31 | // Handle potential errors according to systemPatterns.md mapping 32 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 33 | 34 | if (error instanceof NotFoundError) { 35 | // Project not found - Map to InvalidParams as per convention 36 | throw new McpError(ErrorCode.InvalidParams, error.message); 37 | } else { 38 | // Generic internal error 39 | const message = error instanceof Error ? error.message : 'An unknown error occurred while deleting the project.'; 40 | throw new McpError(ErrorCode.InternalError, message); 41 | } 42 | } 43 | }; 44 | 45 | // Register the tool with the server 46 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); // Using .shape as this schema doesn't use .refine() 47 | 48 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 49 | }; 50 | -------------------------------------------------------------------------------- /src/tools/createProjectTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, CreateProjectArgs } from "./createProjectParams.js"; 4 | import { ProjectService } from "../services/ProjectService.js"; // Assuming ProjectService is exported from services/index.js or directly 5 | import { logger } from '../utils/logger.js'; // Assuming logger exists 6 | // Import custom errors if needed for specific mapping 7 | // import { ServiceError } from "../utils/errors.js"; 8 | 9 | /** 10 | * Registers the createProject tool with the MCP server. 11 | * 12 | * @param server - The McpServer instance. 13 | * @param projectService - An instance of the ProjectService. 14 | */ 15 | export const createProjectTool = (server: McpServer, projectService: ProjectService): void => { 16 | 17 | // Define the asynchronous function that handles the actual tool logic 18 | const processRequest = async (args: CreateProjectArgs) => { 19 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 20 | try { 21 | // Call the service method to create the project 22 | const newProject = await projectService.createProject(args.projectName); 23 | 24 | // Format the successful response according to MCP standards 25 | const responsePayload = { project_id: newProject.project_id }; 26 | logger.info(`[${TOOL_NAME}] Project created successfully: ${newProject.project_id}`); 27 | 28 | return { 29 | content: [{ 30 | type: "text" as const, // Required type assertion 31 | text: JSON.stringify(responsePayload) 32 | }] 33 | }; 34 | } catch (error: unknown) { 35 | // Handle potential errors from the service layer 36 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 37 | 38 | // Basic error mapping: Assume internal error unless a specific known error type is caught 39 | // TODO: Add more specific error mapping if ProjectService throws custom errors 40 | // (e.g., catch (error instanceof ValidationError) { throw new McpError(ErrorCode.InvalidParams, ...)}) 41 | const message = error instanceof Error ? error.message : 'An unknown error occurred during project creation.'; 42 | throw new McpError(ErrorCode.InternalError, message); 43 | } 44 | }; 45 | 46 | // Register the tool with the server 47 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, processRequest); 48 | 49 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 50 | }; 51 | -------------------------------------------------------------------------------- /src/tools/expandTaskTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, ExpandTaskArgs } from "./expandTaskParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError, ConflictError } from "../utils/errors.js"; // Import specific errors 7 | 8 | /** 9 | * Registers the expandTask tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const expandTaskTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | const processRequest = async (args: ExpandTaskArgs) => { 17 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 18 | try { 19 | // Call the service method to expand the task 20 | const updatedParentTask = await taskService.expandTask({ 21 | project_id: args.project_id, 22 | task_id: args.task_id, 23 | subtask_descriptions: args.subtask_descriptions, 24 | force: args.force, 25 | }); 26 | 27 | // Format the successful response 28 | logger.info(`[${TOOL_NAME}] Successfully expanded task ${args.task_id} in project ${args.project_id}`); 29 | return { 30 | content: [{ 31 | type: "text" as const, 32 | // Return the updated parent task details, including new subtasks 33 | text: JSON.stringify(updatedParentTask) 34 | }] 35 | }; 36 | } catch (error: unknown) { 37 | // Handle potential errors 38 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 39 | 40 | if (error instanceof NotFoundError) { 41 | // Project or parent task not found 42 | throw new McpError(ErrorCode.InvalidParams, error.message); 43 | } else if (error instanceof ConflictError) { 44 | // Subtasks exist and force=false - map to InvalidParams as the request is invalid without force=true 45 | throw new McpError(ErrorCode.InvalidParams, error.message); 46 | } else { 47 | // Generic internal error 48 | const message = error instanceof Error ? error.message : 'An unknown error occurred while expanding the task.'; 49 | throw new McpError(ErrorCode.InternalError, message); 50 | } 51 | } 52 | }; 53 | 54 | // Register the tool with the server 55 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 56 | 57 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 58 | }; 59 | -------------------------------------------------------------------------------- /src/tools/updateTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { TaskPriority, TaskStatus } from '../types/taskTypes.js'; // Import shared types 3 | 4 | export const TOOL_NAME = "updateTask"; 5 | 6 | export const TOOL_DESCRIPTION = ` 7 | Updates specific details of an existing task within a project. 8 | Requires the project ID and task ID. Allows updating description, priority, and/or dependencies. 9 | At least one optional field (description, priority, dependencies) must be provided. 10 | Returns the full details of the updated task upon success. 11 | `; 12 | 13 | // Define the possible priority values based on the shared type 14 | const priorities: [TaskPriority, ...TaskPriority[]] = ['high', 'medium', 'low']; 15 | 16 | // Base Zod schema without refinement - needed for server.tool registration 17 | export const UPDATE_TASK_BASE_SCHEMA = z.object({ 18 | project_id: z.string() 19 | .uuid("The project_id must be a valid UUID.") 20 | .describe("The unique identifier (UUID) of the project containing the task to update. This project must exist."), // Required, UUID format 21 | 22 | task_id: z.string() 23 | .uuid("The task_id must be a valid UUID.") // Assuming task IDs are UUIDs for consistency 24 | .describe("The unique identifier (UUID) of the task to update. This task must exist within the specified project."), // Required, UUID format 25 | 26 | description: z.string() 27 | .min(1, "Description cannot be empty if provided.") 28 | .max(1024, "Description cannot exceed 1024 characters.") 29 | .optional() 30 | .describe("Optional. The new textual description for the task (1-1024 characters)."), // Optional, string, limits 31 | 32 | priority: z.enum(priorities) 33 | .optional() 34 | .describe("Optional. The new priority level for the task ('high', 'medium', or 'low')."), // Optional, enum 35 | 36 | dependencies: z.array( 37 | z.string() 38 | .uuid("Each dependency task ID must be a valid UUID.") 39 | .describe("A task ID (UUID) that this task should depend on.") 40 | ) 41 | .max(50, "A task cannot have more than 50 dependencies.") 42 | .optional() 43 | .describe("Optional. The complete list of task IDs (UUIDs) that this task depends on. Replaces the existing list entirely. Max 50 dependencies."), // Optional, array of UUID strings, limit 44 | }); 45 | 46 | // Refined schema for validation and type inference 47 | export const TOOL_PARAMS = UPDATE_TASK_BASE_SCHEMA.refine( 48 | data => data.description !== undefined || data.priority !== undefined || data.dependencies !== undefined, { 49 | message: "At least one field to update (description, priority, or dependencies) must be provided.", 50 | // path: [], // No specific path, applies to the object 51 | } 52 | ); 53 | 54 | // Define the expected type for arguments based on the *refined* Zod schema 55 | export type UpdateTaskArgs = z.infer; 56 | -------------------------------------------------------------------------------- /src/repositories/ProjectRepository.ts: -------------------------------------------------------------------------------- 1 | import { Database as Db } from 'better-sqlite3'; 2 | import { logger } from '../utils/logger.js'; // Assuming logger exists 3 | 4 | export interface ProjectData { 5 | project_id: string; 6 | name: string; 7 | created_at: string; // ISO8601 format 8 | } 9 | 10 | export class ProjectRepository { 11 | private db: Db; 12 | 13 | // Pass the database connection instance 14 | constructor(db: Db) { 15 | this.db = db; 16 | } 17 | 18 | /** 19 | * Creates a new project record in the database. 20 | * @param project - The project data to insert. 21 | * @throws {Error} If the database operation fails. 22 | */ 23 | public create(project: ProjectData): void { 24 | const sql = ` 25 | INSERT INTO projects (project_id, name, created_at) 26 | VALUES (@project_id, @name, @created_at) 27 | `; 28 | try { 29 | const stmt = this.db.prepare(sql); 30 | const info = stmt.run(project); 31 | logger.info(`[ProjectRepository] Created project ${project.project_id}, changes: ${info.changes}`); 32 | } catch (error) { 33 | logger.error(`[ProjectRepository] Failed to create project ${project.project_id}:`, error); 34 | // Re-throw the error to be handled by the service layer 35 | throw error; 36 | } 37 | } 38 | 39 | /** 40 | * Finds a project by its ID. 41 | * @param projectId - The ID of the project to find. 42 | * @returns The project data if found, otherwise undefined. 43 | */ 44 | public findById(projectId: string): ProjectData | undefined { 45 | const sql = `SELECT project_id, name, created_at FROM projects WHERE project_id = ?`; 46 | try { 47 | const stmt = this.db.prepare(sql); 48 | const project = stmt.get(projectId) as ProjectData | undefined; 49 | return project; 50 | } catch (error) { 51 | logger.error(`[ProjectRepository] Failed to find project ${projectId}:`, error); 52 | throw error; // Re-throw 53 | } 54 | } 55 | 56 | /** 57 | * Deletes a project by its ID. 58 | * Relies on ON DELETE CASCADE in the schema to remove associated tasks/dependencies. 59 | * @param projectId - The ID of the project to delete. 60 | * @returns The number of projects deleted (0 or 1). 61 | * @throws {Error} If the database operation fails. 62 | */ 63 | public deleteProject(projectId: string): number { 64 | const sql = `DELETE FROM projects WHERE project_id = ?`; 65 | try { 66 | const stmt = this.db.prepare(sql); 67 | const info = stmt.run(projectId); 68 | logger.info(`[ProjectRepository] Attempted to delete project ${projectId}. Rows affected: ${info.changes}`); 69 | // Cascade delete handles tasks/dependencies in the background via schema definition. 70 | return info.changes; 71 | } catch (error) { 72 | logger.error(`[ProjectRepository] Failed to delete project ${projectId}:`, error); 73 | throw error; // Re-throw 74 | } 75 | } 76 | 77 | // Add other methods as needed (e.g., update, list) 78 | } 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | dist/ 145 | .generalrules 146 | .clinerules 147 | .cursorrules 148 | 149 | 150 | 151 | # End of https://www.toptal.com/developers/gitignore/api/node 152 | .taskmanagerrules 153 | docs/ 154 | data/taskmanager.db 155 | data/taskmanager.* 156 | -------------------------------------------------------------------------------- /src/tools/addTaskTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Correct path for McpServer 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; // Correct path for Error types 3 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, AddTaskArgs } from "./addTaskParams.js"; 4 | import { TaskService } from "../services/TaskService.js"; // Assuming TaskService is exported via services/index.js 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError, ValidationError } from "../utils/errors.js"; // Import custom errors 7 | 8 | /** 9 | * Registers the addTask tool with the MCP server. 10 | * 11 | * @param server - The McpServer instance. 12 | * @param taskService - An instance of the TaskService. 13 | */ 14 | export const addTaskTool = (server: McpServer, taskService: TaskService): void => { 15 | 16 | // Define the asynchronous function that handles the actual tool logic 17 | const processRequest = async (args: AddTaskArgs) => { 18 | logger.info(`[${TOOL_NAME}] Received request with args:`, args); 19 | try { 20 | // Call the service method to add the task 21 | // The Zod schema handles basic type/format/length validation 22 | const newTask = await taskService.addTask({ 23 | project_id: args.project_id, 24 | description: args.description, 25 | dependencies: args.dependencies, // Pass optional fields 26 | priority: args.priority, 27 | status: args.status, 28 | }); 29 | 30 | // Format the successful response according to MCP standards 31 | // Return the full details of the created task as per spec FR-FS-011 32 | logger.info(`[${TOOL_NAME}] Task added successfully: ${newTask.task_id}`); 33 | return { 34 | content: [{ 35 | type: "text" as const, 36 | text: JSON.stringify(newTask) // Return the full task object 37 | }] 38 | }; 39 | } catch (error: unknown) { 40 | // Handle potential errors from the service layer 41 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 42 | 43 | if (error instanceof NotFoundError) { 44 | // Specific error if the project wasn't found - map to InvalidParams as project_id is invalid 45 | throw new McpError(ErrorCode.InvalidParams, error.message); 46 | } else if (error instanceof ValidationError) { 47 | // Specific error for validation issues within the service (e.g., dependency check if implemented) 48 | throw new McpError(ErrorCode.InvalidParams, error.message); 49 | } else { 50 | // Generic internal error for database issues or unexpected problems 51 | const message = error instanceof Error ? error.message : 'An unknown error occurred while adding the task.'; 52 | throw new McpError(ErrorCode.InternalError, message); 53 | } 54 | } 55 | }; 56 | 57 | // Register the tool with the server, passing the shape of the Zod object 58 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS.shape, processRequest); 59 | 60 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 61 | }; 62 | -------------------------------------------------------------------------------- /src/tools/updateTaskTool.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 3 | // Import the base schema shape for registration and the refined schema for validation/types 4 | import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS, UPDATE_TASK_BASE_SCHEMA, UpdateTaskArgs } from "./updateTaskParams.js"; 5 | import { TaskService, FullTaskData } from "../services/TaskService.js"; // Assuming TaskService is exported from index 6 | import { logger } from '../utils/logger.js'; 7 | import { NotFoundError, ValidationError } from "../utils/errors.js"; // Import custom errors 8 | 9 | /** 10 | * Registers the updateTask tool with the MCP server. 11 | * 12 | * @param server - The McpServer instance. 13 | * @param taskService - An instance of the TaskService. 14 | */ 15 | export const updateTaskTool = (server: McpServer, taskService: TaskService): void => { 16 | 17 | const processRequest = async (args: UpdateTaskArgs): Promise<{ content: { type: 'text', text: string }[] }> => { 18 | logger.info(`[${TOOL_NAME}] Received request with args:`, { ...args, dependencies: args.dependencies ? `[${args.dependencies.length} items]` : undefined }); // Avoid logging potentially large arrays 19 | try { 20 | // Call the service method to update the task 21 | // The service method now returns FullTaskData 22 | const updatedTask: FullTaskData = await taskService.updateTask({ 23 | project_id: args.project_id, 24 | task_id: args.task_id, 25 | description: args.description, 26 | priority: args.priority, 27 | dependencies: args.dependencies, 28 | }); 29 | 30 | // Format the successful response 31 | logger.info(`[${TOOL_NAME}] Successfully updated task ${args.task_id} in project ${args.project_id}`); 32 | return { 33 | content: [{ 34 | type: "text" as const, 35 | text: JSON.stringify(updatedTask) // Return the full updated task details 36 | }] 37 | }; 38 | } catch (error: unknown) { 39 | // Handle potential errors according to systemPatterns.md mapping 40 | logger.error(`[${TOOL_NAME}] Error processing request:`, error); 41 | 42 | if (error instanceof ValidationError) { 43 | // Validation error from service (e.g., no fields provided, invalid deps) 44 | throw new McpError(ErrorCode.InvalidParams, error.message); 45 | } else if (error instanceof NotFoundError) { 46 | // Project or task not found - Map to InvalidParams as per SDK limitations/convention 47 | throw new McpError(ErrorCode.InvalidParams, error.message); 48 | } else { 49 | // Generic internal error 50 | const message = error instanceof Error ? error.message : 'An unknown error occurred while updating the task.'; 51 | throw new McpError(ErrorCode.InternalError, message); 52 | } 53 | } 54 | }; 55 | 56 | // Register the tool with the server using the base schema's shape 57 | server.tool(TOOL_NAME, TOOL_DESCRIPTION, UPDATE_TASK_BASE_SCHEMA.shape, processRequest); 58 | 59 | logger.info(`[${TOOL_NAME}] Tool registered successfully.`); 60 | }; 61 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { ConfigurationManager } from "../config/ConfigurationManager.js"; 3 | import { logger } from "../utils/index.js"; // Now using barrel file 4 | import { DatabaseManager } from "../db/DatabaseManager.js"; 5 | import { ProjectRepository } from "../repositories/ProjectRepository.js"; 6 | import { TaskRepository } from "../repositories/TaskRepository.js"; // Added TaskRepository import 7 | import { ProjectService, TaskService } from "../services/index.js"; // Using barrel file, added TaskService 8 | 9 | // Import tool registration functions 10 | // import { exampleTool } from "./exampleTool.js"; // Commenting out example 11 | import { createProjectTool } from "./createProjectTool.js"; 12 | import { addTaskTool } from "./addTaskTool.js"; 13 | import { listTasksTool } from "./listTasksTool.js"; 14 | import { showTaskTool } from "./showTaskTool.js"; 15 | import { setTaskStatusTool } from "./setTaskStatusTool.js"; 16 | import { expandTaskTool } from "./expandTaskTool.js"; 17 | import { getNextTaskTool } from "./getNextTaskTool.js"; 18 | import { exportProjectTool } from "./exportProjectTool.js"; 19 | import { importProjectTool } from "./importProjectTool.js"; 20 | import { updateTaskTool } from "./updateTaskTool.js"; // Import the new tool 21 | import { deleteTaskTool } from "./deleteTaskTool.js"; // Import deleteTask tool 22 | import { deleteProjectTool } from "./deleteProjectTool.js"; // Import deleteProject tool 23 | // import { yourTool } from "./yourTool.js"; // Add other new tool imports here 24 | 25 | /** 26 | * Register all defined tools with the MCP server instance. 27 | * This function centralizes tool registration logic. 28 | * It also instantiates necessary services and repositories. 29 | */ 30 | export function registerTools(server: McpServer): void { 31 | logger.info("Registering tools..."); 32 | const configManager = ConfigurationManager.getInstance(); 33 | 34 | // --- Instantiate Dependencies --- 35 | // Note: Consider dependency injection frameworks for larger applications 36 | try { 37 | const dbManager = DatabaseManager.getInstance(); 38 | const db = dbManager.getDb(); // Get the initialized DB connection 39 | 40 | // Instantiate Repositories 41 | const projectRepository = new ProjectRepository(db); 42 | const taskRepository = new TaskRepository(db); // Instantiate TaskRepository 43 | 44 | // Instantiate Services 45 | const projectService = new ProjectService(db, projectRepository, taskRepository); // Pass db and both repos 46 | const taskService = new TaskService(db, taskRepository, projectRepository); // Instantiate TaskService, passing db and repos 47 | 48 | // --- Register Tools --- 49 | // Register each tool, passing necessary services 50 | 51 | // exampleTool(server, configManager.getExampleServiceConfig()); // Example commented out 52 | 53 | createProjectTool(server, projectService); 54 | addTaskTool(server, taskService); 55 | listTasksTool(server, taskService); 56 | showTaskTool(server, taskService); 57 | setTaskStatusTool(server, taskService); 58 | expandTaskTool(server, taskService); 59 | getNextTaskTool(server, taskService); 60 | exportProjectTool(server, projectService); 61 | importProjectTool(server, projectService); // Register importProjectTool (uses ProjectService) 62 | updateTaskTool(server, taskService); // Register the new updateTask tool 63 | deleteTaskTool(server, taskService); // Register deleteTask tool 64 | deleteProjectTool(server, projectService); // Register deleteProject tool (uses ProjectService) 65 | // ... etc. 66 | 67 | logger.info("All tools registered successfully."); 68 | 69 | } catch (error) { 70 | logger.error("Failed to instantiate dependencies or register tools:", error); 71 | // Depending on the desired behavior, you might want to exit the process 72 | // process.exit(1); 73 | throw new Error("Failed to initialize server components during tool registration."); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/db/DatabaseManager.ts: -------------------------------------------------------------------------------- 1 | import Database, { Database as Db } from 'better-sqlite3'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; // Added for ES Module dirname 5 | import { ConfigurationManager } from '../config/ConfigurationManager.js'; 6 | import { logger } from '../utils/logger.js'; // Assuming logger exists 7 | 8 | export class DatabaseManager { 9 | private static instance: DatabaseManager; 10 | private db!: Db; // Added definite assignment assertion 11 | private dbPath: string; 12 | 13 | private constructor() { 14 | const configManager = ConfigurationManager.getInstance(); 15 | // TODO: Get path from configManager once implemented 16 | // For now, use a default relative path 17 | this.dbPath = configManager.getDatabasePath(); // Assuming this method exists 18 | logger.info(`[DatabaseManager] Using database path: ${this.dbPath}`); 19 | 20 | this.initializeDatabase(); 21 | } 22 | 23 | public static getInstance(): DatabaseManager { 24 | if (!DatabaseManager.instance) { 25 | DatabaseManager.instance = new DatabaseManager(); 26 | } 27 | return DatabaseManager.instance; 28 | } 29 | 30 | private initializeDatabase(): void { 31 | try { 32 | const dbDir = path.dirname(this.dbPath); 33 | if (!fs.existsSync(dbDir)) { 34 | logger.info(`[DatabaseManager] Creating database directory: ${dbDir}`); 35 | fs.mkdirSync(dbDir, { recursive: true }); 36 | } 37 | 38 | const dbExists = fs.existsSync(this.dbPath); 39 | logger.info(`[DatabaseManager] Database file ${this.dbPath} exists: ${dbExists}`); 40 | 41 | // Pass a wrapper function for verbose logging to match expected signature 42 | this.db = new Database(this.dbPath, { 43 | verbose: (message?: any, ...additionalArgs: any[]) => logger.debug({ sql: message, params: additionalArgs }, 'SQLite Query') 44 | }); 45 | 46 | // Always enable foreign keys and WAL mode upon connection 47 | this.db.pragma('foreign_keys = ON'); 48 | // Assert type for pragma result 49 | const journalMode = this.db.pragma('journal_mode = WAL') as [{ journal_mode: string }]; 50 | logger.info(`[DatabaseManager] Journal mode set to: ${journalMode[0]?.journal_mode ?? 'unknown'}`); 51 | 52 | 53 | // Check if initialization is needed (simple check: does 'projects' table exist?) 54 | const tableCheck = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects';").get(); 55 | 56 | if (!tableCheck) { 57 | logger.info('[DatabaseManager] Projects table not found. Initializing schema...'); 58 | // Revert to looking for schema.sql relative to the compiled JS file's directory (__dirname) 59 | const __filename = fileURLToPath(import.meta.url); 60 | const __dirname = path.dirname(__filename); // This will be dist/db when running compiled code 61 | const schemaPath = path.join(__dirname, 'schema.sql'); 62 | 63 | logger.info(`[DatabaseManager] Looking for schema file at: ${schemaPath}`); 64 | if (!fs.existsSync(schemaPath)) { 65 | logger.error(`[DatabaseManager] Schema file not found at ${schemaPath}. Ensure build process copied it correctly.`); 66 | throw new Error(`Schema file not found at ${schemaPath}. Build process might be incomplete.`); 67 | } 68 | const schemaSql = fs.readFileSync(schemaPath, 'utf8'); 69 | this.db.exec(schemaSql); 70 | logger.info('[DatabaseManager] Database schema initialized successfully.'); 71 | } else { 72 | logger.info('[DatabaseManager] Database schema already initialized.'); 73 | } 74 | } catch (error) { 75 | logger.error('[DatabaseManager] Failed to initialize database:', error); 76 | // Propagate the error to prevent the server from starting with a broken DB connection 77 | throw error; 78 | } 79 | } 80 | 81 | public getDb(): Db { 82 | if (!this.db) { 83 | // This should ideally not happen if constructor succeeded 84 | logger.error('[DatabaseManager] Database connection not available.'); 85 | throw new Error('Database connection not available.'); 86 | } 87 | return this.db; 88 | } 89 | 90 | // Optional: Add a close method for graceful shutdown 91 | public closeDb(): void { 92 | if (this.db) { 93 | this.db.close(); 94 | logger.info('[DatabaseManager] Database connection closed.'); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/config/ConfigurationManager.ts: -------------------------------------------------------------------------------- 1 | // Import config types for services as they are added 2 | import { ExampleServiceConfig } from '../types/index.js'; 3 | 4 | // Define the structure for all configurations managed 5 | interface ManagedConfigs { 6 | exampleService: Required; 7 | // Add other service config types here: 8 | // yourService: Required; 9 | databasePath: string; // Added for database file location 10 | } 11 | 12 | /** 13 | * Centralized configuration management for all services. 14 | * Implements singleton pattern to ensure consistent configuration. 15 | */ 16 | export class ConfigurationManager { 17 | private static instance: ConfigurationManager | null = null; 18 | private static instanceLock = false; 19 | 20 | private config: ManagedConfigs; 21 | 22 | private constructor() { 23 | // Initialize with default configurations 24 | this.config = { 25 | exampleService: { 26 | // Define defaults for ExampleService 27 | greeting: "Hello", 28 | enableDetailedLogs: false, 29 | }, 30 | // Initialize other service configs with defaults: 31 | // yourService: { 32 | // someSetting: 'default value', 33 | // retryCount: 3, 34 | // }, 35 | // Default database path 36 | databasePath: './data/taskmanager.db', 37 | }; 38 | 39 | // Optional: Load overrides from environment variables or config files here 40 | this.loadEnvironmentOverrides(); 41 | } 42 | 43 | /** 44 | * Get the singleton instance of ConfigurationManager. 45 | * Basic lock to prevent race conditions during initial creation. 46 | */ 47 | public static getInstance(): ConfigurationManager { 48 | if (!ConfigurationManager.instance) { 49 | if (!ConfigurationManager.instanceLock) { 50 | ConfigurationManager.instanceLock = true; // Lock 51 | try { 52 | ConfigurationManager.instance = new ConfigurationManager(); 53 | } finally { 54 | ConfigurationManager.instanceLock = false; // Unlock 55 | } 56 | } else { 57 | // Basic busy wait if locked (consider a more robust async lock if high contention is expected) 58 | while (ConfigurationManager.instanceLock) { } 59 | // Re-check instance after wait 60 | if (!ConfigurationManager.instance) { 61 | // This path is less likely but handles edge cases if lock logic needs refinement 62 | return ConfigurationManager.getInstance(); 63 | } 64 | } 65 | } 66 | return ConfigurationManager.instance; 67 | } 68 | 69 | // --- Getters for specific configurations --- 70 | 71 | public getExampleServiceConfig(): Required { 72 | // Return a copy to prevent accidental modification of the internal state 73 | return { ...this.config.exampleService }; 74 | } 75 | 76 | // Add getters for other service configs: 77 | // public getYourServiceConfig(): Required { 78 | // return { ...this.config.yourService }; 79 | // } 80 | 81 | public getDatabasePath(): string { 82 | // Return a copy to prevent accidental modification (though less critical for a string) 83 | return this.config.databasePath; 84 | } 85 | 86 | // --- Updaters for specific configurations (if runtime updates are needed) --- 87 | 88 | public updateExampleServiceConfig(update: Partial): void { 89 | this.config.exampleService = { 90 | ...this.config.exampleService, 91 | ...update, 92 | }; 93 | // Optional: Notify relevant services about the config change 94 | } 95 | 96 | // Add updaters for other service configs: 97 | // public updateYourServiceConfig(update: Partial): void { 98 | // this.config.yourService = { 99 | // ...this.config.yourService, 100 | // ...update, 101 | // }; 102 | // } 103 | 104 | /** 105 | * Example method to load configuration overrides from environment variables. 106 | * Call this in the constructor. 107 | */ 108 | private loadEnvironmentOverrides(): void { 109 | // Example for ExampleService 110 | if (process.env.EXAMPLE_GREETING) { 111 | this.config.exampleService.greeting = process.env.EXAMPLE_GREETING; 112 | } 113 | if (process.env.EXAMPLE_ENABLE_LOGS) { 114 | this.config.exampleService.enableDetailedLogs = process.env.EXAMPLE_ENABLE_LOGS.toLowerCase() === 'true'; 115 | } 116 | 117 | // Override for Database Path 118 | if (process.env.DATABASE_PATH) { 119 | this.config.databasePath = process.env.DATABASE_PATH; 120 | } 121 | 122 | // Add logic for other services based on their environment variables 123 | // if (process.env.YOUR_SERVICE_RETRY_COUNT) { 124 | // const retryCount = parseInt(process.env.YOUR_SERVICE_RETRY_COUNT, 10); 125 | // if (!isNaN(retryCount)) { 126 | // this.config.yourService.retryCount = retryCount; 127 | // } 128 | // } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Task Manager Server 2 | 3 |
4 | MCP Task Manager Logo 5 |
6 | 7 | A local Model Context Protocol (MCP) server providing backend tools for client-driven project and task management using a SQLite database. 8 | 9 | ## Overview 10 | 11 | This server acts as a persistent backend for local MCP clients (like AI agents or scripts) that need to manage structured task data within distinct projects. It handles data storage and provides a standardized set of tools for interaction, while the strategic workflow logic resides within the client. 12 | 13 | **Key Features:** 14 | 15 | * **Project-Based:** Tasks are organized within distinct projects. 16 | * **SQLite Persistence:** Uses a local SQLite file (`./data/taskmanager.db` by default) for simple, self-contained data storage. 17 | * **Client-Driven:** Provides tools for clients; does not dictate workflow. 18 | * **MCP Compliant:** Adheres to the Model Context Protocol for tool definition and communication. 19 | * **Task Management:** Supports creating projects, adding tasks, listing/showing tasks, updating status, expanding tasks into subtasks, and identifying the next actionable task. 20 | * **Import/Export:** Allows exporting project data to JSON and importing from JSON to create new projects. 21 | 22 | ## Implemented MCP Tools 23 | 24 | The following tools are available for MCP clients: 25 | 26 | * **`createProject`**: 27 | * **Description:** Creates a new, empty project. 28 | * **Params:** `projectName` (string, optional, max 255) 29 | * **Returns:** `{ project_id: string }` 30 | * **`addTask`**: 31 | * **Description:** Adds a new task to a project. 32 | * **Params:** `project_id` (string, required, UUID), `description` (string, required, 1-1024), `dependencies` (string[], optional, max 50), `priority` (enum 'high'|'medium'|'low', optional, default 'medium'), `status` (enum 'todo'|'in-progress'|'review'|'done', optional, default 'todo') 33 | * **Returns:** Full `TaskData` object of the created task. 34 | * **`listTasks`**: 35 | * **Description:** Lists tasks for a project, with optional filtering and subtask inclusion. 36 | * **Params:** `project_id` (string, required, UUID), `status` (enum 'todo'|'in-progress'|'review'|'done', optional), `include_subtasks` (boolean, optional, default false) 37 | * **Returns:** Array of `TaskData` or `StructuredTaskData` objects. 38 | * **`showTask`**: 39 | * **Description:** Retrieves full details for a specific task, including dependencies and direct subtasks. 40 | * **Params:** `project_id` (string, required, UUID), `task_id` (string, required) 41 | * **Returns:** `FullTaskData` object. 42 | * **`setTaskStatus`**: 43 | * **Description:** Updates the status of one or more tasks. 44 | * **Params:** `project_id` (string, required, UUID), `task_ids` (string[], required, 1-100), `status` (enum 'todo'|'in-progress'|'review'|'done', required) 45 | * **Returns:** `{ success: true, updated_count: number }` 46 | * **`expandTask`**: 47 | * **Description:** Breaks a parent task into subtasks, optionally replacing existing ones. 48 | * **Params:** `project_id` (string, required, UUID), `task_id` (string, required), `subtask_descriptions` (string[], required, 1-20, each 1-512), `force` (boolean, optional, default false) 49 | * **Returns:** Updated parent `FullTaskData` object including new subtasks. 50 | * **`getNextTask`**: 51 | * **Description:** Identifies the next actionable task based on status ('todo'), dependencies ('done'), priority, and creation date. 52 | * **Params:** `project_id` (string, required, UUID) 53 | * **Returns:** `FullTaskData` object of the next task, or `null` if none are ready. 54 | * **`exportProject`**: 55 | * **Description:** Exports complete project data as a JSON string. 56 | * **Params:** `project_id` (string, required, UUID), `format` (enum 'json', optional, default 'json') 57 | * **Returns:** JSON string representing the project. 58 | * **`importProject`**: 59 | * **Description:** Creates a *new* project from an exported JSON string. 60 | * **Params:** `project_data` (string, required, JSON), `new_project_name` (string, optional, max 255) 61 | * **Returns:** `{ project_id: string }` of the newly created project. 62 | * **`updateTask`**: 63 | * **Description:** Updates specific details (description, priority, dependencies) of an existing task. 64 | * **Params:** `project_id` (string, required, UUID), `task_id` (string, required, UUID), `description` (string, optional, 1-1024), `priority` (enum 'high'|'medium'|'low', optional), `dependencies` (string[], optional, max 50, replaces existing) 65 | * **Returns:** Updated `FullTaskData` object. 66 | * **`deleteTask`**: 67 | * **Description:** Deletes one or more tasks (and their subtasks/dependency links via cascade). 68 | * **Params:** `project_id` (string, required, UUID), `task_ids` (string[], required, 1-100) 69 | * **Returns:** `{ success: true, deleted_count: number }` 70 | * **`deleteProject`**: 71 | * **Description:** Permanently deletes a project and ALL associated data. **Use with caution!** 72 | * **Params:** `project_id` (string, required, UUID) 73 | * **Returns:** `{ success: true }` 74 | 75 | *(Note: Refer to the corresponding `src/tools/*Params.ts` files for detailed Zod schemas and parameter descriptions.)* 76 | 77 | ## Getting Started 78 | 79 | 1. **Prerequisites:** Node.js (LTS recommended), npm. 80 | 2. **Install Dependencies:** 81 | 82 | ```bash 83 | npm install 84 | ``` 85 | 86 | 3. **Run in Development Mode:** (Uses `ts-node` and `nodemon` for auto-reloading) 87 | 88 | ```bash 89 | npm run dev 90 | ``` 91 | 92 | The server will connect via stdio. Logs (JSON format) will be printed to stderr. The SQLite database will be created/updated in `./data/taskmanager.db`. 93 | 4. **Build for Production:** 94 | 95 | ```bash 96 | npm run build 97 | ``` 98 | 99 | 5. **Run Production Build:** 100 | 101 | ```bash 102 | npm start 103 | ``` 104 | 105 | ## Configuration 106 | 107 | * **Database Path:** The location of the SQLite database file can be overridden by setting the `DATABASE_PATH` environment variable. The default is `./data/taskmanager.db`. 108 | * **Log Level:** The logging level can be set using the `LOG_LEVEL` environment variable (e.g., `debug`, `info`, `warn`, `error`). The default is `info`. 109 | 110 | ## Project Structure 111 | 112 | * `/src`: Source code. 113 | * `/config`: Configuration management. 114 | * `/db`: Database manager and schema (`schema.sql`). 115 | * `/repositories`: Data access layer (SQLite interaction). 116 | * `/services`: Core business logic. 117 | * `/tools`: MCP tool definitions (*Params.ts) and implementation (*Tool.ts). 118 | * `/types`: Shared TypeScript interfaces (currently minimal, mostly in repos/services). 119 | * `/utils`: Logging, custom errors, etc. 120 | * `createServer.ts`: Server instance creation. 121 | * `server.ts`: Main application entry point. 122 | * `/dist`: Compiled JavaScript output. 123 | * `/docs`: Project documentation (PRD, Feature Specs, RFC). 124 | * `/data`: Default location for the SQLite database file (created automatically). 125 | * `tasks.md`: Manual task tracking file for development. 126 | * Config files (`package.json`, `tsconfig.json`, `.eslintrc.json`, etc.) 127 | 128 | ## Linting and Formatting 129 | 130 | * **Lint:** `npm run lint` 131 | * **Format:** `npm run format` 132 | 133 | (Code is automatically linted/formatted on commit via Husky/lint-staged). 134 | -------------------------------------------------------------------------------- /src/services/ProjectService.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { Database as Db } from 'better-sqlite3'; // Import Db type 3 | import { ProjectRepository, ProjectData } from '../repositories/ProjectRepository.js'; 4 | import { TaskRepository, TaskData, DependencyData } from '../repositories/TaskRepository.js'; 5 | import { logger } from '../utils/logger.js'; 6 | import { NotFoundError, ValidationError, ConflictError } from '../utils/errors.js'; // Import errors 7 | 8 | // Define structure for the export/import JSON 9 | interface ExportTask extends TaskData { 10 | dependencies: string[]; // List of task IDs this task depends on 11 | subtasks: ExportTask[]; // Nested subtasks 12 | } 13 | 14 | interface ExportData { 15 | project_metadata: ProjectData; 16 | tasks: ExportTask[]; // Root tasks 17 | } 18 | 19 | 20 | export class ProjectService { 21 | private projectRepository: ProjectRepository; 22 | private taskRepository: TaskRepository; 23 | private db: Db; // Add db instance 24 | 25 | constructor( 26 | db: Db, // Inject Db instance 27 | projectRepository: ProjectRepository, 28 | taskRepository: TaskRepository 29 | ) { 30 | this.db = db; // Store db instance 31 | this.projectRepository = projectRepository; 32 | this.taskRepository = taskRepository; 33 | } 34 | 35 | /** 36 | * Creates a new project. 37 | */ 38 | public async createProject(projectName?: string): Promise { 39 | const projectId = uuidv4(); 40 | const now = new Date().toISOString(); 41 | const finalProjectName = projectName?.trim() || `New Project ${now}`; 42 | const newProject: ProjectData = { 43 | project_id: projectId, 44 | name: finalProjectName, 45 | created_at: now, 46 | }; 47 | logger.info(`[ProjectService] Attempting to create project: ${projectId} with name "${finalProjectName}"`); 48 | try { 49 | this.projectRepository.create(newProject); 50 | logger.info(`[ProjectService] Successfully created project: ${projectId}`); 51 | return newProject; 52 | } catch (error) { 53 | logger.error(`[ProjectService] Error creating project ${projectId}:`, error); 54 | throw error; 55 | } 56 | } 57 | 58 | /** 59 | * Retrieves a project by its ID. 60 | */ 61 | public async getProjectById(projectId: string): Promise { 62 | logger.info(`[ProjectService] Attempting to find project: ${projectId}`); 63 | try { 64 | const project = this.projectRepository.findById(projectId); 65 | if (project) { 66 | logger.info(`[ProjectService] Found project: ${projectId}`); 67 | } else { 68 | logger.warn(`[ProjectService] Project not found: ${projectId}`); 69 | } 70 | return project; 71 | } catch (error) { 72 | logger.error(`[ProjectService] Error finding project ${projectId}:`, error); 73 | throw error; 74 | } 75 | } 76 | 77 | /** 78 | * Exports all data for a given project as a JSON string. 79 | */ 80 | public async exportProject(projectId: string): Promise { 81 | logger.info(`[ProjectService] Attempting to export project: ${projectId}`); 82 | const projectMetadata = this.projectRepository.findById(projectId); 83 | if (!projectMetadata) { 84 | logger.warn(`[ProjectService] Project not found for export: ${projectId}`); 85 | throw new NotFoundError(`Project with ID ${projectId} not found.`); 86 | } 87 | 88 | try { 89 | const allTasks = this.taskRepository.findAllTasksForProject(projectId); 90 | const allDependencies = this.taskRepository.findAllDependenciesForProject(projectId); 91 | 92 | const taskMap: Map = new Map(); 93 | const rootTasks: ExportTask[] = []; 94 | const dependencyMap: Map = new Map(); 95 | 96 | for (const dep of allDependencies) { 97 | if (!dependencyMap.has(dep.task_id)) { 98 | dependencyMap.set(dep.task_id, []); 99 | } 100 | dependencyMap.get(dep.task_id)!.push(dep.depends_on_task_id); 101 | } 102 | 103 | for (const task of allTasks) { 104 | taskMap.set(task.task_id, { 105 | ...task, 106 | dependencies: dependencyMap.get(task.task_id) || [], 107 | subtasks: [], 108 | }); 109 | } 110 | 111 | for (const task of allTasks) { 112 | const exportTask = taskMap.get(task.task_id)!; 113 | if (task.parent_task_id && taskMap.has(task.parent_task_id)) { 114 | const parent = taskMap.get(task.parent_task_id)!; 115 | if (!parent.subtasks) parent.subtasks = []; 116 | parent.subtasks.push(exportTask); 117 | } else if (!task.parent_task_id) { 118 | rootTasks.push(exportTask); 119 | } 120 | } 121 | 122 | const exportData: ExportData = { 123 | project_metadata: projectMetadata, 124 | tasks: rootTasks, 125 | }; 126 | 127 | const jsonString = JSON.stringify(exportData, null, 2); 128 | logger.info(`[ProjectService] Successfully prepared export data for project ${projectId}`); 129 | return jsonString; 130 | 131 | } catch (error) { 132 | logger.error(`[ProjectService] Error exporting project ${projectId}:`, error); 133 | throw error; 134 | } 135 | } 136 | 137 | /** 138 | * Imports project data from a JSON string, creating a new project. 139 | */ 140 | public async importProject(projectDataString: string, newProjectName?: string): Promise<{ project_id: string }> { 141 | logger.info(`[ProjectService] Attempting to import project...`); 142 | let importData: ExportData; 143 | try { 144 | if (projectDataString.length > 10 * 1024 * 1024) { // Example 10MB limit 145 | throw new ValidationError('Input data exceeds size limit (e.g., 10MB).'); 146 | } 147 | importData = JSON.parse(projectDataString); 148 | // TODO: Implement rigorous schema validation (Zod?) 149 | if (!importData || !importData.project_metadata || !Array.isArray(importData.tasks)) { 150 | throw new ValidationError('Invalid import data structure: Missing required fields.'); 151 | } 152 | logger.debug(`[ProjectService] Successfully parsed import data.`); 153 | } catch (error) { 154 | logger.error('[ProjectService] Failed to parse or validate import JSON:', error); 155 | if (error instanceof SyntaxError) { 156 | throw new ValidationError(`Invalid JSON format: ${error.message}`); 157 | } 158 | throw new ValidationError(`Invalid import data: ${error instanceof Error ? error.message : 'Unknown validation error'}`); 159 | } 160 | 161 | const importTransaction = this.db.transaction(() => { 162 | const newProjectId = uuidv4(); 163 | const now = new Date().toISOString(); 164 | const finalProjectName = newProjectName?.trim() || `${importData.project_metadata.name} (Imported ${now})`; 165 | const newProject: ProjectData = { 166 | project_id: newProjectId, 167 | name: finalProjectName.substring(0, 255), 168 | created_at: now, 169 | }; 170 | this.projectRepository.create(newProject); 171 | logger.info(`[ProjectService] Created new project ${newProjectId} for import.`); 172 | 173 | const idMap = new Map(); 174 | const processTask = (task: ExportTask, parentDbId: string | null) => { 175 | const newTaskId = uuidv4(); 176 | idMap.set(task.task_id, newTaskId); 177 | const newTaskData: TaskData = { 178 | task_id: newTaskId, 179 | project_id: newProjectId, 180 | parent_task_id: parentDbId, 181 | description: task.description, 182 | status: task.status, 183 | priority: task.priority, 184 | created_at: task.created_at, 185 | updated_at: task.updated_at, 186 | }; 187 | this.taskRepository.create(newTaskData, []); // Create task first 188 | if (task.subtasks && task.subtasks.length > 0) { 189 | task.subtasks.forEach(subtask => processTask(subtask, newTaskId)); 190 | } 191 | }; 192 | importData.tasks.forEach(rootTask => processTask(rootTask, null)); 193 | logger.info(`[ProjectService] Processed ${idMap.size} tasks for import.`); 194 | 195 | const insertDependencyStmt = this.db.prepare(` 196 | INSERT INTO task_dependencies (task_id, depends_on_task_id) 197 | VALUES (?, ?) ON CONFLICT DO NOTHING 198 | `); 199 | let depCount = 0; 200 | const processDeps = (task: ExportTask) => { 201 | const newTaskId = idMap.get(task.task_id); 202 | if (newTaskId && task.dependencies && task.dependencies.length > 0) { 203 | for (const oldDepId of task.dependencies) { 204 | const newDepId = idMap.get(oldDepId); 205 | if (newDepId) { 206 | insertDependencyStmt.run(newTaskId, newDepId); 207 | depCount++; 208 | } else { 209 | logger.warn(`[ProjectService] Dependency task ID ${oldDepId} not found in import map for task ${task.task_id}. Skipping dependency.`); 210 | } 211 | } 212 | } 213 | if (task.subtasks && task.subtasks.length > 0) { 214 | task.subtasks.forEach(processDeps); 215 | } 216 | }; 217 | importData.tasks.forEach(processDeps); 218 | logger.info(`[ProjectService] Processed ${depCount} dependencies for import.`); 219 | 220 | return { project_id: newProjectId }; 221 | }); 222 | 223 | try { 224 | const result = importTransaction(); 225 | logger.info(`[ProjectService] Successfully imported project. New project ID: ${result.project_id}`); 226 | return result; 227 | } catch (error) { 228 | logger.error(`[ProjectService] Error during import transaction:`, error); 229 | if (error instanceof NotFoundError || error instanceof ValidationError || error instanceof ConflictError) { 230 | throw error; 231 | } 232 | throw new Error(`Failed to import project: ${error instanceof Error ? error.message : 'Unknown database error'}`); 233 | } 234 | } 235 | 236 | /** 237 | * Deletes a project and all its associated data (tasks, dependencies). 238 | * @param projectId - The ID of the project to delete. 239 | * @returns A boolean indicating success (true if deleted, false if not found initially). 240 | * @throws {NotFoundError} If the project is not found. 241 | * @throws {Error} If the database operation fails. 242 | */ 243 | public async deleteProject(projectId: string): Promise { 244 | logger.info(`[ProjectService] Attempting to delete project: ${projectId}`); 245 | 246 | // 1. Validate Project Existence *before* attempting delete 247 | const projectExists = this.projectRepository.findById(projectId); 248 | if (!projectExists) { 249 | logger.warn(`[ProjectService] Project not found for deletion: ${projectId}`); 250 | throw new NotFoundError(`Project with ID ${projectId} not found.`); 251 | } 252 | 253 | // 2. Call Repository delete method 254 | try { 255 | // The repository method handles the actual DELETE operation on the projects table. 256 | // Cascade delete defined in the schema handles tasks and dependencies. 257 | const deletedCount = this.projectRepository.deleteProject(projectId); 258 | 259 | if (deletedCount !== 1) { 260 | // This shouldn't happen if findById succeeded, but log a warning if it does. 261 | logger.warn(`[ProjectService] Expected to delete 1 project, but repository reported ${deletedCount} deletions for project ${projectId}.`); 262 | // Still return true as the project is gone, but log indicates potential issue. 263 | } 264 | 265 | logger.info(`[ProjectService] Successfully deleted project ${projectId} and associated data.`); 266 | return true; // Indicate success 267 | 268 | } catch (error) { 269 | logger.error(`[ProjectService] Error deleting project ${projectId}:`, error); 270 | throw error; // Re-throw database or other errors 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/repositories/TaskRepository.ts: -------------------------------------------------------------------------------- 1 | import { Database as Db, Statement } from 'better-sqlite3'; 2 | import { logger } from '../utils/logger.js'; 3 | 4 | // Define the structure for task data in the database 5 | // Aligning with schema.sql and feature specs 6 | export interface TaskData { 7 | task_id: string; // UUID 8 | project_id: string; // UUID 9 | parent_task_id?: string | null; // UUID or null 10 | description: string; 11 | status: 'todo' | 'in-progress' | 'review' | 'done'; 12 | priority: 'high' | 'medium' | 'low'; 13 | created_at: string; // ISO8601 14 | updated_at: string; // ISO8601 15 | } 16 | 17 | // Define the structure for dependency data 18 | export interface DependencyData { 19 | task_id: string; 20 | depends_on_task_id: string; 21 | } 22 | 23 | export class TaskRepository { 24 | private db: Db; 25 | private insertTaskStmt: Statement | null = null; 26 | private insertDependencyStmt: Statement | null = null; 27 | 28 | constructor(db: Db) { 29 | this.db = db; 30 | // Prepare statements for efficiency 31 | this.prepareStatements(); 32 | } 33 | 34 | private prepareStatements(): void { 35 | try { 36 | this.insertTaskStmt = this.db.prepare(` 37 | INSERT INTO tasks ( 38 | task_id, project_id, parent_task_id, description, 39 | status, priority, created_at, updated_at 40 | ) VALUES ( 41 | @task_id, @project_id, @parent_task_id, @description, 42 | @status, @priority, @created_at, @updated_at 43 | ) 44 | `); 45 | 46 | this.insertDependencyStmt = this.db.prepare(` 47 | INSERT INTO task_dependencies (task_id, depends_on_task_id) 48 | VALUES (@task_id, @depends_on_task_id) 49 | ON CONFLICT(task_id, depends_on_task_id) DO NOTHING -- Ignore if dependency already exists 50 | `); 51 | } catch (error) { 52 | logger.error('[TaskRepository] Failed to prepare statements:', error); 53 | // Handle error appropriately, maybe re-throw or set a flag 54 | throw error; 55 | } 56 | } 57 | 58 | /** 59 | * Creates a new task and optionally its dependencies in the database. 60 | * Uses a transaction to ensure atomicity. 61 | * @param task - The core task data to insert. 62 | * @param dependencies - An array of dependency task IDs for this task. 63 | * @throws {Error} If the database operation fails. 64 | */ 65 | public create(task: TaskData, dependencies: string[] = []): void { 66 | if (!this.insertTaskStmt || !this.insertDependencyStmt) { 67 | logger.error('[TaskRepository] Statements not prepared. Cannot create task.'); 68 | throw new Error('TaskRepository statements not initialized.'); 69 | } 70 | 71 | // Use a transaction for atomicity 72 | const transaction = this.db.transaction((taskData: TaskData, deps: string[]) => { 73 | // Insert the main task 74 | const taskInfo = this.insertTaskStmt!.run(taskData); 75 | if (taskInfo.changes !== 1) { 76 | throw new Error(`Failed to insert task ${taskData.task_id}. Changes: ${taskInfo.changes}`); 77 | } 78 | 79 | // Insert dependencies 80 | for (const depId of deps) { 81 | const depData: DependencyData = { 82 | task_id: taskData.task_id, 83 | depends_on_task_id: depId, 84 | }; 85 | const depInfo = this.insertDependencyStmt!.run(depData); 86 | // We don't strictly need to check changes here due to ON CONFLICT DO NOTHING 87 | } 88 | return taskInfo.changes; // Indicate success 89 | }); 90 | 91 | try { 92 | transaction(task, dependencies); 93 | logger.info(`[TaskRepository] Created task ${task.task_id} with ${dependencies.length} dependencies.`); 94 | } catch (error) { 95 | logger.error(`[TaskRepository] Failed to create task ${task.task_id} transaction:`, error); 96 | throw error; // Re-throw to be handled by the service layer 97 | } 98 | } 99 | 100 | /** 101 | * Finds tasks by project ID, optionally filtering by status. 102 | * Does not handle subtask nesting directly in this query for V1 simplicity. 103 | * @param projectId - The ID of the project. 104 | * @param statusFilter - Optional status to filter by. 105 | * @returns An array of matching task data. 106 | */ 107 | public findByProjectId(projectId: string, statusFilter?: TaskData['status']): TaskData[] { 108 | let sql = ` 109 | SELECT task_id, project_id, parent_task_id, description, status, priority, created_at, updated_at 110 | FROM tasks 111 | WHERE project_id = ? 112 | `; 113 | const params: (string | null)[] = [projectId]; 114 | 115 | if (statusFilter) { 116 | sql += ` AND status = ?`; 117 | params.push(statusFilter); 118 | } 119 | 120 | // For simplicity in V1, we only fetch top-level tasks or all tasks depending on include_subtasks strategy in service 121 | // If we only wanted top-level: sql += ` AND parent_task_id IS NULL`; 122 | // If fetching all and structuring in service, this query is fine. 123 | 124 | sql += ` ORDER BY created_at ASC`; // Default sort order 125 | 126 | try { 127 | const stmt = this.db.prepare(sql); 128 | const tasks = stmt.all(...params) as TaskData[]; 129 | logger.debug(`[TaskRepository] Found ${tasks.length} tasks for project ${projectId} with status filter '${statusFilter || 'none'}'`); 130 | return tasks; 131 | } catch (error) { 132 | logger.error(`[TaskRepository] Failed to find tasks for project ${projectId}:`, error); 133 | throw error; // Re-throw 134 | } 135 | } 136 | 137 | /** 138 | * Finds a single task by its ID and project ID. 139 | * @param projectId - The project ID. 140 | * @param taskId - The task ID. 141 | * @returns The task data if found, otherwise undefined. 142 | */ 143 | public findById(projectId: string, taskId: string): TaskData | undefined { 144 | const sql = ` 145 | SELECT task_id, project_id, parent_task_id, description, status, priority, created_at, updated_at 146 | FROM tasks 147 | WHERE project_id = ? AND task_id = ? 148 | `; 149 | try { 150 | const stmt = this.db.prepare(sql); 151 | const task = stmt.get(projectId, taskId) as TaskData | undefined; 152 | logger.debug(`[TaskRepository] Found task ${taskId} in project ${projectId}: ${!!task}`); 153 | return task; 154 | } catch (error) { 155 | logger.error(`[TaskRepository] Failed to find task ${taskId} in project ${projectId}:`, error); 156 | throw error; 157 | } 158 | } 159 | 160 | /** 161 | * Finds the direct subtasks for a given parent task ID. 162 | * @param parentTaskId - The ID of the parent task. 163 | * @returns An array of direct subtask data. 164 | */ 165 | public findSubtasks(parentTaskId: string): TaskData[] { 166 | const sql = ` 167 | SELECT task_id, project_id, parent_task_id, description, status, priority, created_at, updated_at 168 | FROM tasks 169 | WHERE parent_task_id = ? 170 | ORDER BY created_at ASC 171 | `; 172 | try { 173 | const stmt = this.db.prepare(sql); 174 | const subtasks = stmt.all(parentTaskId) as TaskData[]; 175 | logger.debug(`[TaskRepository] Found ${subtasks.length} subtasks for parent ${parentTaskId}`); 176 | return subtasks; 177 | } catch (error) { 178 | logger.error(`[TaskRepository] Failed to find subtasks for parent ${parentTaskId}:`, error); 179 | throw error; 180 | } 181 | } 182 | 183 | /** 184 | * Finds the IDs of tasks that the given task depends on. 185 | * @param taskId - The ID of the task whose dependencies are needed. 186 | * @returns An array of task IDs that this task depends on. 187 | */ 188 | public findDependencies(taskId: string): string[] { 189 | const sql = `SELECT depends_on_task_id FROM task_dependencies WHERE task_id = ?`; 190 | try { 191 | const stmt = this.db.prepare(sql); 192 | // Ensure result is always an array of strings 193 | const results = stmt.all(taskId) as { depends_on_task_id: string }[]; 194 | const dependencyIds = results.map(row => row.depends_on_task_id); 195 | logger.debug(`[TaskRepository] Found ${dependencyIds.length} dependencies for task ${taskId}`); 196 | return dependencyIds; 197 | } catch (error) { 198 | logger.error(`[TaskRepository] Failed to find dependencies for task ${taskId}:`, error); 199 | throw error; 200 | } 201 | } 202 | 203 | 204 | /** 205 | * Updates the status and updated_at timestamp for a list of tasks within a project. 206 | * Assumes task existence has already been verified. 207 | * @param projectId - The project ID. 208 | * @param taskIds - An array of task IDs to update. 209 | * @param status - The new status to set. 210 | * @param timestamp - The ISO8601 timestamp for updated_at. 211 | * @returns The number of rows affected by the update. 212 | * @throws {Error} If the database operation fails. 213 | */ 214 | public updateStatus(projectId: string, taskIds: string[], status: TaskData['status'], timestamp: string): number { 215 | if (taskIds.length === 0) { 216 | return 0; 217 | } 218 | 219 | // Create placeholders for the IN clause 220 | const placeholders = taskIds.map(() => '?').join(','); 221 | const sql = ` 222 | UPDATE tasks 223 | SET status = ?, updated_at = ? 224 | WHERE project_id = ? AND task_id IN (${placeholders}) 225 | `; 226 | const params = [status, timestamp, projectId, ...taskIds]; 227 | 228 | try { 229 | const stmt = this.db.prepare(sql); 230 | const info = stmt.run(...params); 231 | logger.info(`[TaskRepository] Updated status for ${info.changes} tasks in project ${projectId} to ${status}.`); 232 | return info.changes; 233 | } catch (error) { 234 | logger.error(`[TaskRepository] Failed to update status for tasks in project ${projectId}:`, error); 235 | throw error; 236 | } 237 | } 238 | 239 | /** 240 | * Checks if all provided task IDs exist within the specified project. 241 | * @param projectId - The project ID. 242 | * @param taskIds - An array of task IDs to check. 243 | * @returns An object indicating if all exist and a list of missing IDs if not. 244 | * @throws {Error} If the database operation fails. 245 | */ 246 | public checkTasksExist(projectId: string, taskIds: string[]): { allExist: boolean; missingIds: string[] } { 247 | if (taskIds.length === 0) { 248 | return { allExist: true, missingIds: [] }; 249 | } 250 | 251 | const placeholders = taskIds.map(() => '?').join(','); 252 | const sql = ` 253 | SELECT task_id FROM tasks 254 | WHERE project_id = ? AND task_id IN (${placeholders}) 255 | `; 256 | const params = [projectId, ...taskIds]; 257 | 258 | try { 259 | const stmt = this.db.prepare(sql); 260 | const foundTasks = stmt.all(...params) as { task_id: string }[]; 261 | const foundIds = new Set(foundTasks.map(t => t.task_id)); 262 | 263 | const missingIds = taskIds.filter(id => !foundIds.has(id)); 264 | const allExist = missingIds.length === 0; 265 | 266 | if (!allExist) { 267 | logger.warn(`[TaskRepository] Missing tasks in project ${projectId}:`, missingIds); 268 | } 269 | return { allExist, missingIds }; 270 | 271 | } catch (error) { 272 | logger.error(`[TaskRepository] Failed to check task existence in project ${projectId}:`, error); 273 | throw error; 274 | } 275 | } 276 | 277 | /** 278 | * Deletes all direct subtasks of a given parent task. 279 | * @param parentTaskId - The ID of the parent task whose subtasks should be deleted. 280 | * @returns The number of subtasks deleted. 281 | * @throws {Error} If the database operation fails. 282 | */ 283 | public deleteSubtasks(parentTaskId: string): number { 284 | const sql = `DELETE FROM tasks WHERE parent_task_id = ?`; 285 | try { 286 | const stmt = this.db.prepare(sql); 287 | const info = stmt.run(parentTaskId); 288 | logger.info(`[TaskRepository] Deleted ${info.changes} subtasks for parent ${parentTaskId}.`); 289 | return info.changes; 290 | } catch (error) { 291 | logger.error(`[TaskRepository] Failed to delete subtasks for parent ${parentTaskId}:`, error); 292 | throw error; 293 | } 294 | } 295 | 296 | /** 297 | * Finds tasks that are ready to be worked on (status 'todo' and all dependencies 'done'). 298 | * Orders them by priority ('high', 'medium', 'low') then creation date. 299 | * @param projectId - The project ID. 300 | * @returns An array of ready task data, ordered by priority and creation date. 301 | */ 302 | public findReadyTasks(projectId: string): TaskData[] { 303 | // This query finds tasks in the project with status 'todo' 304 | // AND for which no dependency exists OR all existing dependencies have status 'done'. 305 | const sql = ` 306 | SELECT t.task_id, t.project_id, t.parent_task_id, t.description, t.status, t.priority, t.created_at, t.updated_at 307 | FROM tasks t 308 | WHERE t.project_id = ? AND t.status = 'todo' 309 | AND NOT EXISTS ( 310 | SELECT 1 311 | FROM task_dependencies td 312 | JOIN tasks dep_task ON td.depends_on_task_id = dep_task.task_id 313 | WHERE td.task_id = t.task_id AND dep_task.status != 'done' 314 | ) 315 | ORDER BY 316 | CASE t.priority 317 | WHEN 'high' THEN 1 318 | WHEN 'medium' THEN 2 319 | WHEN 'low' THEN 3 320 | ELSE 4 -- Should not happen based on CHECK constraint 321 | END ASC, 322 | t.created_at ASC 323 | `; 324 | try { 325 | const stmt = this.db.prepare(sql); 326 | const tasks = stmt.all(projectId) as TaskData[]; 327 | logger.debug(`[TaskRepository] Found ${tasks.length} ready tasks for project ${projectId}`); 328 | return tasks; 329 | } catch (error) { 330 | logger.error(`[TaskRepository] Failed to find ready tasks for project ${projectId}:`, error); 331 | throw error; 332 | } 333 | } 334 | 335 | /** 336 | * Finds ALL tasks for a given project ID, ordered by creation date. 337 | * @param projectId - The project ID. 338 | * @returns An array of all task data for the project. 339 | */ 340 | public findAllTasksForProject(projectId: string): TaskData[] { 341 | const sql = ` 342 | SELECT task_id, project_id, parent_task_id, description, status, priority, created_at, updated_at 343 | FROM tasks 344 | WHERE project_id = ? 345 | ORDER BY created_at ASC 346 | `; 347 | try { 348 | const stmt = this.db.prepare(sql); 349 | const tasks = stmt.all(projectId) as TaskData[]; 350 | logger.debug(`[TaskRepository] Found all ${tasks.length} tasks for project ${projectId}`); 351 | return tasks; 352 | } catch (error) { 353 | logger.error(`[TaskRepository] Failed to find all tasks for project ${projectId}:`, error); 354 | throw error; 355 | } 356 | } 357 | 358 | /** 359 | * Finds ALL dependencies for tasks within a given project ID. 360 | * @param projectId - The project ID. 361 | * @returns An array of all dependency relationships for the project. 362 | */ 363 | public findAllDependenciesForProject(projectId: string): DependencyData[] { 364 | // Select dependencies where the *dependent* task belongs to the project 365 | const sql = ` 366 | SELECT td.task_id, td.depends_on_task_id 367 | FROM task_dependencies td 368 | JOIN tasks t ON td.task_id = t.task_id 369 | WHERE t.project_id = ? 370 | `; 371 | try { 372 | const stmt = this.db.prepare(sql); 373 | const dependencies = stmt.all(projectId) as DependencyData[]; 374 | logger.debug(`[TaskRepository] Found ${dependencies.length} dependencies for project ${projectId}`); 375 | return dependencies; 376 | } catch (error) { 377 | logger.error(`[TaskRepository] Failed to find all dependencies for project ${projectId}:`, error); 378 | throw error; 379 | } 380 | } 381 | 382 | 383 | // --- Add other methods later --- 384 | /** 385 | * Updates a task's description, priority, and/or dependencies. 386 | * Handles dependency replacement atomically within a transaction. 387 | * @param projectId - The project ID. 388 | * @param taskId - The task ID to update. 389 | * @param updatePayload - Object containing optional fields to update. 390 | * @param timestamp - The ISO8601 timestamp for updated_at. 391 | * @returns The updated task data. 392 | * @throws {Error} If the task doesn't exist or the database operation fails. 393 | */ 394 | public updateTask( 395 | projectId: string, 396 | taskId: string, 397 | updatePayload: { description?: string; priority?: TaskData['priority']; dependencies?: string[] }, 398 | timestamp: string 399 | ): TaskData { 400 | 401 | const transaction = this.db.transaction(() => { 402 | const setClauses: string[] = []; 403 | const params: (string | null)[] = []; 404 | 405 | if (updatePayload.description !== undefined) { 406 | setClauses.push('description = ?'); 407 | params.push(updatePayload.description); 408 | } 409 | if (updatePayload.priority !== undefined) { 410 | setClauses.push('priority = ?'); 411 | params.push(updatePayload.priority); 412 | } 413 | 414 | // Always update the timestamp 415 | setClauses.push('updated_at = ?'); 416 | params.push(timestamp); 417 | 418 | // If nothing else to update, we still update the timestamp 419 | if (setClauses.length === 1 && updatePayload.dependencies === undefined) { 420 | logger.warn(`[TaskRepository] updateTask called for ${taskId} with no fields to update other than timestamp.`); 421 | // Or potentially throw an error if this shouldn't happen based on service validation 422 | } 423 | 424 | // Update the main task table if there are fields to update 425 | let changes = 0; 426 | if (setClauses.length > 0) { 427 | const updateSql = ` 428 | UPDATE tasks 429 | SET ${setClauses.join(', ')} 430 | WHERE project_id = ? AND task_id = ? 431 | `; 432 | params.push(projectId, taskId); 433 | 434 | const updateStmt = this.db.prepare(updateSql); 435 | const info = updateStmt.run(...params); 436 | changes = info.changes; 437 | 438 | if (changes !== 1) { 439 | // Check if the task actually exists before throwing generic error 440 | const exists = this.findById(projectId, taskId); 441 | if (!exists) { 442 | throw new Error(`Task ${taskId} not found in project ${projectId}.`); // Will be caught and mapped later 443 | } else { 444 | throw new Error(`Failed to update task ${taskId}. Expected 1 change, got ${changes}.`); 445 | } 446 | } 447 | logger.debug(`[TaskRepository] Updated task ${taskId} fields.`); 448 | } 449 | 450 | 451 | // Handle dependencies if provided (replaces existing) 452 | if (updatePayload.dependencies !== undefined) { 453 | if (!this.insertDependencyStmt) { 454 | throw new Error('TaskRepository insertDependencyStmt not initialized.'); 455 | } 456 | // 1. Delete existing dependencies for this task 457 | const deleteDepsStmt = this.db.prepare(`DELETE FROM task_dependencies WHERE task_id = ?`); 458 | const deleteInfo = deleteDepsStmt.run(taskId); 459 | logger.debug(`[TaskRepository] Deleted ${deleteInfo.changes} existing dependencies for task ${taskId}.`); 460 | 461 | // 2. Insert new dependencies 462 | const newDeps = updatePayload.dependencies; 463 | for (const depId of newDeps) { 464 | const depData: DependencyData = { 465 | task_id: taskId, 466 | depends_on_task_id: depId, 467 | }; 468 | // ON CONFLICT DO NOTHING handles duplicates or self-references if schema allows 469 | this.insertDependencyStmt.run(depData); 470 | } 471 | logger.debug(`[TaskRepository] Inserted ${newDeps.length} new dependencies for task ${taskId}.`); 472 | } 473 | 474 | // Fetch and return the updated task data 475 | const updatedTask = this.findById(projectId, taskId); 476 | if (!updatedTask) { 477 | // Should not happen if update succeeded, but safety check 478 | throw new Error(`Failed to retrieve updated task ${taskId} after update.`); 479 | } 480 | return updatedTask; 481 | }); 482 | 483 | try { 484 | const result = transaction(); 485 | logger.info(`[TaskRepository] Successfully updated task ${taskId}.`); 486 | return result; 487 | } catch (error) { 488 | logger.error(`[TaskRepository] Failed transaction for updating task ${taskId}:`, error); 489 | throw error; // Re-throw to be handled by the service layer 490 | } 491 | } 492 | 493 | 494 | /** 495 | * Deletes multiple tasks by their IDs within a specific project. 496 | * Relies on ON DELETE CASCADE for subtasks and dependencies. 497 | * @param projectId - The project ID. 498 | * @param taskIds - An array of task IDs to delete. 499 | * @returns The number of tasks deleted. 500 | * @throws {Error} If the database operation fails. 501 | */ 502 | public deleteTasks(projectId: string, taskIds: string[]): number { 503 | if (taskIds.length === 0) { 504 | return 0; 505 | } 506 | 507 | // Create placeholders for the IN clause 508 | const placeholders = taskIds.map(() => '?').join(','); 509 | const sql = ` 510 | DELETE FROM tasks 511 | WHERE project_id = ? AND task_id IN (${placeholders}) 512 | `; 513 | const params = [projectId, ...taskIds]; 514 | 515 | try { 516 | const stmt = this.db.prepare(sql); 517 | const info = stmt.run(...params); 518 | logger.info(`[TaskRepository] Deleted ${info.changes} tasks from project ${projectId}.`); 519 | // Note: Cascade deletes for subtasks/dependencies happen automatically via schema. 520 | return info.changes; 521 | } catch (error) { 522 | logger.error(`[TaskRepository] Failed to delete tasks from project ${projectId}:`, error); 523 | throw error; 524 | } 525 | } 526 | 527 | 528 | // --- Add other methods later --- 529 | // deleteById(taskId: string): void; 530 | } 531 | -------------------------------------------------------------------------------- /src/services/TaskService.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { TaskRepository, TaskData } from '../repositories/TaskRepository.js'; 3 | import { ProjectRepository } from '../repositories/ProjectRepository.js'; // Needed to check project existence 4 | import { logger } from '../utils/logger.js'; 5 | import { NotFoundError, ValidationError } from '../utils/errors.js'; // Using custom errors 6 | 7 | // Define the input structure for adding a task, based on feature spec 8 | export interface AddTaskInput { 9 | project_id: string; 10 | description: string; 11 | dependencies?: string[]; 12 | priority?: 'high' | 'medium' | 'low'; 13 | status?: 'todo' | 'in-progress' | 'review' | 'done'; 14 | } 15 | 16 | // Options for listing tasks 17 | export interface ListTasksOptions { 18 | project_id: string; 19 | status?: TaskData['status']; 20 | include_subtasks?: boolean; 21 | } 22 | 23 | // Type for task data potentially including nested subtasks 24 | export interface StructuredTaskData extends TaskData { 25 | subtasks?: StructuredTaskData[]; 26 | } 27 | 28 | // Type for full task details including dependencies and subtasks 29 | export interface FullTaskData extends TaskData { 30 | dependencies: string[]; 31 | subtasks: TaskData[]; // For V1 showTask, just return direct subtasks without their own nesting/deps 32 | } 33 | 34 | // Input for expanding a task 35 | export interface ExpandTaskInput { 36 | project_id: string; 37 | task_id: string; // Parent task ID 38 | subtask_descriptions: string[]; 39 | force?: boolean; 40 | } 41 | 42 | 43 | import { Database as Db } from 'better-sqlite3'; // Import Db type 44 | import { ConflictError } from '../utils/errors.js'; // Import ConflictError 45 | 46 | export class TaskService { 47 | private taskRepository: TaskRepository; 48 | private projectRepository: ProjectRepository; 49 | private db: Db; // Add db instance 50 | 51 | constructor( 52 | db: Db, // Inject Db instance 53 | taskRepository: TaskRepository, 54 | projectRepository: ProjectRepository 55 | ) { 56 | this.db = db; // Store db instance 57 | this.taskRepository = taskRepository; 58 | this.projectRepository = projectRepository; 59 | } 60 | 61 | /** 62 | * Adds a new task to a specified project. 63 | */ 64 | public async addTask(input: AddTaskInput): Promise { 65 | logger.info(`[TaskService] Attempting to add task to project: ${input.project_id}`); 66 | const projectExists = this.projectRepository.findById(input.project_id); 67 | if (!projectExists) { 68 | logger.warn(`[TaskService] Project not found: ${input.project_id}`); 69 | throw new NotFoundError(`Project with ID ${input.project_id} not found.`); 70 | } 71 | 72 | const taskId = uuidv4(); 73 | const now = new Date().toISOString(); 74 | const newTaskData: TaskData = { 75 | task_id: taskId, 76 | project_id: input.project_id, 77 | parent_task_id: null, 78 | description: input.description, 79 | status: input.status ?? 'todo', 80 | priority: input.priority ?? 'medium', 81 | created_at: now, 82 | updated_at: now, 83 | }; 84 | 85 | // TODO: Validate Dependency Existence 86 | 87 | try { 88 | this.taskRepository.create(newTaskData, input.dependencies); 89 | logger.info(`[TaskService] Successfully added task ${taskId} to project ${input.project_id}`); 90 | return newTaskData; 91 | } catch (error) { 92 | logger.error(`[TaskService] Error adding task to project ${input.project_id}:`, error); 93 | throw error; 94 | } 95 | } 96 | 97 | /** 98 | * Lists tasks for a project. 99 | */ 100 | public async listTasks(options: ListTasksOptions): Promise { 101 | logger.info(`[TaskService] Attempting to list tasks for project: ${options.project_id}`, options); 102 | const projectExists = this.projectRepository.findById(options.project_id); 103 | if (!projectExists) { 104 | logger.warn(`[TaskService] Project not found: ${options.project_id}`); 105 | throw new NotFoundError(`Project with ID ${options.project_id} not found.`); 106 | } 107 | 108 | try { 109 | const allTasks = this.taskRepository.findByProjectId(options.project_id, options.status); 110 | 111 | if (!options.include_subtasks) { 112 | const topLevelTasks = allTasks.filter(task => !task.parent_task_id); 113 | logger.info(`[TaskService] Found ${topLevelTasks.length} top-level tasks for project ${options.project_id}`); 114 | return topLevelTasks; 115 | } else { 116 | const taskMap: Map = new Map(); 117 | const rootTasks: StructuredTaskData[] = []; 118 | for (const task of allTasks) { 119 | taskMap.set(task.task_id, { ...task, subtasks: [] }); 120 | } 121 | for (const task of allTasks) { 122 | if (task.parent_task_id && taskMap.has(task.parent_task_id)) { 123 | const parent = taskMap.get(task.parent_task_id)!; 124 | parent.subtasks!.push(taskMap.get(task.task_id)!); 125 | } else if (!task.parent_task_id) { 126 | rootTasks.push(taskMap.get(task.task_id)!); 127 | } 128 | } 129 | logger.info(`[TaskService] Found ${rootTasks.length} structured root tasks for project ${options.project_id}`); 130 | return rootTasks; 131 | } 132 | } catch (error) { 133 | logger.error(`[TaskService] Error listing tasks for project ${options.project_id}:`, error); 134 | throw error; 135 | } 136 | } 137 | 138 | /** 139 | * Retrieves the full details of a single task. 140 | */ 141 | public async getTaskById(projectId: string, taskId: string): Promise { 142 | logger.info(`[TaskService] Attempting to get task ${taskId} for project ${projectId}`); 143 | const task = this.taskRepository.findById(projectId, taskId); 144 | if (!task) { 145 | logger.warn(`[TaskService] Task ${taskId} not found in project ${projectId}`); 146 | throw new NotFoundError(`Task with ID ${taskId} not found in project ${projectId}.`); 147 | } 148 | 149 | try { 150 | const dependencies = this.taskRepository.findDependencies(taskId); 151 | const subtasks = this.taskRepository.findSubtasks(taskId); 152 | const fullTaskData: FullTaskData = { 153 | ...task, 154 | dependencies: dependencies, 155 | subtasks: subtasks, 156 | }; 157 | logger.info(`[TaskService] Successfully retrieved task ${taskId}`); 158 | return fullTaskData; 159 | } catch (error) { 160 | logger.error(`[TaskService] Error retrieving details for task ${taskId}:`, error); 161 | throw error; 162 | } 163 | } 164 | 165 | /** 166 | * Sets the status for one or more tasks within a project. 167 | */ 168 | public async setTaskStatus(projectId: string, taskIds: string[], status: TaskData['status']): Promise { 169 | logger.info(`[TaskService] Attempting to set status to '${status}' for ${taskIds.length} tasks in project ${projectId}`); 170 | const projectExists = this.projectRepository.findById(projectId); 171 | if (!projectExists) { 172 | logger.warn(`[TaskService] Project not found: ${projectId}`); 173 | throw new NotFoundError(`Project with ID ${projectId} not found.`); 174 | } 175 | 176 | const existenceCheck = this.taskRepository.checkTasksExist(projectId, taskIds); 177 | if (!existenceCheck.allExist) { 178 | logger.warn(`[TaskService] One or more tasks not found in project ${projectId}:`, existenceCheck.missingIds); 179 | throw new NotFoundError(`One or more tasks not found in project ${projectId}: ${existenceCheck.missingIds.join(', ')}`); 180 | } 181 | 182 | try { 183 | const now = new Date().toISOString(); 184 | const updatedCount = this.taskRepository.updateStatus(projectId, taskIds, status, now); 185 | if (updatedCount !== taskIds.length) { 186 | logger.warn(`[TaskService] Expected to update ${taskIds.length} tasks, but ${updatedCount} were affected.`); 187 | } 188 | logger.info(`[TaskService] Successfully updated status for ${updatedCount} tasks in project ${projectId}`); 189 | return updatedCount; 190 | } catch (error) { 191 | logger.error(`[TaskService] Error setting status for tasks in project ${projectId}:`, error); 192 | throw error; 193 | } 194 | } 195 | 196 | 197 | /** 198 | * Expands a parent task by adding new subtasks. 199 | * Optionally deletes existing subtasks first if 'force' is true. 200 | * Uses a transaction to ensure atomicity. 201 | * @param input - Details including parent task ID, project ID, subtask descriptions, and force flag. 202 | * @returns The updated parent task details (including new subtasks). 203 | * @throws {NotFoundError} If the project or parent task is not found. 204 | * @throws {ConflictError} If subtasks exist and force is false. 205 | * @throws {Error} If the database operation fails. 206 | */ 207 | public async expandTask(input: ExpandTaskInput): Promise { 208 | const { project_id, task_id: parentTaskId, subtask_descriptions, force = false } = input; 209 | logger.info(`[TaskService] Attempting to expand task ${parentTaskId} in project ${project_id} with ${subtask_descriptions.length} subtasks (force=${force})`); 210 | 211 | // Use a transaction for the entire operation 212 | const expandTransaction = this.db.transaction(() => { 213 | // 1. Validate Parent Task Existence (within the transaction) 214 | const parentTask = this.taskRepository.findById(project_id, parentTaskId); 215 | if (!parentTask) { 216 | logger.warn(`[TaskService] Parent task ${parentTaskId} not found in project ${project_id}`); 217 | throw new NotFoundError(`Parent task with ID ${parentTaskId} not found in project ${project_id}.`); 218 | } 219 | 220 | // 2. Check for existing subtasks 221 | const existingSubtasks = this.taskRepository.findSubtasks(parentTaskId); 222 | 223 | // 3. Handle existing subtasks based on 'force' flag 224 | if (existingSubtasks.length > 0) { 225 | if (!force) { 226 | logger.warn(`[TaskService] Conflict: Task ${parentTaskId} already has subtasks and force=false.`); 227 | throw new ConflictError(`Task ${parentTaskId} already has subtasks. Use force=true to replace them.`); 228 | } else { 229 | logger.info(`[TaskService] Force=true: Deleting ${existingSubtasks.length} existing subtasks for parent ${parentTaskId}.`); 230 | this.taskRepository.deleteSubtasks(parentTaskId); 231 | // Note: Dependencies of deleted subtasks are implicitly handled by ON DELETE CASCADE in schema 232 | } 233 | } 234 | 235 | // 4. Create new subtasks 236 | const now = new Date().toISOString(); 237 | const createdSubtasks: TaskData[] = []; 238 | for (const description of subtask_descriptions) { 239 | const subtaskId = uuidv4(); 240 | const newSubtaskData: TaskData = { 241 | task_id: subtaskId, 242 | project_id: project_id, 243 | parent_task_id: parentTaskId, 244 | description: description, // Assuming length validation done by Zod 245 | status: 'todo', // Default status 246 | priority: 'medium', // Default priority 247 | created_at: now, 248 | updated_at: now, 249 | }; 250 | // Use the repository's create method (which handles its own transaction part for task+deps, but is fine here) 251 | // We pass an empty array for dependencies as expandTask doesn't set them for new subtasks 252 | this.taskRepository.create(newSubtaskData, []); 253 | createdSubtasks.push(newSubtaskData); 254 | } 255 | 256 | // 5. Fetch updated parent task details (including new subtasks and existing dependencies) 257 | // We re-fetch to get the consistent state after the transaction commits. 258 | // Note: This requires the transaction function to return the necessary data. 259 | // Alternatively, construct the FullTaskData manually here. Let's construct manually. 260 | const dependencies = this.taskRepository.findDependencies(parentTaskId); // Fetch parent's dependencies 261 | const finalParentData: FullTaskData = { 262 | ...parentTask, // Use data fetched at the start of transaction 263 | updated_at: now, // Update timestamp conceptually (though not saved unless status changes) 264 | dependencies: dependencies, 265 | subtasks: createdSubtasks, // Return the newly created subtasks 266 | }; 267 | return finalParentData; 268 | }); 269 | 270 | try { 271 | // Execute the transaction 272 | const result = expandTransaction(); 273 | logger.info(`[TaskService] Successfully expanded task ${parentTaskId} with ${subtask_descriptions.length} new subtasks.`); 274 | return result; 275 | } catch (error) { 276 | logger.error(`[TaskService] Error expanding task ${parentTaskId}:`, error); 277 | // Re-throw specific errors or generic internal error 278 | if (error instanceof NotFoundError || error instanceof ConflictError) { 279 | throw error; 280 | } 281 | throw new Error(`Failed to expand task: ${error instanceof Error ? error.message : 'Unknown error'}`); 282 | } 283 | } 284 | 285 | 286 | /** 287 | * Finds the next available task based on readiness (status 'todo', dependencies 'done') 288 | * and prioritization (priority, creation date). 289 | * @param projectId - The project ID. 290 | * @returns The full details of the next task, or null if no task is ready. 291 | * @throws {NotFoundError} If the project is not found. 292 | * @throws {Error} If the database operation fails. 293 | */ 294 | public async getNextTask(projectId: string): Promise { 295 | logger.info(`[TaskService] Attempting to get next task for project ${projectId}`); 296 | 297 | // 1. Validate Project Existence 298 | const projectExists = this.projectRepository.findById(projectId); 299 | if (!projectExists) { 300 | logger.warn(`[TaskService] Project not found: ${projectId}`); 301 | throw new NotFoundError(`Project with ID ${projectId} not found.`); 302 | } 303 | 304 | // 2. Find ready tasks using the repository method 305 | try { 306 | const readyTasks = this.taskRepository.findReadyTasks(projectId); 307 | 308 | if (readyTasks.length === 0) { 309 | logger.info(`[TaskService] No ready tasks found for project ${projectId}`); 310 | return null; // No task is ready 311 | } 312 | 313 | // 3. The first task in the list is the highest priority one due to repo ordering 314 | const nextTask = readyTasks[0]; 315 | logger.info(`[TaskService] Next task identified: ${nextTask.task_id}`); 316 | 317 | // 4. Fetch full details (dependencies, subtasks) for the selected task 318 | // We could potentially optimize this if findReadyTasks returned more details, 319 | // but for separation of concerns, we call getTaskById logic (or similar). 320 | // Re-using getTaskById logic: 321 | return await this.getTaskById(projectId, nextTask.task_id); 322 | 323 | } catch (error) { 324 | logger.error(`[TaskService] Error getting next task for project ${projectId}:`, error); 325 | throw error; // Re-throw repository or other errors 326 | } 327 | } 328 | 329 | /** 330 | * Updates specific fields of an existing task. 331 | * @param input - Contains project ID, task ID, and optional fields to update. 332 | * @returns The full details of the updated task. 333 | * @throws {ValidationError} If no update fields are provided or if dependencies are invalid. 334 | * @throws {NotFoundError} If the project, task, or any specified dependency task is not found. 335 | * @throws {Error} If the database operation fails. 336 | */ 337 | public async updateTask(input: { 338 | project_id: string; 339 | task_id: string; 340 | description?: string; 341 | priority?: TaskData['priority']; 342 | dependencies?: string[]; 343 | }): Promise { 344 | const { project_id, task_id } = input; 345 | logger.info(`[TaskService] Attempting to update task ${task_id} in project ${project_id}`); 346 | 347 | // 1. Validate that at least one field is being updated 348 | if (input.description === undefined && input.priority === undefined && input.dependencies === undefined) { 349 | throw new ValidationError("At least one field (description, priority, or dependencies) must be provided for update."); 350 | } 351 | 352 | // 2. Validate Project Existence (using repo method) 353 | const projectExists = this.projectRepository.findById(project_id); 354 | if (!projectExists) { 355 | logger.warn(`[TaskService] Project not found: ${project_id}`); 356 | throw new NotFoundError(`Project with ID ${project_id} not found.`); 357 | } 358 | 359 | // 3. Validate Task Existence (using repo method - findById also implicitly checks project scope) 360 | // We need the task data anyway if dependencies are involved, so fetch it now. 361 | const existingTask = this.taskRepository.findById(project_id, task_id); 362 | if (!existingTask) { 363 | logger.warn(`[TaskService] Task ${task_id} not found in project ${project_id}`); 364 | throw new NotFoundError(`Task with ID ${task_id} not found in project ${project_id}.`); 365 | } 366 | 367 | // 4. Validate Dependency Existence if provided 368 | if (input.dependencies !== undefined) { 369 | if (input.dependencies.length > 0) { 370 | const depCheck = this.taskRepository.checkTasksExist(project_id, input.dependencies); 371 | if (!depCheck.allExist) { 372 | logger.warn(`[TaskService] Invalid dependencies provided for task ${task_id}:`, depCheck.missingIds); 373 | throw new ValidationError(`One or more dependency tasks not found in project ${project_id}: ${depCheck.missingIds.join(', ')}`); 374 | } 375 | // Also check for self-dependency 376 | if (input.dependencies.includes(task_id)) { 377 | throw new ValidationError(`Task ${task_id} cannot depend on itself.`); 378 | } 379 | } 380 | // If input.dependencies is an empty array, it means "remove all dependencies" 381 | } 382 | 383 | // 5. Prepare payload for repository 384 | const updatePayload: { description?: string; priority?: TaskData['priority']; dependencies?: string[] } = {}; 385 | if (input.description !== undefined) updatePayload.description = input.description; 386 | if (input.priority !== undefined) updatePayload.priority = input.priority; 387 | if (input.dependencies !== undefined) updatePayload.dependencies = input.dependencies; 388 | 389 | // 6. Call Repository update method 390 | try { 391 | const now = new Date().toISOString(); 392 | // The repo method handles the transaction for task update + dependency replacement 393 | const updatedTaskData = this.taskRepository.updateTask(project_id, task_id, updatePayload, now); 394 | 395 | // 7. Fetch full details (including potentially updated dependencies and existing subtasks) 396 | // Re-use logic similar to getTaskById 397 | const finalDependencies = this.taskRepository.findDependencies(task_id); 398 | const finalSubtasks = this.taskRepository.findSubtasks(task_id); 399 | 400 | const fullUpdatedTask: FullTaskData = { 401 | ...updatedTaskData, // Use the data returned by the update method 402 | dependencies: finalDependencies, 403 | subtasks: finalSubtasks, 404 | }; 405 | 406 | logger.info(`[TaskService] Successfully updated task ${task_id} in project ${project_id}`); 407 | return fullUpdatedTask; 408 | 409 | } catch (error) { 410 | logger.error(`[TaskService] Error updating task ${task_id} in project ${project_id}:`, error); 411 | // Re-throw specific errors if needed, otherwise let the generic error propagate 412 | if (error instanceof Error && error.message.includes('not found')) { 413 | // Map repo's generic error for not found back to specific NotFoundError 414 | throw new NotFoundError(error.message); 415 | } 416 | throw error; // Re-throw other errors (like DB constraint errors or unexpected ones) 417 | } 418 | } 419 | 420 | 421 | /** 422 | * Deletes one or more tasks within a project. 423 | * @param projectId - The project ID. 424 | * @param taskIds - An array of task IDs to delete. 425 | * @returns The number of tasks successfully deleted. 426 | * @throws {NotFoundError} If the project or any of the specified tasks are not found. 427 | * @throws {Error} If the database operation fails. 428 | */ 429 | public async deleteTasks(projectId: string, taskIds: string[]): Promise { 430 | logger.info(`[TaskService] Attempting to delete ${taskIds.length} tasks from project ${projectId}`); 431 | 432 | // 1. Validate Project Existence 433 | const projectExists = this.projectRepository.findById(projectId); 434 | if (!projectExists) { 435 | logger.warn(`[TaskService] Project not found: ${projectId}`); 436 | throw new NotFoundError(`Project with ID ${projectId} not found.`); 437 | } 438 | 439 | // 2. Validate Task Existence *before* attempting delete 440 | // This ensures we report an accurate count and catch non-existent IDs early. 441 | const existenceCheck = this.taskRepository.checkTasksExist(projectId, taskIds); 442 | if (!existenceCheck.allExist) { 443 | logger.warn(`[TaskService] Cannot delete: One or more tasks not found in project ${projectId}:`, existenceCheck.missingIds); 444 | // Throw NotFoundError here, as InvalidParams might be confusing if some IDs were valid 445 | throw new NotFoundError(`One or more tasks to delete not found in project ${projectId}: ${existenceCheck.missingIds.join(', ')}`); 446 | } 447 | 448 | // 3. Call Repository delete method 449 | try { 450 | // The repository method handles the actual DELETE operation 451 | const deletedCount = this.taskRepository.deleteTasks(projectId, taskIds); 452 | 453 | // Double-check count (optional, but good sanity check) 454 | if (deletedCount !== taskIds.length) { 455 | logger.warn(`[TaskService] Expected to delete ${taskIds.length} tasks, but repository reported ${deletedCount} deletions.`); 456 | // This might indicate a race condition or unexpected DB behavior, though unlikely with cascade. 457 | // For V1, we'll trust the repo count but log the warning. 458 | } 459 | 460 | logger.info(`[TaskService] Successfully deleted ${deletedCount} tasks from project ${projectId}`); 461 | return deletedCount; 462 | 463 | } catch (error) { 464 | logger.error(`[TaskService] Error deleting tasks from project ${projectId}:`, error); 465 | throw error; // Re-throw database or other errors 466 | } 467 | } 468 | 469 | 470 | // --- Add other task service methods later --- 471 | } 472 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------