From a69d8c91dc9205a3fdaf9d32276144fa3bcad55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Fabja=C5=84czuk?= Date: Tue, 14 Oct 2025 20:01:21 +0200 Subject: [PATCH] feat: add configurable MCP tool loading to reduce LLM context usage (#1181) Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> --- .changeset/light-owls-stay.md | 35 ++ .env.example | 2 +- README.md | 74 +++- apps/docs/capabilities/mcp.mdx | 120 +++++ .../quick-start/configuration-quick.mdx | 19 + docs/configuration.md | 84 +++- mcp-server/src/index.js | 38 +- mcp-server/src/tools/index.js | 301 +++++++++---- mcp-server/src/tools/tool-registry.js | 168 +++++++ tests/helpers/tool-counts.js | 123 ++++++ .../unit/mcp/tools/tool-registration.test.js | 410 ++++++++++++++++++ 11 files changed, 1273 insertions(+), 101 deletions(-) create mode 100644 .changeset/light-owls-stay.md create mode 100644 mcp-server/src/tools/tool-registry.js create mode 100644 tests/helpers/tool-counts.js create mode 100644 tests/unit/mcp/tools/tool-registration.test.js diff --git a/.changeset/light-owls-stay.md b/.changeset/light-owls-stay.md new file mode 100644 index 00000000..d75f963b --- /dev/null +++ b/.changeset/light-owls-stay.md @@ -0,0 +1,35 @@ +--- +"task-master-ai": minor +--- + +Add configurable MCP tool loading to optimize LLM context usage + +You can now control which Task Master MCP tools are loaded by setting the `TASK_MASTER_TOOLS` environment variable in your MCP configuration. This helps reduce context usage for LLMs by only loading the tools you need. + +**Configuration Options:** + +- `all` (default): Load all 36 tools +- `core` or `lean`: Load only 7 essential tools for daily development + - Includes: `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` +- `standard`: Load 15 commonly used tools (all core tools plus 8 more) + - Additional tools: `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `generate`, `add_task`, `complexity_report` +- Custom list: Comma-separated tool names (e.g., `get_tasks,next_task,set_task_status`) + +**Example .mcp.json configuration:** + +```json +{ + "mcpServers": { + "task-master-ai": { + "command": "npx", + "args": ["-y", "task-master-ai"], + "env": { + "TASK_MASTER_TOOLS": "standard", + "ANTHROPIC_API_KEY": "your_key_here" + } + } + } +} +``` + +For complete details on all available tools, configuration examples, and usage guidelines, see the [MCP Tools documentation](https://docs.task-master.dev/capabilities/mcp#configurable-tool-loading). diff --git a/.env.example b/.env.example index b97c1efd..1a261b92 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,4 @@ OLLAMA_API_KEY=YOUR_OLLAMA_API_KEY_HERE VERTEX_PROJECT_ID=your-gcp-project-id VERTEX_LOCATION=us-central1 # Optional: Path to service account credentials JSON file (alternative to API key) -GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json +GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json \ No newline at end of file diff --git a/README.md b/README.md index a9db65cf..55502bc2 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor. "command": "npx", "args": ["-y", "task-master-ai"], "env": { + // "TASK_MASTER_TOOLS": "all", // Options: "all", "standard", "core", or comma-separated list of tools "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", @@ -148,6 +149,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor. "command": "npx", "args": ["-y", "task-master-ai"], "env": { + // "TASK_MASTER_TOOLS": "all", // Options: "all", "standard", "core", or comma-separated list of tools "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", @@ -196,7 +198,7 @@ Initialize taskmaster-ai in my project #### 5. Make sure you have a PRD (Recommended) -For **new projects**: Create your PRD at `.taskmaster/docs/prd.txt` +For **new projects**: Create your PRD at `.taskmaster/docs/prd.txt`. For **existing projects**: You can use `scripts/prd.txt` or migrate with `task-master migrate` An example PRD template is available after initialization in `.taskmaster/templates/example_prd.txt`. @@ -282,6 +284,76 @@ task-master generate task-master rules add windsurf,roo,vscode ``` +## Tool Loading Configuration + +### Optimizing MCP Tool Loading + +Task Master's MCP server supports selective tool loading to reduce context window usage. By default, all 36 tools are loaded (~21,000 tokens) to maintain backward compatibility with existing installations. + +You can optimize performance by configuring the `TASK_MASTER_TOOLS` environment variable: + +### Available Modes + +| Mode | Tools | Context Usage | Use Case | +|------|-------|--------------|----------| +| `all` (default) | 36 | ~21,000 tokens | Complete feature set - all tools available | +| `standard` | 15 | ~10,000 tokens | Common task management operations | +| `core` (or `lean`) | 7 | ~5,000 tokens | Essential daily development workflow | +| `custom` | Variable | Variable | Comma-separated list of specific tools | + +### Configuration Methods + +#### Method 1: Environment Variable in MCP Configuration + +Add `TASK_MASTER_TOOLS` to your MCP configuration file's `env` section: + +```jsonc +{ + "mcpServers": { // or "servers" for VS Code + "task-master-ai": { + "command": "npx", + "args": ["-y", "--package=task-master-ai", "task-master-ai"], + "env": { + "TASK_MASTER_TOOLS": "standard", // Options: "all", "standard", "core", "lean", or comma-separated list + "ANTHROPIC_API_KEY": "your-key-here", + // ... other API keys + } + } + } +} +``` + +#### Method 2: Claude Code CLI (One-Time Setup) + +For Claude Code users, you can set the mode during installation: + +```bash +# Core mode example (~70% token reduction) +claude mcp add task-master-ai --scope user \ + --env TASK_MASTER_TOOLS="core" \ + -- npx -y task-master-ai@latest + +# Custom tools example +claude mcp add task-master-ai --scope user \ + --env TASK_MASTER_TOOLS="get_tasks,next_task,set_task_status" \ + -- npx -y task-master-ai@latest +``` + +### Tool Sets Details + +**Core Tools (7):** `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` + +**Standard Tools (15):** All core tools plus `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `generate`, `add_task`, `complexity_report` + +**All Tools (36):** Complete set including project setup, task management, analysis, dependencies, tags, research, and more + +### Recommendations + +- **New users**: Start with `"standard"` mode for a good balance +- **Large projects**: Use `"core"` mode to minimize token usage +- **Complex workflows**: Use `"all"` mode or custom selection +- **Backward compatibility**: If not specified, defaults to `"all"` mode + ## Claude Code Support Task Master now supports Claude models through the Claude Code CLI, which requires no API key: diff --git a/apps/docs/capabilities/mcp.mdx b/apps/docs/capabilities/mcp.mdx index 25f6b779..1c210863 100644 --- a/apps/docs/capabilities/mcp.mdx +++ b/apps/docs/capabilities/mcp.mdx @@ -13,6 +13,126 @@ The MCP interface is built on top of the `fastmcp` library and registers a set o Each tool is defined with a name, a description, and a set of parameters that are validated using the `zod` library. The `execute` function of each tool calls the corresponding core logic function from `scripts/modules/task-manager.js`. +## Configurable Tool Loading + +To optimize LLM context usage, you can control which Task Master MCP tools are loaded using the `TASK_MASTER_TOOLS` environment variable. This is particularly useful when working with LLMs that have context limits or when you only need a subset of tools. + +### Configuration Modes + +#### All Tools (Default) +Loads all 36 available tools. Use when you need full Task Master functionality. + +```json +{ + "mcpServers": { + "task-master-ai": { + "command": "npx", + "args": ["-y", "task-master-ai"], + "env": { + "TASK_MASTER_TOOLS": "all", + "ANTHROPIC_API_KEY": "your_key_here" + } + } + } +} +``` + +If `TASK_MASTER_TOOLS` is not set, all tools are loaded by default. + +#### Core Tools (Lean Mode) +Loads only 7 essential tools for daily development. Ideal for minimal context usage. + +**Core tools included:** +- `get_tasks` - List all tasks +- `next_task` - Find the next task to work on +- `get_task` - Get detailed task information +- `set_task_status` - Update task status +- `update_subtask` - Add implementation notes +- `parse_prd` - Generate tasks from PRD +- `expand_task` - Break down tasks into subtasks + +```json +{ + "mcpServers": { + "task-master-ai": { + "command": "npx", + "args": ["-y", "task-master-ai"], + "env": { + "TASK_MASTER_TOOLS": "core", + "ANTHROPIC_API_KEY": "your_key_here" + } + } + } +} +``` + +You can also use `"lean"` as an alias for `"core"`. + +#### Standard Tools +Loads 15 commonly used tools. Balances functionality with context efficiency. + +**Standard tools include all core tools plus:** +- `initialize_project` - Set up new projects +- `analyze_project_complexity` - Analyze task complexity +- `expand_all` - Expand all eligible tasks +- `add_subtask` - Add subtasks manually +- `remove_task` - Remove tasks +- `generate` - Generate task markdown files +- `add_task` - Create new tasks +- `complexity_report` - View complexity analysis + +```json +{ + "mcpServers": { + "task-master-ai": { + "command": "npx", + "args": ["-y", "task-master-ai"], + "env": { + "TASK_MASTER_TOOLS": "standard", + "ANTHROPIC_API_KEY": "your_key_here" + } + } + } +} +``` + +#### Custom Tool Selection +Specify exactly which tools to load using a comma-separated list. Tool names are case-insensitive and support both underscores and hyphens. + +```json +{ + "mcpServers": { + "task-master-ai": { + "command": "npx", + "args": ["-y", "task-master-ai"], + "env": { + "TASK_MASTER_TOOLS": "get_tasks,next_task,set_task_status,update_subtask", + "ANTHROPIC_API_KEY": "your_key_here" + } + } + } +} +``` + +### Choosing the Right Configuration + +- **Use `core`/`lean`**: When working with basic task management workflows or when context limits are strict +- **Use `standard`**: For most development workflows that include task creation and analysis +- **Use `all`**: When you need full functionality including tag management, dependencies, and advanced features +- **Use custom list**: When you have specific tool requirements or want to experiment with minimal sets + +### Verification + +When the MCP server starts, it logs which tools were loaded: + +``` +Task Master MCP Server starting... +Tool mode configuration: standard +Loading standard tools +Registering 15 MCP tools (mode: standard) +Successfully registered 15/15 tools +``` + ## Tool Categories The MCP tools can be categorized in the same way as the core functionalities: diff --git a/apps/docs/getting-started/quick-start/configuration-quick.mdx b/apps/docs/getting-started/quick-start/configuration-quick.mdx index 74325602..754ec2d4 100644 --- a/apps/docs/getting-started/quick-start/configuration-quick.mdx +++ b/apps/docs/getting-started/quick-start/configuration-quick.mdx @@ -37,6 +37,25 @@ For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json } ``` + +**Optimize Context Usage**: You can control which Task Master MCP tools are loaded using the `TASK_MASTER_TOOLS` environment variable. This helps reduce LLM context usage by only loading the tools you need. + +Options: +- `all` (default) - All 36 tools +- `standard` - 15 commonly used tools +- `core` or `lean` - 7 essential tools + +Example: +```json +"env": { + "TASK_MASTER_TOOLS": "standard", + "ANTHROPIC_API_KEY": "your_key_here" +} +``` + +See the [MCP Tools documentation](/capabilities/mcp#configurable-tool-loading) for details. + + ### CLI Usage: `.env` File Create a `.env` file in your project root and include the keys for the providers you plan to use: diff --git a/docs/configuration.md b/docs/configuration.md index e5c97deb..2d538eb1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -59,6 +59,76 @@ Taskmaster uses two primary methods for configuration: - **Migration:** Use `task-master migrate` to move this to `.taskmaster/config.json`. - **Deprecation:** While still supported, you'll see warnings encouraging migration to the new structure. +## MCP Tool Loading Configuration + +### TASK_MASTER_TOOLS Environment Variable + +The `TASK_MASTER_TOOLS` environment variable controls which tools are loaded by the Task Master MCP server. This allows you to optimize token usage based on your workflow needs. + +> Note +> Prefer setting `TASK_MASTER_TOOLS` in your MCP client's `env` block (e.g., `.cursor/mcp.json`) or in CI/deployment env. The `.env` file is reserved for API keys/endpoints; avoid persisting non-secret settings there. + +#### Configuration Options + +- **`all`** (default): Loads all 36 available tools (~21,000 tokens) + - Best for: Users who need the complete feature set + - Use when: Working with complex projects requiring all Task Master features + - Backward compatibility: This is the default to maintain compatibility with existing installations + +- **`standard`**: Loads 15 commonly used tools (~10,000 tokens, 50% reduction) + - Best for: Regular task management workflows + - Tools included: All core tools plus project initialization, complexity analysis, task generation, and more + - Use when: You need a balanced set of features with reduced token usage + +- **`core`** (or `lean`): Loads 7 essential tools (~5,000 tokens, 70% reduction) + - Best for: Daily development with minimal token overhead + - Tools included: `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` + - Use when: Working in large contexts where token usage is critical + - Note: "lean" is an alias for "core" (same tools, token estimate and recommended use). You can refer to it as either "core" or "lean" when configuring. + +- **Custom list**: Comma-separated list of specific tool names + - Best for: Specialized workflows requiring specific tools + - Example: `"get_tasks,next_task,set_task_status"` + - Use when: You know exactly which tools you need + +#### How to Configure + +1. **In MCP configuration files** (`.cursor/mcp.json`, `.vscode/mcp.json`, etc.) - **Recommended**: + + ```jsonc + { + "mcpServers": { + "task-master-ai": { + "env": { + "TASK_MASTER_TOOLS": "standard", // Set tool loading mode + // API keys can still use .env for security + } + } + } + } + ``` + +2. **Via Claude Code CLI**: + + ```bash + claude mcp add task-master-ai --scope user \ + --env TASK_MASTER_TOOLS="core" \ + -- npx -y task-master-ai@latest + ``` + +3. **In CI/deployment environment variables**: + ```bash + export TASK_MASTER_TOOLS="standard" + node mcp-server/server.js + ``` + +#### Tool Loading Behavior + +- When `TASK_MASTER_TOOLS` is unset or empty, the system defaults to `"all"` +- Invalid tool names in a user-specified list are ignored (a warning is emitted for each) +- If every tool name in a custom list is invalid, the system falls back to `"all"` +- Tool names are case-insensitive (e.g., `"CORE"`, `"core"`, and `"Core"` are treated identically) + ## Environment Variables (`.env` file or MCP `env` block - For API Keys Only) - Used **exclusively** for sensitive API keys and specific endpoint URLs. @@ -223,10 +293,10 @@ node scripts/init.js ```bash # Set MCP provider for main role task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022 - - # Set MCP provider for research role + + # Set MCP provider for research role task-master models set-research --provider mcp --model claude-3-opus-20240229 - + # Verify configuration task-master models list ``` @@ -357,7 +427,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c "temperature": 0.7 }, "fallback": { - "provider": "azure", + "provider": "azure", "modelId": "gpt-4o-mini", "maxTokens": 10000, "temperature": 0.7 @@ -376,7 +446,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c "models": { "main": { "provider": "azure", - "modelId": "gpt-4o", + "modelId": "gpt-4o", "maxTokens": 16000, "temperature": 0.7, "baseURL": "https://your-resource-name.azure.com/openai/deployments" @@ -390,7 +460,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c "fallback": { "provider": "azure", "modelId": "gpt-4o-mini", - "maxTokens": 10000, + "maxTokens": 10000, "temperature": 0.7, "baseURL": "https://your-resource-name.azure.com/openai/deployments" } @@ -402,7 +472,7 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c ```bash # In .env file AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here - + # Optional: Override endpoint for all Azure models AZURE_OPENAI_ENDPOINT=https://your-resource-name.azure.com/openai/deployments ``` diff --git a/mcp-server/src/index.js b/mcp-server/src/index.js index bc03e404..802002bc 100644 --- a/mcp-server/src/index.js +++ b/mcp-server/src/index.js @@ -4,12 +4,14 @@ import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import fs from 'fs'; import logger from './logger.js'; -import { registerTaskMasterTools } from './tools/index.js'; +import { + registerTaskMasterTools, + getToolsConfiguration +} from './tools/index.js'; import ProviderRegistry from '../../src/provider-registry/index.js'; import { MCPProvider } from './providers/mcp-provider.js'; import packageJson from '../../package.json' with { type: 'json' }; -// Load environment variables dotenv.config(); // Constants @@ -29,12 +31,10 @@ class TaskMasterMCPServer { this.server = new FastMCP(this.options); this.initialized = false; - // Bind methods this.init = this.init.bind(this); this.start = this.start.bind(this); this.stop = this.stop.bind(this); - // Setup logging this.logger = logger; } @@ -44,8 +44,34 @@ class TaskMasterMCPServer { async init() { if (this.initialized) return; - // Pass the manager instance to the tool registration function - registerTaskMasterTools(this.server, this.asyncManager); + const normalizedToolMode = getToolsConfiguration(); + + this.logger.info('Task Master MCP Server starting...'); + this.logger.info(`Tool mode configuration: ${normalizedToolMode}`); + + const registrationResult = registerTaskMasterTools( + this.server, + normalizedToolMode + ); + + this.logger.info( + `Normalized tool mode: ${registrationResult.normalizedMode}` + ); + this.logger.info( + `Registered ${registrationResult.registeredTools.length} tools successfully` + ); + + if (registrationResult.registeredTools.length > 0) { + this.logger.debug( + `Registered tools: ${registrationResult.registeredTools.join(', ')}` + ); + } + + if (registrationResult.failedTools.length > 0) { + this.logger.warn( + `Failed to register ${registrationResult.failedTools.length} tools: ${registrationResult.failedTools.join(', ')}` + ); + } this.initialized = true; diff --git a/mcp-server/src/tools/index.js b/mcp-server/src/tools/index.js index 3d0f87f0..d8b165a1 100644 --- a/mcp-server/src/tools/index.js +++ b/mcp-server/src/tools/index.js @@ -3,109 +3,238 @@ * Export all Task Master CLI tools for MCP server */ -import { registerListTasksTool } from './get-tasks.js'; import logger from '../logger.js'; -import { registerSetTaskStatusTool } from './set-task-status.js'; -import { registerParsePRDTool } from './parse-prd.js'; -import { registerUpdateTool } from './update.js'; -import { registerUpdateTaskTool } from './update-task.js'; -import { registerUpdateSubtaskTool } from './update-subtask.js'; -import { registerGenerateTool } from './generate.js'; -import { registerShowTaskTool } from './get-task.js'; -import { registerNextTaskTool } from './next-task.js'; -import { registerExpandTaskTool } from './expand-task.js'; -import { registerAddTaskTool } from './add-task.js'; -import { registerAddSubtaskTool } from './add-subtask.js'; -import { registerRemoveSubtaskTool } from './remove-subtask.js'; -import { registerAnalyzeProjectComplexityTool } from './analyze.js'; -import { registerClearSubtasksTool } from './clear-subtasks.js'; -import { registerExpandAllTool } from './expand-all.js'; -import { registerRemoveDependencyTool } from './remove-dependency.js'; -import { registerValidateDependenciesTool } from './validate-dependencies.js'; -import { registerFixDependenciesTool } from './fix-dependencies.js'; -import { registerComplexityReportTool } from './complexity-report.js'; -import { registerAddDependencyTool } from './add-dependency.js'; -import { registerRemoveTaskTool } from './remove-task.js'; -import { registerInitializeProjectTool } from './initialize-project.js'; -import { registerModelsTool } from './models.js'; -import { registerMoveTaskTool } from './move-task.js'; -import { registerResponseLanguageTool } from './response-language.js'; -import { registerAddTagTool } from './add-tag.js'; -import { registerDeleteTagTool } from './delete-tag.js'; -import { registerListTagsTool } from './list-tags.js'; -import { registerUseTagTool } from './use-tag.js'; -import { registerRenameTagTool } from './rename-tag.js'; -import { registerCopyTagTool } from './copy-tag.js'; -import { registerResearchTool } from './research.js'; -import { registerRulesTool } from './rules.js'; -import { registerScopeUpTool } from './scope-up.js'; -import { registerScopeDownTool } from './scope-down.js'; +import { + toolRegistry, + coreTools, + standardTools, + getAvailableTools, + getToolRegistration, + isValidTool +} from './tool-registry.js'; /** - * Register all Task Master tools with the MCP server - * @param {Object} server - FastMCP server instance + * Helper function to safely read and normalize the TASK_MASTER_TOOLS environment variable + * @returns {string} The tools configuration string, defaults to 'all' */ -export function registerTaskMasterTools(server) { +export function getToolsConfiguration() { + const rawValue = process.env.TASK_MASTER_TOOLS; + + if (!rawValue || rawValue.trim() === '') { + logger.debug('No TASK_MASTER_TOOLS env var found, defaulting to "all"'); + return 'all'; + } + + const normalizedValue = rawValue.trim(); + logger.debug(`TASK_MASTER_TOOLS env var: "${normalizedValue}"`); + return normalizedValue; +} + +/** + * Register Task Master tools with the MCP server + * Supports selective tool loading via TASK_MASTER_TOOLS environment variable + * @param {Object} server - FastMCP server instance + * @param {string} toolMode - The tool mode configuration (defaults to 'all') + * @returns {Object} Object containing registered tools, failed tools, and normalized mode + */ +export function registerTaskMasterTools(server, toolMode = 'all') { + const registeredTools = []; + const failedTools = []; + try { - // Register each tool in a logical workflow order + const enabledTools = toolMode.trim(); + let toolsToRegister = []; - // Group 1: Initialization & Setup - registerInitializeProjectTool(server); - registerModelsTool(server); - registerRulesTool(server); - registerParsePRDTool(server); + const lowerCaseConfig = enabledTools.toLowerCase(); - // Group 2: Task Analysis & Expansion - registerAnalyzeProjectComplexityTool(server); - registerExpandTaskTool(server); - registerExpandAllTool(server); - registerScopeUpTool(server); - registerScopeDownTool(server); + switch (lowerCaseConfig) { + case 'all': + toolsToRegister = Object.keys(toolRegistry); + logger.info('Loading all available tools'); + break; + case 'core': + case 'lean': + toolsToRegister = coreTools; + logger.info('Loading core tools only'); + break; + case 'standard': + toolsToRegister = standardTools; + logger.info('Loading standard tools'); + break; + default: + const requestedTools = enabledTools + .split(',') + .map((t) => t.trim()) + .filter((t) => t.length > 0); - // Group 3: Task Listing & Viewing - registerListTasksTool(server); - registerShowTaskTool(server); - registerNextTaskTool(server); - registerComplexityReportTool(server); + const uniqueTools = new Set(); + const unknownTools = []; - // Group 4: Task Status & Management - registerSetTaskStatusTool(server); - registerGenerateTool(server); + const aliasMap = { + response_language: 'response-language' + }; - // Group 5: Task Creation & Modification - registerAddTaskTool(server); - registerAddSubtaskTool(server); - registerUpdateTool(server); - registerUpdateTaskTool(server); - registerUpdateSubtaskTool(server); - registerRemoveTaskTool(server); - registerRemoveSubtaskTool(server); - registerClearSubtasksTool(server); - registerMoveTaskTool(server); + for (const toolName of requestedTools) { + let resolvedName = null; + const lowerToolName = toolName.toLowerCase(); - // Group 6: Dependency Management - registerAddDependencyTool(server); - registerRemoveDependencyTool(server); - registerValidateDependenciesTool(server); - registerFixDependenciesTool(server); - registerResponseLanguageTool(server); + if (aliasMap[lowerToolName]) { + const aliasTarget = aliasMap[lowerToolName]; + for (const registryKey of Object.keys(toolRegistry)) { + if (registryKey.toLowerCase() === aliasTarget.toLowerCase()) { + resolvedName = registryKey; + break; + } + } + } - // Group 7: Tag Management - registerListTagsTool(server); - registerAddTagTool(server); - registerDeleteTagTool(server); - registerUseTagTool(server); - registerRenameTagTool(server); - registerCopyTagTool(server); + if (!resolvedName) { + for (const registryKey of Object.keys(toolRegistry)) { + if (registryKey.toLowerCase() === lowerToolName) { + resolvedName = registryKey; + break; + } + } + } - // Group 8: Research Features - registerResearchTool(server); + if (!resolvedName) { + const withHyphens = lowerToolName.replace(/_/g, '-'); + for (const registryKey of Object.keys(toolRegistry)) { + if (registryKey.toLowerCase() === withHyphens) { + resolvedName = registryKey; + break; + } + } + } + + if (!resolvedName) { + const withUnderscores = lowerToolName.replace(/-/g, '_'); + for (const registryKey of Object.keys(toolRegistry)) { + if (registryKey.toLowerCase() === withUnderscores) { + resolvedName = registryKey; + break; + } + } + } + + if (resolvedName) { + uniqueTools.add(resolvedName); + logger.debug(`Resolved tool "${toolName}" to "${resolvedName}"`); + } else { + unknownTools.push(toolName); + logger.warn(`Unknown tool specified: "${toolName}"`); + } + } + + toolsToRegister = Array.from(uniqueTools); + + if (unknownTools.length > 0) { + logger.warn(`Unknown tools: ${unknownTools.join(', ')}`); + } + + if (toolsToRegister.length === 0) { + logger.warn( + `No valid tools found in custom list. Loading all tools as fallback.` + ); + toolsToRegister = Object.keys(toolRegistry); + } else { + logger.info( + `Loading ${toolsToRegister.length} custom tools from list (${uniqueTools.size} unique after normalization)` + ); + } + break; + } + + logger.info( + `Registering ${toolsToRegister.length} MCP tools (mode: ${enabledTools})` + ); + + toolsToRegister.forEach((toolName) => { + try { + const registerFunction = getToolRegistration(toolName); + if (registerFunction) { + registerFunction(server); + logger.debug(`Registered tool: ${toolName}`); + registeredTools.push(toolName); + } else { + logger.warn(`Tool ${toolName} not found in registry`); + failedTools.push(toolName); + } + } catch (error) { + if (error.message && error.message.includes('already registered')) { + logger.debug(`Tool ${toolName} already registered, skipping`); + registeredTools.push(toolName); + } else { + logger.error(`Failed to register tool ${toolName}: ${error.message}`); + failedTools.push(toolName); + } + } + }); + + logger.info( + `Successfully registered ${registeredTools.length}/${toolsToRegister.length} tools` + ); + if (failedTools.length > 0) { + logger.warn(`Failed tools: ${failedTools.join(', ')}`); + } + + return { + registeredTools, + failedTools, + normalizedMode: lowerCaseConfig + }; } catch (error) { - logger.error(`Error registering Task Master tools: ${error.message}`); - throw error; + logger.error( + `Error parsing TASK_MASTER_TOOLS environment variable: ${error.message}` + ); + logger.info('Falling back to loading all tools'); + + const fallbackTools = Object.keys(toolRegistry); + for (const toolName of fallbackTools) { + const registerFunction = getToolRegistration(toolName); + if (registerFunction) { + try { + registerFunction(server); + registeredTools.push(toolName); + } catch (err) { + if (err.message && err.message.includes('already registered')) { + logger.debug( + `Fallback tool ${toolName} already registered, skipping` + ); + registeredTools.push(toolName); + } else { + logger.warn( + `Failed to register fallback tool '${toolName}': ${err.message}` + ); + failedTools.push(toolName); + } + } + } else { + logger.warn(`Tool '${toolName}' not found in registry`); + failedTools.push(toolName); + } + } + logger.info( + `Successfully registered ${registeredTools.length} fallback tools` + ); + + return { + registeredTools, + failedTools, + normalizedMode: 'all' + }; } } +export { + toolRegistry, + coreTools, + standardTools, + getAvailableTools, + getToolRegistration, + isValidTool +}; + export default { registerTaskMasterTools }; diff --git a/mcp-server/src/tools/tool-registry.js b/mcp-server/src/tools/tool-registry.js new file mode 100644 index 00000000..bf8456eb --- /dev/null +++ b/mcp-server/src/tools/tool-registry.js @@ -0,0 +1,168 @@ +/** + * tool-registry.js + * Tool Registry Object Structure - Maps all 36 tool names to registration functions + */ + +import { registerListTasksTool } from './get-tasks.js'; +import { registerSetTaskStatusTool } from './set-task-status.js'; +import { registerParsePRDTool } from './parse-prd.js'; +import { registerUpdateTool } from './update.js'; +import { registerUpdateTaskTool } from './update-task.js'; +import { registerUpdateSubtaskTool } from './update-subtask.js'; +import { registerGenerateTool } from './generate.js'; +import { registerShowTaskTool } from './get-task.js'; +import { registerNextTaskTool } from './next-task.js'; +import { registerExpandTaskTool } from './expand-task.js'; +import { registerAddTaskTool } from './add-task.js'; +import { registerAddSubtaskTool } from './add-subtask.js'; +import { registerRemoveSubtaskTool } from './remove-subtask.js'; +import { registerAnalyzeProjectComplexityTool } from './analyze.js'; +import { registerClearSubtasksTool } from './clear-subtasks.js'; +import { registerExpandAllTool } from './expand-all.js'; +import { registerRemoveDependencyTool } from './remove-dependency.js'; +import { registerValidateDependenciesTool } from './validate-dependencies.js'; +import { registerFixDependenciesTool } from './fix-dependencies.js'; +import { registerComplexityReportTool } from './complexity-report.js'; +import { registerAddDependencyTool } from './add-dependency.js'; +import { registerRemoveTaskTool } from './remove-task.js'; +import { registerInitializeProjectTool } from './initialize-project.js'; +import { registerModelsTool } from './models.js'; +import { registerMoveTaskTool } from './move-task.js'; +import { registerResponseLanguageTool } from './response-language.js'; +import { registerAddTagTool } from './add-tag.js'; +import { registerDeleteTagTool } from './delete-tag.js'; +import { registerListTagsTool } from './list-tags.js'; +import { registerUseTagTool } from './use-tag.js'; +import { registerRenameTagTool } from './rename-tag.js'; +import { registerCopyTagTool } from './copy-tag.js'; +import { registerResearchTool } from './research.js'; +import { registerRulesTool } from './rules.js'; +import { registerScopeUpTool } from './scope-up.js'; +import { registerScopeDownTool } from './scope-down.js'; + +/** + * Comprehensive tool registry mapping all 36 tool names to their registration functions + * Used for dynamic tool registration and validation + */ +export const toolRegistry = { + initialize_project: registerInitializeProjectTool, + models: registerModelsTool, + rules: registerRulesTool, + parse_prd: registerParsePRDTool, + 'response-language': registerResponseLanguageTool, + analyze_project_complexity: registerAnalyzeProjectComplexityTool, + expand_task: registerExpandTaskTool, + expand_all: registerExpandAllTool, + scope_up_task: registerScopeUpTool, + scope_down_task: registerScopeDownTool, + get_tasks: registerListTasksTool, + get_task: registerShowTaskTool, + next_task: registerNextTaskTool, + complexity_report: registerComplexityReportTool, + set_task_status: registerSetTaskStatusTool, + generate: registerGenerateTool, + add_task: registerAddTaskTool, + add_subtask: registerAddSubtaskTool, + update: registerUpdateTool, + update_task: registerUpdateTaskTool, + update_subtask: registerUpdateSubtaskTool, + remove_task: registerRemoveTaskTool, + remove_subtask: registerRemoveSubtaskTool, + clear_subtasks: registerClearSubtasksTool, + move_task: registerMoveTaskTool, + add_dependency: registerAddDependencyTool, + remove_dependency: registerRemoveDependencyTool, + validate_dependencies: registerValidateDependenciesTool, + fix_dependencies: registerFixDependenciesTool, + list_tags: registerListTagsTool, + add_tag: registerAddTagTool, + delete_tag: registerDeleteTagTool, + use_tag: registerUseTagTool, + rename_tag: registerRenameTagTool, + copy_tag: registerCopyTagTool, + research: registerResearchTool +}; + +/** + * Core tools array containing the 7 essential tools for daily development + * These represent the minimal set needed for basic task management operations + */ +export const coreTools = [ + 'get_tasks', + 'next_task', + 'get_task', + 'set_task_status', + 'update_subtask', + 'parse_prd', + 'expand_task' +]; + +/** + * Standard tools array containing the 15 most commonly used tools + * Includes all core tools plus frequently used additional tools + */ +export const standardTools = [ + ...coreTools, + 'initialize_project', + 'analyze_project_complexity', + 'expand_all', + 'add_subtask', + 'remove_task', + 'generate', + 'add_task', + 'complexity_report' +]; + +/** + * Get all available tool names + * @returns {string[]} Array of tool names + */ +export function getAvailableTools() { + return Object.keys(toolRegistry); +} + +/** + * Get tool counts for all categories + * @returns {Object} Object with core, standard, and total counts + */ +export function getToolCounts() { + return { + core: coreTools.length, + standard: standardTools.length, + total: Object.keys(toolRegistry).length + }; +} + +/** + * Get tool arrays organized by category + * @returns {Object} Object with arrays for each category + */ +export function getToolCategories() { + const allTools = Object.keys(toolRegistry); + return { + core: [...coreTools], + standard: [...standardTools], + all: [...allTools], + extended: allTools.filter((t) => !standardTools.includes(t)) + }; +} + +/** + * Get registration function for a specific tool + * @param {string} toolName - Name of the tool + * @returns {Function|null} Registration function or null if not found + */ +export function getToolRegistration(toolName) { + return toolRegistry[toolName] || null; +} + +/** + * Validate if a tool exists in the registry + * @param {string} toolName - Name of the tool + * @returns {boolean} True if tool exists + */ +export function isValidTool(toolName) { + return toolName in toolRegistry; +} + +export default toolRegistry; diff --git a/tests/helpers/tool-counts.js b/tests/helpers/tool-counts.js new file mode 100644 index 00000000..95cbdc6f --- /dev/null +++ b/tests/helpers/tool-counts.js @@ -0,0 +1,123 @@ +/** + * tool-counts.js + * Shared helper for validating tool counts across tests and validation scripts + */ + +import { + getToolCounts, + getToolCategories +} from '../../mcp-server/src/tools/tool-registry.js'; + +/** + * Expected tool counts - update these when tools are added/removed + * These serve as the canonical source of truth for expected counts + */ +export const EXPECTED_TOOL_COUNTS = { + core: 7, + standard: 15, + total: 36 +}; + +/** + * Expected core tools list for validation + */ +export const EXPECTED_CORE_TOOLS = [ + 'get_tasks', + 'next_task', + 'get_task', + 'set_task_status', + 'update_subtask', + 'parse_prd', + 'expand_task' +]; + +/** + * Validate that actual tool counts match expected counts + * @returns {Object} Validation result with isValid flag and details + */ +export function validateToolCounts() { + const actual = getToolCounts(); + const expected = EXPECTED_TOOL_COUNTS; + + const isValid = + actual.core === expected.core && + actual.standard === expected.standard && + actual.total === expected.total; + + return { + isValid, + actual, + expected, + differences: { + core: actual.core - expected.core, + standard: actual.standard - expected.standard, + total: actual.total - expected.total + } + }; +} + +/** + * Validate that tool categories have correct structure and content + * @returns {Object} Validation result + */ +export function validateToolStructure() { + const categories = getToolCategories(); + const counts = getToolCounts(); + + // Check that core tools are subset of standard tools + const coreInStandard = categories.core.every((tool) => + categories.standard.includes(tool) + ); + + // Check that standard tools are subset of all tools + const standardInAll = categories.standard.every((tool) => + categories.all.includes(tool) + ); + + // Check that expected core tools match actual + const expectedCoreMatch = + EXPECTED_CORE_TOOLS.every((tool) => categories.core.includes(tool)) && + categories.core.every((tool) => EXPECTED_CORE_TOOLS.includes(tool)); + + // Check array lengths match counts + const lengthsMatch = + categories.core.length === counts.core && + categories.standard.length === counts.standard && + categories.all.length === counts.total; + + return { + isValid: + coreInStandard && standardInAll && expectedCoreMatch && lengthsMatch, + details: { + coreInStandard, + standardInAll, + expectedCoreMatch, + lengthsMatch + }, + categories, + counts + }; +} + +/** + * Get a detailed report of all tool information + * @returns {Object} Comprehensive tool information + */ +export function getToolReport() { + const counts = getToolCounts(); + const categories = getToolCategories(); + const validation = validateToolCounts(); + const structure = validateToolStructure(); + + return { + counts, + categories, + validation, + structure, + summary: { + totalValid: validation.isValid && structure.isValid, + countsValid: validation.isValid, + structureValid: structure.isValid + } + }; +} diff --git a/tests/unit/mcp/tools/tool-registration.test.js b/tests/unit/mcp/tools/tool-registration.test.js new file mode 100644 index 00000000..23f85c8c --- /dev/null +++ b/tests/unit/mcp/tools/tool-registration.test.js @@ -0,0 +1,410 @@ +/** + * tool-registration.test.js + * Comprehensive unit tests for the Task Master MCP tool registration system + * Tests environment variable control system covering all configuration modes and edge cases + */ + +import { + describe, + it, + expect, + beforeEach, + afterEach, + jest +} from '@jest/globals'; + +import { + EXPECTED_TOOL_COUNTS, + EXPECTED_CORE_TOOLS, + validateToolCounts, + validateToolStructure +} from '../../../helpers/tool-counts.js'; + +import { registerTaskMasterTools } from '../../../../mcp-server/src/tools/index.js'; +import { + toolRegistry, + coreTools, + standardTools +} from '../../../../mcp-server/src/tools/tool-registry.js'; + +// Derive constants from imported registry to avoid brittle magic numbers +const ALL_COUNT = Object.keys(toolRegistry).length; +const CORE_COUNT = coreTools.length; +const STANDARD_COUNT = standardTools.length; + +describe('Task Master Tool Registration System', () => { + let mockServer; + let originalEnv; + + beforeEach(() => { + originalEnv = process.env.TASK_MASTER_TOOLS; + + mockServer = { + tools: [], + addTool: jest.fn((tool) => { + mockServer.tools.push(tool); + return tool; + }) + }; + + delete process.env.TASK_MASTER_TOOLS; + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.TASK_MASTER_TOOLS = originalEnv; + } else { + delete process.env.TASK_MASTER_TOOLS; + } + + jest.clearAllMocks(); + }); + + describe('Test Environment Setup', () => { + it('should have properly configured mock server', () => { + expect(mockServer).toBeDefined(); + expect(typeof mockServer.addTool).toBe('function'); + expect(Array.isArray(mockServer.tools)).toBe(true); + expect(mockServer.tools.length).toBe(0); + }); + + it('should have correct tool registry structure', () => { + const validation = validateToolCounts(); + expect(validation.isValid).toBe(true); + + if (!validation.isValid) { + console.error('Tool count validation failed:', validation); + } + + expect(validation.actual.total).toBe(EXPECTED_TOOL_COUNTS.total); + expect(validation.actual.core).toBe(EXPECTED_TOOL_COUNTS.core); + expect(validation.actual.standard).toBe(EXPECTED_TOOL_COUNTS.standard); + }); + + it('should have correct core tools', () => { + const structure = validateToolStructure(); + expect(structure.isValid).toBe(true); + + if (!structure.isValid) { + console.error('Tool structure validation failed:', structure); + } + + expect(coreTools).toEqual(expect.arrayContaining(EXPECTED_CORE_TOOLS)); + expect(coreTools.length).toBe(EXPECTED_TOOL_COUNTS.core); + }); + + it('should have correct standard tools that include all core tools', () => { + const structure = validateToolStructure(); + expect(structure.details.coreInStandard).toBe(true); + expect(standardTools.length).toBe(EXPECTED_TOOL_COUNTS.standard); + + coreTools.forEach((tool) => { + expect(standardTools).toContain(tool); + }); + }); + + it('should have all expected tools in registry', () => { + const expectedTools = [ + 'initialize_project', + 'models', + 'research', + 'add_tag', + 'delete_tag', + 'get_tasks', + 'next_task', + 'get_task' + ]; + expectedTools.forEach((tool) => { + expect(toolRegistry).toHaveProperty(tool); + }); + }); + }); + + describe('Configuration Modes', () => { + it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS is not set (default behavior)`, () => { + delete process.env.TASK_MASTER_TOOLS; + + registerTaskMasterTools(mockServer); + + expect(mockServer.addTool).toHaveBeenCalledTimes( + EXPECTED_TOOL_COUNTS.total + ); + }); + + it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS=all`, () => { + process.env.TASK_MASTER_TOOLS = 'all'; + + registerTaskMasterTools(mockServer); + + expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT); + }); + + it(`should register exactly ${CORE_COUNT} core tools when TASK_MASTER_TOOLS=core`, () => { + process.env.TASK_MASTER_TOOLS = 'core'; + + registerTaskMasterTools(mockServer, 'core'); + + expect(mockServer.addTool).toHaveBeenCalledTimes( + EXPECTED_TOOL_COUNTS.core + ); + }); + + it(`should register exactly ${STANDARD_COUNT} standard tools when TASK_MASTER_TOOLS=standard`, () => { + process.env.TASK_MASTER_TOOLS = 'standard'; + + registerTaskMasterTools(mockServer, 'standard'); + + expect(mockServer.addTool).toHaveBeenCalledTimes( + EXPECTED_TOOL_COUNTS.standard + ); + }); + + it(`should treat lean as alias for core mode (${CORE_COUNT} tools)`, () => { + process.env.TASK_MASTER_TOOLS = 'lean'; + + registerTaskMasterTools(mockServer, 'lean'); + + expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT); + }); + + it('should handle case insensitive configuration values', () => { + process.env.TASK_MASTER_TOOLS = 'CORE'; + + registerTaskMasterTools(mockServer, 'CORE'); + + expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT); + }); + }); + + describe('Custom Tool Selection and Edge Cases', () => { + it('should register specific tools from comma-separated list', () => { + process.env.TASK_MASTER_TOOLS = 'get_tasks,next_task,get_task'; + + registerTaskMasterTools(mockServer, 'get_tasks,next_task,get_task'); + + expect(mockServer.addTool).toHaveBeenCalledTimes(3); + }); + + it('should handle mixed valid and invalid tool names gracefully', () => { + process.env.TASK_MASTER_TOOLS = + 'invalid_tool,get_tasks,fake_tool,next_task'; + + registerTaskMasterTools( + mockServer, + 'invalid_tool,get_tasks,fake_tool,next_task' + ); + + expect(mockServer.addTool).toHaveBeenCalledTimes(2); + }); + + it('should default to all tools with completely invalid input', () => { + process.env.TASK_MASTER_TOOLS = 'completely_invalid'; + + registerTaskMasterTools(mockServer); + + expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT); + }); + + it('should handle empty string environment variable', () => { + process.env.TASK_MASTER_TOOLS = ''; + + registerTaskMasterTools(mockServer); + + expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT); + }); + + it('should handle whitespace in comma-separated lists', () => { + process.env.TASK_MASTER_TOOLS = ' get_tasks , next_task , get_task '; + + registerTaskMasterTools(mockServer, ' get_tasks , next_task , get_task '); + + expect(mockServer.addTool).toHaveBeenCalledTimes(3); + }); + + it('should ignore duplicate tools in list', () => { + process.env.TASK_MASTER_TOOLS = 'get_tasks,get_tasks,next_task,get_tasks'; + + registerTaskMasterTools( + mockServer, + 'get_tasks,get_tasks,next_task,get_tasks' + ); + + expect(mockServer.addTool).toHaveBeenCalledTimes(2); + }); + + it('should handle only commas and empty entries', () => { + process.env.TASK_MASTER_TOOLS = ',,,'; + + registerTaskMasterTools(mockServer); + + expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT); + }); + + it('should handle single tool selection', () => { + process.env.TASK_MASTER_TOOLS = 'get_tasks'; + + registerTaskMasterTools(mockServer, 'get_tasks'); + + expect(mockServer.addTool).toHaveBeenCalledTimes(1); + }); + }); + + describe('Coverage Analysis and Integration Tests', () => { + it('should provide 100% code coverage for environment control logic', () => { + const testCases = [ + { + env: undefined, + expectedCount: ALL_COUNT, + description: 'undefined env (all)' + }, + { + env: '', + expectedCount: ALL_COUNT, + description: 'empty string (all)' + }, + { env: 'all', expectedCount: ALL_COUNT, description: 'all mode' }, + { env: 'core', expectedCount: CORE_COUNT, description: 'core mode' }, + { + env: 'lean', + expectedCount: CORE_COUNT, + description: 'lean mode (alias)' + }, + { + env: 'standard', + expectedCount: STANDARD_COUNT, + description: 'standard mode' + }, + { + env: 'get_tasks,next_task', + expectedCount: 2, + description: 'custom list' + }, + { + env: 'invalid_tool', + expectedCount: ALL_COUNT, + description: 'invalid fallback' + } + ]; + + testCases.forEach((testCase) => { + delete process.env.TASK_MASTER_TOOLS; + if (testCase.env !== undefined) { + process.env.TASK_MASTER_TOOLS = testCase.env; + } + + mockServer.tools = []; + mockServer.addTool.mockClear(); + + registerTaskMasterTools(mockServer, testCase.env || 'all'); + + expect(mockServer.addTool).toHaveBeenCalledTimes( + testCase.expectedCount + ); + }); + }); + + it('should have optimal performance characteristics', () => { + const startTime = Date.now(); + + process.env.TASK_MASTER_TOOLS = 'all'; + + registerTaskMasterTools(mockServer); + + const endTime = Date.now(); + const executionTime = endTime - startTime; + + expect(executionTime).toBeLessThan(100); + expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT); + }); + + it('should validate token reduction claims', () => { + expect(coreTools.length).toBeLessThan(standardTools.length); + expect(standardTools.length).toBeLessThan( + Object.keys(toolRegistry).length + ); + + expect(coreTools.length).toBe(CORE_COUNT); + expect(standardTools.length).toBe(STANDARD_COUNT); + expect(Object.keys(toolRegistry).length).toBe(ALL_COUNT); + + const allToolsCount = Object.keys(toolRegistry).length; + const coreReduction = + ((allToolsCount - coreTools.length) / allToolsCount) * 100; + const standardReduction = + ((allToolsCount - standardTools.length) / allToolsCount) * 100; + + expect(coreReduction).toBeGreaterThan(80); + expect(standardReduction).toBeGreaterThan(50); + }); + + it('should maintain referential integrity of tool registry', () => { + coreTools.forEach((tool) => { + expect(standardTools).toContain(tool); + }); + + standardTools.forEach((tool) => { + expect(toolRegistry).toHaveProperty(tool); + }); + + Object.keys(toolRegistry).forEach((tool) => { + expect(typeof toolRegistry[tool]).toBe('function'); + }); + }); + + it('should handle concurrent registration attempts', () => { + process.env.TASK_MASTER_TOOLS = 'core'; + + registerTaskMasterTools(mockServer, 'core'); + registerTaskMasterTools(mockServer, 'core'); + registerTaskMasterTools(mockServer, 'core'); + + expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT * 3); + }); + + it('should validate all documented tool categories exist', () => { + const allTools = Object.keys(toolRegistry); + + const projectSetupTools = allTools.filter((tool) => + ['initialize_project', 'models', 'rules', 'parse_prd'].includes(tool) + ); + expect(projectSetupTools.length).toBeGreaterThan(0); + + const taskManagementTools = allTools.filter((tool) => + ['get_tasks', 'get_task', 'next_task', 'set_task_status'].includes(tool) + ); + expect(taskManagementTools.length).toBeGreaterThan(0); + + const analysisTools = allTools.filter((tool) => + ['analyze_project_complexity', 'complexity_report'].includes(tool) + ); + expect(analysisTools.length).toBeGreaterThan(0); + + const tagManagementTools = allTools.filter((tool) => + ['add_tag', 'delete_tag', 'list_tags', 'use_tag'].includes(tool) + ); + expect(tagManagementTools.length).toBeGreaterThan(0); + }); + + it('should handle error conditions gracefully', () => { + const problematicInputs = [ + 'null', + 'undefined', + ' ', + '\n\t', + 'special!@#$%^&*()characters', + 'very,very,very,very,very,very,very,long,comma,separated,list,with,invalid,tools,that,should,fallback,to,all' + ]; + + problematicInputs.forEach((input) => { + mockServer.tools = []; + mockServer.addTool.mockClear(); + + process.env.TASK_MASTER_TOOLS = input; + + expect(() => registerTaskMasterTools(mockServer)).not.toThrow(); + + expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT); + }); + }); + }); +});