From 21e74ab8f5f5152877936998ae9f03a2ac379bdc Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:39:20 +0000 Subject: [PATCH] feat(wip): set up mcp server and tools, but mcp on cursor not working despite working in inspector --- .cursor/mcp.json | 8 + mcp-server/server.js | 12 +- mcp-server/src/api-handlers.js | 970 -------------------------- mcp-server/src/auth.js | 285 -------- mcp-server/src/context-manager.js | 873 ----------------------- mcp-server/src/index.js | 314 +-------- mcp-server/src/logger.js | 68 ++ mcp-server/src/tools/addTask.js | 56 ++ mcp-server/src/tools/expandTask.js | 66 ++ mcp-server/src/tools/index.js | 29 + mcp-server/src/tools/listTasks.js | 51 ++ mcp-server/src/tools/nextTask.js | 45 ++ mcp-server/src/tools/setTaskStatus.js | 52 ++ mcp-server/src/tools/showTask.js | 45 ++ mcp-server/src/tools/utils.js | 90 +++ 15 files changed, 529 insertions(+), 2435 deletions(-) create mode 100644 .cursor/mcp.json delete mode 100644 mcp-server/src/api-handlers.js delete mode 100644 mcp-server/src/auth.js delete mode 100644 mcp-server/src/context-manager.js create mode 100644 mcp-server/src/logger.js create mode 100644 mcp-server/src/tools/addTask.js create mode 100644 mcp-server/src/tools/expandTask.js create mode 100644 mcp-server/src/tools/index.js create mode 100644 mcp-server/src/tools/listTasks.js create mode 100644 mcp-server/src/tools/nextTask.js create mode 100644 mcp-server/src/tools/setTaskStatus.js create mode 100644 mcp-server/src/tools/showTask.js create mode 100644 mcp-server/src/tools/utils.js diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..3b7160ae --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "taskMaster": { + "command": "node", + "args": ["mcp-server/server.js"] + } + } +} diff --git a/mcp-server/server.js b/mcp-server/server.js index ed5c3c69..dfca0f55 100755 --- a/mcp-server/server.js +++ b/mcp-server/server.js @@ -2,15 +2,11 @@ import TaskMasterMCPServer from "./src/index.js"; import dotenv from "dotenv"; -import { logger } from "../scripts/modules/utils.js"; +import logger from "./src/logger.js"; // Load environment variables dotenv.config(); -// Constants -const PORT = process.env.MCP_SERVER_PORT || 3000; -const HOST = process.env.MCP_SERVER_HOST || "localhost"; - /** * Start the MCP server */ @@ -19,21 +15,17 @@ async function startServer() { // Handle graceful shutdown process.on("SIGINT", async () => { - logger.info("Received SIGINT, shutting down gracefully..."); await server.stop(); process.exit(0); }); process.on("SIGTERM", async () => { - logger.info("Received SIGTERM, shutting down gracefully..."); await server.stop(); process.exit(0); }); try { - await server.start({ port: PORT, host: HOST }); - logger.info(`MCP server running at http://${HOST}:${PORT}`); - logger.info("Press Ctrl+C to stop"); + await server.start(); } catch (error) { logger.error(`Failed to start MCP server: ${error.message}`); process.exit(1); diff --git a/mcp-server/src/api-handlers.js b/mcp-server/src/api-handlers.js deleted file mode 100644 index ead546f2..00000000 --- a/mcp-server/src/api-handlers.js +++ /dev/null @@ -1,970 +0,0 @@ -import { z } from "zod"; -import { logger } from "../../scripts/modules/utils.js"; -import ContextManager from "./context-manager.js"; - -/** - * MCP API Handlers class - * Implements handlers for the MCP API endpoints - */ -class MCPApiHandlers { - constructor(server) { - this.server = server; - this.contextManager = new ContextManager(); - this.logger = logger; - - // Bind methods - this.registerEndpoints = this.registerEndpoints.bind(this); - this.setupContextHandlers = this.setupContextHandlers.bind(this); - this.setupModelHandlers = this.setupModelHandlers.bind(this); - this.setupExecuteHandlers = this.setupExecuteHandlers.bind(this); - - // Register all handlers - this.registerEndpoints(); - } - - /** - * Register all MCP API endpoints - */ - registerEndpoints() { - this.setupContextHandlers(); - this.setupModelHandlers(); - this.setupExecuteHandlers(); - - this.logger.info("Registered all MCP API endpoint handlers"); - } - - /** - * Set up handlers for the /context endpoint - */ - setupContextHandlers() { - // Add a tool to create context - this.server.addTool({ - name: "createContext", - description: - "Create a new context with the given data and optional metadata", - parameters: z.object({ - contextId: z.string().describe("Unique identifier for the context"), - data: z.any().describe("The context data to store"), - metadata: z - .object({}) - .optional() - .describe("Optional metadata for the context"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.createContext( - args.contextId, - args.data, - args.metadata || {} - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error creating context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to get context - this.server.addTool({ - name: "getContext", - description: - "Retrieve a context by its ID, optionally a specific version", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to retrieve"), - versionId: z - .string() - .optional() - .describe("Optional specific version ID to retrieve"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.getContext( - args.contextId, - args.versionId - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error retrieving context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to update context - this.server.addTool({ - name: "updateContext", - description: "Update an existing context with new data and/or metadata", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to update"), - data: z - .any() - .optional() - .describe("New data to update the context with"), - metadata: z - .object({}) - .optional() - .describe("New metadata to update the context with"), - createNewVersion: z - .boolean() - .optional() - .default(true) - .describe( - "Whether to create a new version (true) or update in place (false)" - ), - }), - execute: async (args) => { - try { - const context = await this.contextManager.updateContext( - args.contextId, - args.data || {}, - args.metadata || {}, - args.createNewVersion - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error updating context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to delete context - this.server.addTool({ - name: "deleteContext", - description: "Delete a context by its ID", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to delete"), - }), - execute: async (args) => { - try { - const result = await this.contextManager.deleteContext( - args.contextId - ); - return { success: result }; - } catch (error) { - this.logger.error(`Error deleting context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to list contexts with pagination and advanced filtering - this.server.addTool({ - name: "listContexts", - description: - "List available contexts with filtering, pagination and sorting", - parameters: z.object({ - // Filtering parameters - filters: z - .object({ - tag: z.string().optional().describe("Filter contexts by tag"), - metadataKey: z - .string() - .optional() - .describe("Filter contexts by metadata key"), - metadataValue: z - .string() - .optional() - .describe("Filter contexts by metadata value"), - createdAfter: z - .string() - .optional() - .describe("Filter contexts created after date (ISO format)"), - updatedAfter: z - .string() - .optional() - .describe("Filter contexts updated after date (ISO format)"), - }) - .optional() - .describe("Filters to apply to the context list"), - - // Pagination parameters - limit: z - .number() - .optional() - .default(100) - .describe("Maximum number of contexts to return"), - offset: z - .number() - .optional() - .default(0) - .describe("Number of contexts to skip"), - - // Sorting parameters - sortBy: z - .string() - .optional() - .default("updated") - .describe("Field to sort by (id, created, updated, size)"), - sortDirection: z - .enum(["asc", "desc"]) - .optional() - .default("desc") - .describe("Sort direction"), - - // Search query - query: z.string().optional().describe("Free text search query"), - }), - execute: async (args) => { - try { - const result = await this.contextManager.listContexts(args); - return { - success: true, - ...result, - }; - } catch (error) { - this.logger.error(`Error listing contexts: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to get context history - this.server.addTool({ - name: "getContextHistory", - description: "Get the version history of a context", - parameters: z.object({ - contextId: z - .string() - .describe("The ID of the context to get history for"), - }), - execute: async (args) => { - try { - const history = await this.contextManager.getContextHistory( - args.contextId - ); - return { - success: true, - history, - contextId: args.contextId, - }; - } catch (error) { - this.logger.error(`Error getting context history: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to merge contexts - this.server.addTool({ - name: "mergeContexts", - description: "Merge multiple contexts into a new context", - parameters: z.object({ - contextIds: z - .array(z.string()) - .describe("Array of context IDs to merge"), - newContextId: z.string().describe("ID for the new merged context"), - metadata: z - .object({}) - .optional() - .describe("Optional metadata for the new context"), - }), - execute: async (args) => { - try { - const mergedContext = await this.contextManager.mergeContexts( - args.contextIds, - args.newContextId, - args.metadata || {} - ); - return { - success: true, - context: mergedContext, - }; - } catch (error) { - this.logger.error(`Error merging contexts: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to add tags to a context - this.server.addTool({ - name: "addTags", - description: "Add tags to a context", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to tag"), - tags: z - .array(z.string()) - .describe("Array of tags to add to the context"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.addTags( - args.contextId, - args.tags - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error adding tags to context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to remove tags from a context - this.server.addTool({ - name: "removeTags", - description: "Remove tags from a context", - parameters: z.object({ - contextId: z - .string() - .describe("The ID of the context to remove tags from"), - tags: z - .array(z.string()) - .describe("Array of tags to remove from the context"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.removeTags( - args.contextId, - args.tags - ); - return { success: true, context }; - } catch (error) { - this.logger.error( - `Error removing tags from context: ${error.message}` - ); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to truncate context - this.server.addTool({ - name: "truncateContext", - description: "Truncate a context to a maximum size", - parameters: z.object({ - contextId: z.string().describe("The ID of the context to truncate"), - maxSize: z - .number() - .describe("Maximum size (in characters) for the context"), - strategy: z - .enum(["start", "end", "middle"]) - .default("end") - .describe("Truncation strategy: start, end, or middle"), - }), - execute: async (args) => { - try { - const context = await this.contextManager.truncateContext( - args.contextId, - args.maxSize, - args.strategy - ); - return { success: true, context }; - } catch (error) { - this.logger.error(`Error truncating context: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - this.logger.info("Registered context endpoint handlers"); - } - - /** - * Set up handlers for the /models endpoint - */ - setupModelHandlers() { - // Add a tool to list available models - this.server.addTool({ - name: "listModels", - description: "List all available models with their capabilities", - parameters: z.object({}), - execute: async () => { - // Here we could get models from a more dynamic source - // For now, returning static list of models supported by Task Master - const models = [ - { - id: "claude-3-opus-20240229", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-100k", - ], - }, - { - id: "claude-3-7-sonnet-20250219", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-200k", - ], - }, - { - id: "sonar-medium-online", - provider: "perplexity", - capabilities: ["text-generation", "web-search", "research"], - }, - ]; - - return { success: true, models }; - }, - }); - - // Add a tool to get model details - this.server.addTool({ - name: "getModelDetails", - description: "Get detailed information about a specific model", - parameters: z.object({ - modelId: z.string().describe("The ID of the model to get details for"), - }), - execute: async (args) => { - // Here we could get model details from a more dynamic source - // For now, returning static information - const modelsMap = { - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-100k", - ], - maxTokens: 100000, - temperature: { min: 0, max: 1, default: 0.7 }, - pricing: { input: 0.000015, output: 0.000075 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - provider: "anthropic", - capabilities: [ - "text-generation", - "embeddings", - "context-window-200k", - ], - maxTokens: 200000, - temperature: { min: 0, max: 1, default: 0.7 }, - pricing: { input: 0.000003, output: 0.000015 }, - }, - "sonar-medium-online": { - id: "sonar-medium-online", - provider: "perplexity", - capabilities: ["text-generation", "web-search", "research"], - maxTokens: 4096, - temperature: { min: 0, max: 1, default: 0.7 }, - }, - }; - - const model = modelsMap[args.modelId]; - if (!model) { - return { - success: false, - error: `Model with ID ${args.modelId} not found`, - }; - } - - return { success: true, model }; - }, - }); - - this.logger.info("Registered models endpoint handlers"); - } - - /** - * Set up handlers for the /execute endpoint - */ - setupExecuteHandlers() { - // Add a tool to execute operations with context - this.server.addTool({ - name: "executeWithContext", - description: "Execute an operation with the provided context", - parameters: z.object({ - operation: z.string().describe("The operation to execute"), - contextId: z.string().describe("The ID of the context to use"), - parameters: z - .record(z.any()) - .optional() - .describe("Additional parameters for the operation"), - versionId: z - .string() - .optional() - .describe("Optional specific context version to use"), - }), - execute: async (args) => { - try { - // Get the context first, with version if specified - const context = await this.contextManager.getContext( - args.contextId, - args.versionId - ); - - // Execute different operations based on the operation name - switch (args.operation) { - case "generateTask": - return await this.executeGenerateTask(context, args.parameters); - case "expandTask": - return await this.executeExpandTask(context, args.parameters); - case "analyzeComplexity": - return await this.executeAnalyzeComplexity( - context, - args.parameters - ); - case "mergeContexts": - return await this.executeMergeContexts(context, args.parameters); - case "searchContexts": - return await this.executeSearchContexts(args.parameters); - case "extractInsights": - return await this.executeExtractInsights( - context, - args.parameters - ); - case "syncWithRepository": - return await this.executeSyncWithRepository( - context, - args.parameters - ); - default: - return { - success: false, - error: `Unknown operation: ${args.operation}`, - }; - } - } catch (error) { - this.logger.error(`Error executing operation: ${error.message}`); - return { - success: false, - error: error.message, - operation: args.operation, - contextId: args.contextId, - }; - } - }, - }); - - // Add tool for batch operations - this.server.addTool({ - name: "executeBatchOperations", - description: "Execute multiple operations in a single request", - parameters: z.object({ - operations: z - .array( - z.object({ - operation: z.string().describe("The operation to execute"), - contextId: z.string().describe("The ID of the context to use"), - parameters: z - .record(z.any()) - .optional() - .describe("Additional parameters"), - versionId: z - .string() - .optional() - .describe("Optional context version"), - }) - ) - .describe("Array of operations to execute in sequence"), - }), - execute: async (args) => { - const results = []; - let hasErrors = false; - - for (const op of args.operations) { - try { - const context = await this.contextManager.getContext( - op.contextId, - op.versionId - ); - - let result; - switch (op.operation) { - case "generateTask": - result = await this.executeGenerateTask(context, op.parameters); - break; - case "expandTask": - result = await this.executeExpandTask(context, op.parameters); - break; - case "analyzeComplexity": - result = await this.executeAnalyzeComplexity( - context, - op.parameters - ); - break; - case "mergeContexts": - result = await this.executeMergeContexts( - context, - op.parameters - ); - break; - case "searchContexts": - result = await this.executeSearchContexts(op.parameters); - break; - case "extractInsights": - result = await this.executeExtractInsights( - context, - op.parameters - ); - break; - case "syncWithRepository": - result = await this.executeSyncWithRepository( - context, - op.parameters - ); - break; - default: - result = { - success: false, - error: `Unknown operation: ${op.operation}`, - }; - hasErrors = true; - } - - results.push({ - operation: op.operation, - contextId: op.contextId, - result: result, - }); - - if (!result.success) { - hasErrors = true; - } - } catch (error) { - this.logger.error( - `Error in batch operation ${op.operation}: ${error.message}` - ); - results.push({ - operation: op.operation, - contextId: op.contextId, - result: { - success: false, - error: error.message, - }, - }); - hasErrors = true; - } - } - - return { - success: !hasErrors, - results: results, - }; - }, - }); - - this.logger.info("Registered execute endpoint handlers"); - } - - /** - * Execute the generateTask operation - * @param {object} context - The context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeGenerateTask(context, parameters = {}) { - // This is a placeholder for actual task generation logic - // In a real implementation, this would use Task Master's task generation - - this.logger.info(`Generating task with context ${context.id}`); - - // Improved task generation with more detailed result - const task = { - id: Math.floor(Math.random() * 1000), - title: parameters.title || "New Task", - description: parameters.description || "Task generated from context", - status: "pending", - dependencies: parameters.dependencies || [], - priority: parameters.priority || "medium", - details: `This task was generated using context ${ - context.id - }.\n\n${JSON.stringify(context.data, null, 2)}`, - metadata: { - generatedAt: new Date().toISOString(), - generatedFrom: context.id, - contextVersion: context.metadata.version, - generatedBy: parameters.user || "system", - }, - }; - - return { - success: true, - task, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } - - /** - * Execute the expandTask operation - * @param {object} context - The context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeExpandTask(context, parameters = {}) { - // This is a placeholder for actual task expansion logic - // In a real implementation, this would use Task Master's task expansion - - this.logger.info(`Expanding task with context ${context.id}`); - - // Enhanced task expansion with more configurable options - const numSubtasks = parameters.numSubtasks || 3; - const subtaskPrefix = parameters.subtaskPrefix || ""; - const subtasks = []; - - for (let i = 1; i <= numSubtasks; i++) { - subtasks.push({ - id: `${subtaskPrefix}${i}`, - title: parameters.titleTemplate - ? parameters.titleTemplate.replace("{i}", i) - : `Subtask ${i}`, - description: parameters.descriptionTemplate - ? parameters.descriptionTemplate - .replace("{i}", i) - .replace("{taskId}", parameters.taskId || "unknown") - : `Subtask ${i} for ${parameters.taskId || "unknown task"}`, - dependencies: i > 1 ? [i - 1] : [], - status: "pending", - metadata: { - expandedAt: new Date().toISOString(), - expandedFrom: context.id, - contextVersion: context.metadata.version, - expandedBy: parameters.user || "system", - }, - }); - } - - return { - success: true, - taskId: parameters.taskId, - subtasks, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } - - /** - * Execute the analyzeComplexity operation - * @param {object} context - The context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeAnalyzeComplexity(context, parameters = {}) { - // This is a placeholder for actual complexity analysis logic - // In a real implementation, this would use Task Master's complexity analysis - - this.logger.info(`Analyzing complexity with context ${context.id}`); - - // Enhanced complexity analysis with more detailed factors - const complexityScore = Math.floor(Math.random() * 10) + 1; - const recommendedSubtasks = Math.floor(complexityScore / 2) + 1; - - // More detailed analysis with weighted factors - const factors = [ - { - name: "Task scope breadth", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.3, - description: "How broad is the scope of this task", - }, - { - name: "Technical complexity", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.4, - description: "How technically complex is the implementation", - }, - { - name: "External dependencies", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.2, - description: "How many external dependencies does this task have", - }, - { - name: "Risk assessment", - score: Math.floor(Math.random() * 10) + 1, - weight: 0.1, - description: "What is the risk level of this task", - }, - ]; - - return { - success: true, - analysis: { - taskId: parameters.taskId || "unknown", - complexityScore, - recommendedSubtasks, - factors, - recommendedTimeEstimate: `${complexityScore * 2}-${ - complexityScore * 4 - } hours`, - metadata: { - analyzedAt: new Date().toISOString(), - analyzedUsing: context.id, - contextVersion: context.metadata.version, - analyzedBy: parameters.user || "system", - }, - }, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } - - /** - * Execute the mergeContexts operation - * @param {object} primaryContext - The primary context to use - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeMergeContexts(primaryContext, parameters = {}) { - this.logger.info( - `Merging contexts with primary context ${primaryContext.id}` - ); - - if ( - !parameters.contextIds || - !Array.isArray(parameters.contextIds) || - parameters.contextIds.length === 0 - ) { - return { - success: false, - error: "No context IDs provided for merging", - }; - } - - if (!parameters.newContextId) { - return { - success: false, - error: "New context ID is required for the merged context", - }; - } - - try { - // Add the primary context to the list if not already included - if (!parameters.contextIds.includes(primaryContext.id)) { - parameters.contextIds.unshift(primaryContext.id); - } - - const mergedContext = await this.contextManager.mergeContexts( - parameters.contextIds, - parameters.newContextId, - { - mergedAt: new Date().toISOString(), - mergedBy: parameters.user || "system", - mergeStrategy: parameters.strategy || "concatenate", - ...parameters.metadata, - } - ); - - return { - success: true, - mergedContext, - sourceContexts: parameters.contextIds, - }; - } catch (error) { - this.logger.error(`Error merging contexts: ${error.message}`); - return { - success: false, - error: error.message, - }; - } - } - - /** - * Execute the searchContexts operation - * @param {object} parameters - Search parameters - * @returns {Promise} The result of the operation - */ - async executeSearchContexts(parameters = {}) { - this.logger.info( - `Searching contexts with query: ${parameters.query || ""}` - ); - - try { - const searchResults = await this.contextManager.listContexts({ - query: parameters.query || "", - filters: parameters.filters || {}, - limit: parameters.limit || 100, - offset: parameters.offset || 0, - sortBy: parameters.sortBy || "updated", - sortDirection: parameters.sortDirection || "desc", - }); - - return { - success: true, - ...searchResults, - }; - } catch (error) { - this.logger.error(`Error searching contexts: ${error.message}`); - return { - success: false, - error: error.message, - }; - } - } - - /** - * Execute the extractInsights operation - * @param {object} context - The context to analyze - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeExtractInsights(context, parameters = {}) { - this.logger.info(`Extracting insights from context ${context.id}`); - - // Placeholder for actual insight extraction - // In a real implementation, this would perform analysis on the context data - - const insights = [ - { - type: "summary", - content: `Summary of context ${context.id}`, - confidence: 0.85, - }, - { - type: "key_points", - content: ["First key point", "Second key point", "Third key point"], - confidence: 0.78, - }, - { - type: "recommendations", - content: ["First recommendation", "Second recommendation"], - confidence: 0.72, - }, - ]; - - return { - success: true, - insights, - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - metadata: { - extractedAt: new Date().toISOString(), - model: parameters.model || "default", - extractedBy: parameters.user || "system", - }, - }; - } - - /** - * Execute the syncWithRepository operation - * @param {object} context - The context to sync - * @param {object} parameters - Additional parameters - * @returns {Promise} The result of the operation - */ - async executeSyncWithRepository(context, parameters = {}) { - this.logger.info(`Syncing context ${context.id} with repository`); - - // Placeholder for actual repository sync - // In a real implementation, this would sync the context with an external repository - - return { - success: true, - syncStatus: "complete", - syncedTo: parameters.repository || "default", - syncTimestamp: new Date().toISOString(), - contextUsed: { - id: context.id, - version: context.metadata.version, - }, - }; - } -} - -export default MCPApiHandlers; diff --git a/mcp-server/src/auth.js b/mcp-server/src/auth.js deleted file mode 100644 index 22c36973..00000000 --- a/mcp-server/src/auth.js +++ /dev/null @@ -1,285 +0,0 @@ -import jwt from "jsonwebtoken"; -import { logger } from "../../scripts/modules/utils.js"; -import crypto from "crypto"; -import fs from "fs/promises"; -import path from "path"; -import { fileURLToPath } from "url"; - -// Constants -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const API_KEYS_FILE = - process.env.MCP_API_KEYS_FILE || path.join(__dirname, "../api-keys.json"); -const JWT_SECRET = - process.env.MCP_JWT_SECRET || "task-master-mcp-server-secret"; -const JWT_EXPIRATION = process.env.MCP_JWT_EXPIRATION || "24h"; - -/** - * Authentication middleware and utilities for MCP server - */ -class MCPAuth { - constructor() { - this.apiKeys = new Map(); - this.logger = logger; - this.loadApiKeys(); - } - - /** - * Load API keys from disk - */ - async loadApiKeys() { - try { - // Create API keys file if it doesn't exist - try { - await fs.access(API_KEYS_FILE); - } catch (error) { - // File doesn't exist, create it with a default admin key - const defaultApiKey = this.generateApiKey(); - const defaultApiKeys = { - keys: [ - { - id: "admin", - key: defaultApiKey, - role: "admin", - created: new Date().toISOString(), - }, - ], - }; - - await fs.mkdir(path.dirname(API_KEYS_FILE), { recursive: true }); - await fs.writeFile( - API_KEYS_FILE, - JSON.stringify(defaultApiKeys, null, 2), - "utf8" - ); - - this.logger.info( - `Created default API keys file with admin key: ${defaultApiKey}` - ); - } - - // Load API keys - const data = await fs.readFile(API_KEYS_FILE, "utf8"); - const apiKeys = JSON.parse(data); - - apiKeys.keys.forEach((key) => { - this.apiKeys.set(key.key, { - id: key.id, - role: key.role, - created: key.created, - }); - }); - - this.logger.info(`Loaded ${this.apiKeys.size} API keys`); - } catch (error) { - this.logger.error(`Failed to load API keys: ${error.message}`); - throw error; - } - } - - /** - * Save API keys to disk - */ - async saveApiKeys() { - try { - const keys = []; - - this.apiKeys.forEach((value, key) => { - keys.push({ - id: value.id, - key, - role: value.role, - created: value.created, - }); - }); - - await fs.writeFile( - API_KEYS_FILE, - JSON.stringify({ keys }, null, 2), - "utf8" - ); - - this.logger.info(`Saved ${keys.length} API keys`); - } catch (error) { - this.logger.error(`Failed to save API keys: ${error.message}`); - throw error; - } - } - - /** - * Generate a new API key - * @returns {string} The generated API key - */ - generateApiKey() { - return crypto.randomBytes(32).toString("hex"); - } - - /** - * Create a new API key - * @param {string} id - Client identifier - * @param {string} role - Client role (admin, user) - * @returns {string} The generated API key - */ - async createApiKey(id, role = "user") { - const apiKey = this.generateApiKey(); - - this.apiKeys.set(apiKey, { - id, - role, - created: new Date().toISOString(), - }); - - await this.saveApiKeys(); - - this.logger.info(`Created new API key for ${id} with role ${role}`); - return apiKey; - } - - /** - * Revoke an API key - * @param {string} apiKey - The API key to revoke - * @returns {boolean} True if the key was revoked - */ - async revokeApiKey(apiKey) { - if (!this.apiKeys.has(apiKey)) { - return false; - } - - this.apiKeys.delete(apiKey); - await this.saveApiKeys(); - - this.logger.info(`Revoked API key`); - return true; - } - - /** - * Validate an API key - * @param {string} apiKey - The API key to validate - * @returns {object|null} The API key details if valid, null otherwise - */ - validateApiKey(apiKey) { - return this.apiKeys.get(apiKey) || null; - } - - /** - * Generate a JWT token for a client - * @param {string} clientId - Client identifier - * @param {string} role - Client role - * @returns {string} The JWT token - */ - generateToken(clientId, role) { - return jwt.sign({ clientId, role }, JWT_SECRET, { - expiresIn: JWT_EXPIRATION, - }); - } - - /** - * Verify a JWT token - * @param {string} token - The JWT token to verify - * @returns {object|null} The token payload if valid, null otherwise - */ - verifyToken(token) { - try { - return jwt.verify(token, JWT_SECRET); - } catch (error) { - this.logger.error(`Failed to verify token: ${error.message}`); - return null; - } - } - - /** - * Express middleware for API key authentication - * @param {object} req - Express request object - * @param {object} res - Express response object - * @param {function} next - Express next function - */ - authenticateApiKey(req, res, next) { - const apiKey = req.headers["x-api-key"]; - - if (!apiKey) { - return res.status(401).json({ - success: false, - error: "API key is required", - }); - } - - const keyDetails = this.validateApiKey(apiKey); - - if (!keyDetails) { - return res.status(401).json({ - success: false, - error: "Invalid API key", - }); - } - - // Attach client info to request - req.client = { - id: keyDetails.id, - role: keyDetails.role, - }; - - next(); - } - - /** - * Express middleware for JWT authentication - * @param {object} req - Express request object - * @param {object} res - Express response object - * @param {function} next - Express next function - */ - authenticateToken(req, res, next) { - const authHeader = req.headers["authorization"]; - const token = authHeader && authHeader.split(" ")[1]; - - if (!token) { - return res.status(401).json({ - success: false, - error: "Authentication token is required", - }); - } - - const payload = this.verifyToken(token); - - if (!payload) { - return res.status(401).json({ - success: false, - error: "Invalid or expired token", - }); - } - - // Attach client info to request - req.client = { - id: payload.clientId, - role: payload.role, - }; - - next(); - } - - /** - * Express middleware for role-based authorization - * @param {Array} roles - Array of allowed roles - * @returns {function} Express middleware - */ - authorizeRoles(roles) { - return (req, res, next) => { - if (!req.client || !req.client.role) { - return res.status(401).json({ - success: false, - error: "Unauthorized: Authentication required", - }); - } - - if (!roles.includes(req.client.role)) { - return res.status(403).json({ - success: false, - error: "Forbidden: Insufficient permissions", - }); - } - - next(); - }; - } -} - -export default MCPAuth; diff --git a/mcp-server/src/context-manager.js b/mcp-server/src/context-manager.js deleted file mode 100644 index 5b94b538..00000000 --- a/mcp-server/src/context-manager.js +++ /dev/null @@ -1,873 +0,0 @@ -import { logger } from "../../scripts/modules/utils.js"; -import fs from "fs/promises"; -import path from "path"; -import { fileURLToPath } from "url"; -import crypto from "crypto"; -import Fuse from "fuse.js"; - -// Constants -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const CONTEXT_DIR = - process.env.MCP_CONTEXT_DIR || path.join(__dirname, "../contexts"); -const MAX_CONTEXT_HISTORY = parseInt( - process.env.MCP_MAX_CONTEXT_HISTORY || "10", - 10 -); - -/** - * Context Manager for MCP server - * Handles storage, retrieval, and manipulation of context data - * Implements efficient indexing, versioning, and advanced context operations - */ -class ContextManager { - constructor() { - this.contexts = new Map(); - this.contextHistory = new Map(); // For version history - this.contextIndex = null; // For fuzzy search - this.logger = logger; - this.ensureContextDir(); - this.rebuildSearchIndex(); - } - - /** - * Ensure the contexts directory exists - */ - async ensureContextDir() { - try { - await fs.mkdir(CONTEXT_DIR, { recursive: true }); - this.logger.info(`Context directory ensured at ${CONTEXT_DIR}`); - - // Also create a versions subdirectory for history - await fs.mkdir(path.join(CONTEXT_DIR, "versions"), { recursive: true }); - } catch (error) { - this.logger.error(`Failed to create context directory: ${error.message}`); - throw error; - } - } - - /** - * Rebuild the search index for efficient context lookup - */ - async rebuildSearchIndex() { - await this.loadAllContextsFromDisk(); - - const contextsForIndex = Array.from(this.contexts.values()).map((ctx) => ({ - id: ctx.id, - content: - typeof ctx.data === "string" ? ctx.data : JSON.stringify(ctx.data), - tags: ctx.tags.join(" "), - metadata: Object.entries(ctx.metadata) - .map(([k, v]) => `${k}:${v}`) - .join(" "), - })); - - this.contextIndex = new Fuse(contextsForIndex, { - keys: ["id", "content", "tags", "metadata"], - includeScore: true, - threshold: 0.6, - }); - - this.logger.info( - `Rebuilt search index with ${contextsForIndex.length} contexts` - ); - } - - /** - * Create a new context - * @param {string} contextId - Unique identifier for the context - * @param {object|string} contextData - Initial context data - * @param {object} metadata - Optional metadata for the context - * @returns {object} The created context - */ - async createContext(contextId, contextData, metadata = {}) { - if (this.contexts.has(contextId)) { - throw new Error(`Context with ID ${contextId} already exists`); - } - - const timestamp = new Date().toISOString(); - const versionId = this.generateVersionId(); - - const context = { - id: contextId, - data: contextData, - metadata: { - created: timestamp, - updated: timestamp, - version: versionId, - ...metadata, - }, - tags: metadata.tags || [], - size: this.estimateSize(contextData), - }; - - this.contexts.set(contextId, context); - - // Initialize version history - this.contextHistory.set(contextId, [ - { - versionId, - timestamp, - data: JSON.parse(JSON.stringify(contextData)), // Deep clone - metadata: { ...context.metadata }, - }, - ]); - - await this.persistContext(contextId); - await this.persistContextVersion(contextId, versionId); - - // Update the search index - this.rebuildSearchIndex(); - - this.logger.info(`Created context: ${contextId} (version: ${versionId})`); - return context; - } - - /** - * Retrieve a context by ID - * @param {string} contextId - The context ID to retrieve - * @param {string} versionId - Optional specific version to retrieve - * @returns {object} The context object - */ - async getContext(contextId, versionId = null) { - // If specific version requested, try to get it from history - if (versionId) { - return this.getContextVersion(contextId, versionId); - } - - // Try to get from memory first - if (this.contexts.has(contextId)) { - return this.contexts.get(contextId); - } - - // Try to load from disk - try { - const context = await this.loadContextFromDisk(contextId); - if (context) { - this.contexts.set(contextId, context); - return context; - } - } catch (error) { - this.logger.error( - `Failed to load context ${contextId}: ${error.message}` - ); - } - - throw new Error(`Context with ID ${contextId} not found`); - } - - /** - * Get a specific version of a context - * @param {string} contextId - The context ID - * @param {string} versionId - The version ID - * @returns {object} The versioned context - */ - async getContextVersion(contextId, versionId) { - // Check if version history is in memory - if (this.contextHistory.has(contextId)) { - const history = this.contextHistory.get(contextId); - const version = history.find((v) => v.versionId === versionId); - if (version) { - return { - id: contextId, - data: version.data, - metadata: version.metadata, - tags: version.metadata.tags || [], - size: this.estimateSize(version.data), - versionId: version.versionId, - }; - } - } - - // Try to load from disk - try { - const versionPath = path.join( - CONTEXT_DIR, - "versions", - `${contextId}_${versionId}.json` - ); - const data = await fs.readFile(versionPath, "utf8"); - const version = JSON.parse(data); - - // Add to memory cache - if (!this.contextHistory.has(contextId)) { - this.contextHistory.set(contextId, []); - } - const history = this.contextHistory.get(contextId); - history.push(version); - - return { - id: contextId, - data: version.data, - metadata: version.metadata, - tags: version.metadata.tags || [], - size: this.estimateSize(version.data), - versionId: version.versionId, - }; - } catch (error) { - this.logger.error( - `Failed to load context version ${contextId}@${versionId}: ${error.message}` - ); - throw new Error( - `Context version ${versionId} for ${contextId} not found` - ); - } - } - - /** - * Update an existing context - * @param {string} contextId - The context ID to update - * @param {object|string} contextData - New context data - * @param {object} metadata - Optional metadata updates - * @param {boolean} createNewVersion - Whether to create a new version - * @returns {object} The updated context - */ - async updateContext( - contextId, - contextData, - metadata = {}, - createNewVersion = true - ) { - const context = await this.getContext(contextId); - const timestamp = new Date().toISOString(); - - // Generate a new version ID if requested - const versionId = createNewVersion - ? this.generateVersionId() - : context.metadata.version; - - // Create a backup of the current state for versioning - if (createNewVersion) { - // Store the current version in history - if (!this.contextHistory.has(contextId)) { - this.contextHistory.set(contextId, []); - } - - const history = this.contextHistory.get(contextId); - - // Add current state to history - history.push({ - versionId: context.metadata.version, - timestamp: context.metadata.updated, - data: JSON.parse(JSON.stringify(context.data)), // Deep clone - metadata: { ...context.metadata }, - }); - - // Trim history if it exceeds the maximum size - if (history.length > MAX_CONTEXT_HISTORY) { - const excessVersions = history.splice( - 0, - history.length - MAX_CONTEXT_HISTORY - ); - // Clean up excess versions from disk - for (const version of excessVersions) { - this.removeContextVersionFile(contextId, version.versionId).catch( - (err) => - this.logger.error( - `Failed to remove old version file: ${err.message}` - ) - ); - } - } - - // Persist version - await this.persistContextVersion(contextId, context.metadata.version); - } - - // Update the context - context.data = contextData; - context.metadata = { - ...context.metadata, - ...metadata, - updated: timestamp, - }; - - if (createNewVersion) { - context.metadata.version = versionId; - context.metadata.previousVersion = context.metadata.version; - } - - if (metadata.tags) { - context.tags = metadata.tags; - } - - // Update size estimate - context.size = this.estimateSize(contextData); - - this.contexts.set(contextId, context); - await this.persistContext(contextId); - - // Update the search index - this.rebuildSearchIndex(); - - this.logger.info(`Updated context: ${contextId} (version: ${versionId})`); - return context; - } - - /** - * Delete a context and all its versions - * @param {string} contextId - The context ID to delete - * @returns {boolean} True if deletion was successful - */ - async deleteContext(contextId) { - if (!this.contexts.has(contextId)) { - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - try { - await fs.access(contextPath); - } catch (error) { - throw new Error(`Context with ID ${contextId} not found`); - } - } - - this.contexts.delete(contextId); - - // Remove from history - const history = this.contextHistory.get(contextId) || []; - this.contextHistory.delete(contextId); - - try { - // Delete main context file - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - await fs.unlink(contextPath); - - // Delete all version files - for (const version of history) { - await this.removeContextVersionFile(contextId, version.versionId); - } - - // Update the search index - this.rebuildSearchIndex(); - - this.logger.info(`Deleted context: ${contextId}`); - return true; - } catch (error) { - this.logger.error( - `Failed to delete context files for ${contextId}: ${error.message}` - ); - throw error; - } - } - - /** - * List all available contexts with pagination and advanced filtering - * @param {object} options - Options for listing contexts - * @param {object} options.filters - Filters to apply - * @param {number} options.limit - Maximum number of contexts to return - * @param {number} options.offset - Number of contexts to skip - * @param {string} options.sortBy - Field to sort by - * @param {string} options.sortDirection - Sort direction ('asc' or 'desc') - * @param {string} options.query - Free text search query - * @returns {Array} Array of context objects - */ - async listContexts(options = {}) { - // Load all contexts from disk first - await this.loadAllContextsFromDisk(); - - const { - filters = {}, - limit = 100, - offset = 0, - sortBy = "updated", - sortDirection = "desc", - query = "", - } = options; - - let contexts; - - // If there's a search query, use the search index - if (query && this.contextIndex) { - const searchResults = this.contextIndex.search(query); - contexts = searchResults.map((result) => - this.contexts.get(result.item.id) - ); - } else { - contexts = Array.from(this.contexts.values()); - } - - // Apply filters - if (filters.tag) { - contexts = contexts.filter( - (ctx) => ctx.tags && ctx.tags.includes(filters.tag) - ); - } - - if (filters.metadataKey && filters.metadataValue) { - contexts = contexts.filter( - (ctx) => - ctx.metadata && - ctx.metadata[filters.metadataKey] === filters.metadataValue - ); - } - - if (filters.createdAfter) { - const timestamp = new Date(filters.createdAfter); - contexts = contexts.filter( - (ctx) => new Date(ctx.metadata.created) >= timestamp - ); - } - - if (filters.updatedAfter) { - const timestamp = new Date(filters.updatedAfter); - contexts = contexts.filter( - (ctx) => new Date(ctx.metadata.updated) >= timestamp - ); - } - - // Apply sorting - contexts.sort((a, b) => { - let valueA, valueB; - - if (sortBy === "created" || sortBy === "updated") { - valueA = new Date(a.metadata[sortBy]).getTime(); - valueB = new Date(b.metadata[sortBy]).getTime(); - } else if (sortBy === "size") { - valueA = a.size || 0; - valueB = b.size || 0; - } else if (sortBy === "id") { - valueA = a.id; - valueB = b.id; - } else { - valueA = a.metadata[sortBy]; - valueB = b.metadata[sortBy]; - } - - if (valueA === valueB) return 0; - - const sortFactor = sortDirection === "asc" ? 1 : -1; - return valueA < valueB ? -1 * sortFactor : 1 * sortFactor; - }); - - // Apply pagination - const paginatedContexts = contexts.slice(offset, offset + limit); - - return { - contexts: paginatedContexts, - total: contexts.length, - offset, - limit, - hasMore: offset + limit < contexts.length, - }; - } - - /** - * Get the version history of a context - * @param {string} contextId - The context ID - * @returns {Array} Array of version objects - */ - async getContextHistory(contextId) { - // Ensure context exists - await this.getContext(contextId); - - // Load history if not in memory - if (!this.contextHistory.has(contextId)) { - await this.loadContextHistoryFromDisk(contextId); - } - - const history = this.contextHistory.get(contextId) || []; - - // Return versions in reverse chronological order (newest first) - return history.sort((a, b) => { - const timeA = new Date(a.timestamp).getTime(); - const timeB = new Date(b.timestamp).getTime(); - return timeB - timeA; - }); - } - - /** - * Add tags to a context - * @param {string} contextId - The context ID - * @param {Array} tags - Array of tags to add - * @returns {object} The updated context - */ - async addTags(contextId, tags) { - const context = await this.getContext(contextId); - - const currentTags = context.tags || []; - const uniqueTags = [...new Set([...currentTags, ...tags])]; - - // Update context with new tags - return this.updateContext( - contextId, - context.data, - { - tags: uniqueTags, - }, - false - ); // Don't create a new version for tag updates - } - - /** - * Remove tags from a context - * @param {string} contextId - The context ID - * @param {Array} tags - Array of tags to remove - * @returns {object} The updated context - */ - async removeTags(contextId, tags) { - const context = await this.getContext(contextId); - - const currentTags = context.tags || []; - const newTags = currentTags.filter((tag) => !tags.includes(tag)); - - // Update context with new tags - return this.updateContext( - contextId, - context.data, - { - tags: newTags, - }, - false - ); // Don't create a new version for tag updates - } - - /** - * Handle context windowing and truncation - * @param {string} contextId - The context ID - * @param {number} maxSize - Maximum size in tokens/chars - * @param {string} strategy - Truncation strategy ('start', 'end', 'middle') - * @returns {object} The truncated context - */ - async truncateContext(contextId, maxSize, strategy = "end") { - const context = await this.getContext(contextId); - const contextText = - typeof context.data === "string" - ? context.data - : JSON.stringify(context.data); - - if (contextText.length <= maxSize) { - return context; // No truncation needed - } - - let truncatedData; - - switch (strategy) { - case "start": - truncatedData = contextText.slice(contextText.length - maxSize); - break; - case "middle": - const halfSize = Math.floor(maxSize / 2); - truncatedData = - contextText.slice(0, halfSize) + - "...[truncated]..." + - contextText.slice(contextText.length - halfSize); - break; - case "end": - default: - truncatedData = contextText.slice(0, maxSize); - break; - } - - // If original data was an object, try to parse the truncated data - // Otherwise use it as a string - let updatedData; - if (typeof context.data === "object") { - try { - // This may fail if truncation broke JSON structure - updatedData = { - ...context.data, - truncated: true, - truncation_strategy: strategy, - original_size: contextText.length, - truncated_size: truncatedData.length, - }; - } catch (error) { - updatedData = truncatedData; - } - } else { - updatedData = truncatedData; - } - - // Update with truncated data - return this.updateContext( - contextId, - updatedData, - { - truncated: true, - truncation_strategy: strategy, - original_size: contextText.length, - truncated_size: truncatedData.length, - }, - true - ); // Create a new version for the truncated data - } - - /** - * Merge multiple contexts into a new context - * @param {Array} contextIds - Array of context IDs to merge - * @param {string} newContextId - ID for the new merged context - * @param {object} metadata - Optional metadata for the new context - * @returns {object} The new merged context - */ - async mergeContexts(contextIds, newContextId, metadata = {}) { - if (contextIds.length === 0) { - throw new Error("At least one context ID must be provided for merging"); - } - - if (this.contexts.has(newContextId)) { - throw new Error(`Context with ID ${newContextId} already exists`); - } - - // Load all contexts to be merged - const contextsToMerge = []; - for (const id of contextIds) { - try { - const context = await this.getContext(id); - contextsToMerge.push(context); - } catch (error) { - this.logger.error( - `Could not load context ${id} for merging: ${error.message}` - ); - throw new Error(`Failed to merge contexts: ${error.message}`); - } - } - - // Check data types and decide how to merge - const allStrings = contextsToMerge.every((c) => typeof c.data === "string"); - const allObjects = contextsToMerge.every( - (c) => typeof c.data === "object" && c.data !== null - ); - - let mergedData; - - if (allStrings) { - // Merge strings with newlines between them - mergedData = contextsToMerge.map((c) => c.data).join("\n\n"); - } else if (allObjects) { - // Merge objects by combining their properties - mergedData = {}; - for (const context of contextsToMerge) { - mergedData = { ...mergedData, ...context.data }; - } - } else { - // Convert everything to strings and concatenate - mergedData = contextsToMerge - .map((c) => - typeof c.data === "string" ? c.data : JSON.stringify(c.data) - ) - .join("\n\n"); - } - - // Collect all tags from merged contexts - const allTags = new Set(); - for (const context of contextsToMerge) { - for (const tag of context.tags || []) { - allTags.add(tag); - } - } - - // Create merged metadata - const mergedMetadata = { - ...metadata, - tags: [...allTags], - merged_from: contextIds, - merged_at: new Date().toISOString(), - }; - - // Create the new merged context - return this.createContext(newContextId, mergedData, mergedMetadata); - } - - /** - * Persist a context to disk - * @param {string} contextId - The context ID to persist - * @returns {Promise} - */ - async persistContext(contextId) { - const context = this.contexts.get(contextId); - if (!context) { - throw new Error(`Context with ID ${contextId} not found`); - } - - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - try { - await fs.writeFile(contextPath, JSON.stringify(context, null, 2), "utf8"); - this.logger.debug(`Persisted context ${contextId} to disk`); - } catch (error) { - this.logger.error( - `Failed to persist context ${contextId}: ${error.message}` - ); - throw error; - } - } - - /** - * Persist a context version to disk - * @param {string} contextId - The context ID - * @param {string} versionId - The version ID - * @returns {Promise} - */ - async persistContextVersion(contextId, versionId) { - if (!this.contextHistory.has(contextId)) { - throw new Error(`Context history for ${contextId} not found`); - } - - const history = this.contextHistory.get(contextId); - const version = history.find((v) => v.versionId === versionId); - - if (!version) { - throw new Error(`Version ${versionId} of context ${contextId} not found`); - } - - const versionPath = path.join( - CONTEXT_DIR, - "versions", - `${contextId}_${versionId}.json` - ); - try { - await fs.writeFile(versionPath, JSON.stringify(version, null, 2), "utf8"); - this.logger.debug( - `Persisted context version ${contextId}@${versionId} to disk` - ); - } catch (error) { - this.logger.error( - `Failed to persist context version ${contextId}@${versionId}: ${error.message}` - ); - throw error; - } - } - - /** - * Remove a context version file from disk - * @param {string} contextId - The context ID - * @param {string} versionId - The version ID - * @returns {Promise} - */ - async removeContextVersionFile(contextId, versionId) { - const versionPath = path.join( - CONTEXT_DIR, - "versions", - `${contextId}_${versionId}.json` - ); - try { - await fs.unlink(versionPath); - this.logger.debug( - `Removed context version file ${contextId}@${versionId}` - ); - } catch (error) { - if (error.code !== "ENOENT") { - this.logger.error( - `Failed to remove context version file ${contextId}@${versionId}: ${error.message}` - ); - throw error; - } - } - } - - /** - * Load a context from disk - * @param {string} contextId - The context ID to load - * @returns {Promise} The loaded context - */ - async loadContextFromDisk(contextId) { - const contextPath = path.join(CONTEXT_DIR, `${contextId}.json`); - try { - const data = await fs.readFile(contextPath, "utf8"); - const context = JSON.parse(data); - this.logger.debug(`Loaded context ${contextId} from disk`); - return context; - } catch (error) { - this.logger.error( - `Failed to load context ${contextId} from disk: ${error.message}` - ); - throw error; - } - } - - /** - * Load context history from disk - * @param {string} contextId - The context ID - * @returns {Promise} The loaded history - */ - async loadContextHistoryFromDisk(contextId) { - try { - const files = await fs.readdir(path.join(CONTEXT_DIR, "versions")); - const versionFiles = files.filter( - (file) => file.startsWith(`${contextId}_`) && file.endsWith(".json") - ); - - const history = []; - - for (const file of versionFiles) { - try { - const data = await fs.readFile( - path.join(CONTEXT_DIR, "versions", file), - "utf8" - ); - const version = JSON.parse(data); - history.push(version); - } catch (error) { - this.logger.error( - `Failed to load context version file ${file}: ${error.message}` - ); - } - } - - this.contextHistory.set(contextId, history); - this.logger.debug( - `Loaded ${history.length} versions for context ${contextId}` - ); - - return history; - } catch (error) { - this.logger.error( - `Failed to load context history for ${contextId}: ${error.message}` - ); - this.contextHistory.set(contextId, []); - return []; - } - } - - /** - * Load all contexts from disk - * @returns {Promise} - */ - async loadAllContextsFromDisk() { - try { - const files = await fs.readdir(CONTEXT_DIR); - const contextFiles = files.filter((file) => file.endsWith(".json")); - - for (const file of contextFiles) { - const contextId = path.basename(file, ".json"); - if (!this.contexts.has(contextId)) { - try { - const context = await this.loadContextFromDisk(contextId); - this.contexts.set(contextId, context); - } catch (error) { - // Already logged in loadContextFromDisk - } - } - } - - this.logger.info(`Loaded ${this.contexts.size} contexts from disk`); - } catch (error) { - this.logger.error(`Failed to load contexts from disk: ${error.message}`); - throw error; - } - } - - /** - * Generate a unique version ID - * @returns {string} A unique version ID - */ - generateVersionId() { - return crypto.randomBytes(8).toString("hex"); - } - - /** - * Estimate the size of context data - * @param {object|string} data - The context data - * @returns {number} Estimated size in bytes - */ - estimateSize(data) { - if (typeof data === "string") { - return Buffer.byteLength(data, "utf8"); - } - - if (typeof data === "object" && data !== null) { - return Buffer.byteLength(JSON.stringify(data), "utf8"); - } - - return 0; - } -} - -export default ContextManager; diff --git a/mcp-server/src/index.js b/mcp-server/src/index.js index eb820f95..3fe17b58 100644 --- a/mcp-server/src/index.js +++ b/mcp-server/src/index.js @@ -1,16 +1,10 @@ import { FastMCP } from "fastmcp"; -import { z } from "zod"; import path from "path"; -import fs from "fs/promises"; import dotenv from "dotenv"; import { fileURLToPath } from "url"; -import express from "express"; -import cors from "cors"; -import helmet from "helmet"; -import { logger } from "../../scripts/modules/utils.js"; -import MCPAuth from "./auth.js"; -import MCPApiHandlers from "./api-handlers.js"; -import ContextManager from "./context-manager.js"; +import fs from "fs"; +import logger from "./logger.js"; +import { registerTaskMasterTools } from "./tools/index.js"; // Load environment variables dotenv.config(); @@ -18,25 +12,27 @@ dotenv.config(); // Constants const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const DEFAULT_PORT = process.env.MCP_SERVER_PORT || 3000; -const DEFAULT_HOST = process.env.MCP_SERVER_HOST || "localhost"; /** * Main MCP server class that integrates with Task Master */ class TaskMasterMCPServer { - constructor(options = {}) { + constructor() { + // Get version from package.json using synchronous fs + const packagePath = path.join(__dirname, "../../package.json"); + const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); + this.options = { name: "Task Master MCP Server", - version: process.env.PROJECT_VERSION || "1.0.0", - ...options, + version: packageJson.version, }; this.server = new FastMCP(this.options); - this.expressApp = null; this.initialized = false; - this.auth = new MCPAuth(); - this.contextManager = new ContextManager(); + + // this.server.addResource({}); + + // this.server.addResourceTemplate({}); // Bind methods this.init = this.init.bind(this); @@ -53,301 +49,27 @@ class TaskMasterMCPServer { async init() { if (this.initialized) return; - this.logger.info("Initializing Task Master MCP server..."); - - // Set up express for additional customization if needed - this.expressApp = express(); - this.expressApp.use(cors()); - this.expressApp.use(helmet()); - this.expressApp.use(express.json()); - - // Set up authentication middleware - this.setupAuthentication(); - - // Register API handlers - this.apiHandlers = new MCPApiHandlers(this.server); - - // Register additional task master specific tools - this.registerTaskMasterTools(); + // Register Task Master tools + registerTaskMasterTools(this.server); this.initialized = true; - this.logger.info("Task Master MCP server initialized successfully"); return this; } - /** - * Set up authentication for the MCP server - */ - setupAuthentication() { - // Add a health check endpoint that doesn't require authentication - this.expressApp.get("/health", (req, res) => { - res.status(200).json({ - status: "ok", - service: this.options.name, - version: this.options.version, - }); - }); - - // Add an authenticate endpoint to get a JWT token using an API key - this.expressApp.post("/auth/token", async (req, res) => { - const apiKey = req.headers["x-api-key"]; - - if (!apiKey) { - return res.status(401).json({ - success: false, - error: "API key is required", - }); - } - - const keyDetails = this.auth.validateApiKey(apiKey); - - if (!keyDetails) { - return res.status(401).json({ - success: false, - error: "Invalid API key", - }); - } - - const token = this.auth.generateToken(keyDetails.id, keyDetails.role); - - res.status(200).json({ - success: true, - token, - expiresIn: process.env.MCP_JWT_EXPIRATION || "24h", - clientId: keyDetails.id, - role: keyDetails.role, - }); - }); - - // Create authenticator middleware for FastMCP - this.server.setAuthenticator((request) => { - // Get token from Authorization header - const authHeader = request.headers?.authorization; - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return null; - } - - const token = authHeader.split(" ")[1]; - const payload = this.auth.verifyToken(token); - - if (!payload) { - return null; - } - - return { - clientId: payload.clientId, - role: payload.role, - }; - }); - - // Set up a protected route for API key management (admin only) - this.expressApp.post( - "/auth/api-keys", - (req, res, next) => { - this.auth.authenticateToken(req, res, next); - }, - (req, res, next) => { - this.auth.authorizeRoles(["admin"])(req, res, next); - }, - async (req, res) => { - const { clientId, role } = req.body; - - if (!clientId) { - return res.status(400).json({ - success: false, - error: "Client ID is required", - }); - } - - try { - const apiKey = await this.auth.createApiKey(clientId, role || "user"); - - res.status(201).json({ - success: true, - apiKey, - clientId, - role: role || "user", - }); - } catch (error) { - this.logger.error(`Error creating API key: ${error.message}`); - - res.status(500).json({ - success: false, - error: "Failed to create API key", - }); - } - } - ); - - this.logger.info("Set up MCP authentication"); - } - - /** - * Register Task Master specific tools with the MCP server - */ - registerTaskMasterTools() { - // Add a tool to get tasks from Task Master - this.server.addTool({ - name: "listTasks", - description: "List all tasks from Task Master", - parameters: z.object({ - status: z.string().optional().describe("Filter tasks by status"), - withSubtasks: z - .boolean() - .optional() - .describe("Include subtasks in the response"), - }), - execute: async (args) => { - try { - // In a real implementation, this would use the Task Master API - // to fetch tasks. For now, returning mock data. - - this.logger.info( - `Listing tasks with filters: ${JSON.stringify(args)}` - ); - - // Mock task data - const tasks = [ - { - id: 1, - title: "Implement Task Data Structure", - status: "done", - dependencies: [], - priority: "high", - }, - { - id: 2, - title: "Develop Command Line Interface Foundation", - status: "done", - dependencies: [1], - priority: "high", - }, - { - id: 23, - title: "Implement MCP Server Functionality", - status: "in-progress", - dependencies: [22], - priority: "medium", - subtasks: [ - { - id: "23.1", - title: "Create Core MCP Server Module", - status: "in-progress", - dependencies: [], - }, - { - id: "23.2", - title: "Implement Context Management System", - status: "pending", - dependencies: ["23.1"], - }, - ], - }, - ]; - - // Apply status filter if provided - let filteredTasks = tasks; - if (args.status) { - filteredTasks = tasks.filter((task) => task.status === args.status); - } - - // Remove subtasks if not requested - if (!args.withSubtasks) { - filteredTasks = filteredTasks.map((task) => { - const { subtasks, ...taskWithoutSubtasks } = task; - return taskWithoutSubtasks; - }); - } - - return { success: true, tasks: filteredTasks }; - } catch (error) { - this.logger.error(`Error listing tasks: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - // Add a tool to get task details - this.server.addTool({ - name: "getTaskDetails", - description: "Get detailed information about a specific task", - parameters: z.object({ - taskId: z - .union([z.number(), z.string()]) - .describe("The ID of the task to get details for"), - }), - execute: async (args) => { - try { - // In a real implementation, this would use the Task Master API - // to fetch task details. For now, returning mock data. - - this.logger.info(`Getting details for task ${args.taskId}`); - - // Mock task details - const taskDetails = { - id: 23, - title: "Implement MCP Server Functionality", - description: - "Extend Task Master to function as an MCP server, allowing it to provide context management services to other applications.", - status: "in-progress", - dependencies: [22], - priority: "medium", - details: - "This task involves implementing the Model Context Protocol server capabilities within Task Master.", - testStrategy: - "Testing should include unit tests, integration tests, and compatibility tests.", - subtasks: [ - { - id: "23.1", - title: "Create Core MCP Server Module", - status: "in-progress", - dependencies: [], - }, - { - id: "23.2", - title: "Implement Context Management System", - status: "pending", - dependencies: ["23.1"], - }, - ], - }; - - return { success: true, task: taskDetails }; - } catch (error) { - this.logger.error(`Error getting task details: ${error.message}`); - return { success: false, error: error.message }; - } - }, - }); - - this.logger.info("Registered Task Master specific tools"); - } - /** * Start the MCP server */ - async start({ port = DEFAULT_PORT, host = DEFAULT_HOST } = {}) { + async start() { if (!this.initialized) { await this.init(); } - this.logger.info( - `Starting Task Master MCP server on http://${host}:${port}` - ); - // Start the FastMCP server await this.server.start({ - port, - host, - transportType: "sse", - expressApp: this.expressApp, + transportType: "stdio", }); - this.logger.info( - `Task Master MCP server running at http://${host}:${port}` - ); - return this; } @@ -356,9 +78,7 @@ class TaskMasterMCPServer { */ async stop() { if (this.server) { - this.logger.info("Stopping Task Master MCP server..."); await this.server.stop(); - this.logger.info("Task Master MCP server stopped"); } } } diff --git a/mcp-server/src/logger.js b/mcp-server/src/logger.js new file mode 100644 index 00000000..80c0e55c --- /dev/null +++ b/mcp-server/src/logger.js @@ -0,0 +1,68 @@ +import chalk from "chalk"; + +// Define log levels +const LOG_LEVELS = { + debug: 0, + info: 1, + warn: 2, + error: 3, + success: 4, +}; + +// Get log level from environment or default to info +const LOG_LEVEL = process.env.LOG_LEVEL + ? LOG_LEVELS[process.env.LOG_LEVEL.toLowerCase()] + : LOG_LEVELS.info; + +/** + * Logs a message with the specified level + * @param {string} level - The log level (debug, info, warn, error, success) + * @param {...any} args - Arguments to log + */ +function log(level, ...args) { + const icons = { + debug: chalk.gray("🔍"), + info: chalk.blue("â„šī¸"), + warn: chalk.yellow("âš ī¸"), + error: chalk.red("❌"), + success: chalk.green("✅"), + }; + + if (LOG_LEVELS[level] >= LOG_LEVEL) { + const icon = icons[level] || ""; + + if (level === "error") { + console.error(icon, chalk.red(...args)); + } else if (level === "warn") { + console.warn(icon, chalk.yellow(...args)); + } else if (level === "success") { + console.log(icon, chalk.green(...args)); + } else if (level === "info") { + console.log(icon, chalk.blue(...args)); + } else { + console.log(icon, ...args); + } + } +} + +/** + * Create a logger object with methods for different log levels + * Can be used as a drop-in replacement for existing logger initialization + * @returns {Object} Logger object with info, error, debug, warn, and success methods + */ +export function createLogger() { + return { + debug: (message) => log("debug", message), + info: (message) => log("info", message), + warn: (message) => log("warn", message), + error: (message) => log("error", message), + success: (message) => log("success", message), + log: log, // Also expose the raw log function + }; +} + +// Export a default logger instance +const logger = createLogger(); + +export default logger; +export { log, LOG_LEVELS }; diff --git a/mcp-server/src/tools/addTask.js b/mcp-server/src/tools/addTask.js new file mode 100644 index 00000000..0622d0e8 --- /dev/null +++ b/mcp-server/src/tools/addTask.js @@ -0,0 +1,56 @@ +/** + * tools/addTask.js + * Tool to add a new task using AI + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the addTask tool with the MCP server + * @param {FastMCP} server - FastMCP server instance + */ +export function registerAddTaskTool(server) { + server.addTool({ + name: "addTask", + description: "Add a new task using AI", + parameters: z.object({ + prompt: z.string().describe("Description of the task to add"), + dependencies: z + .string() + .optional() + .describe("Comma-separated list of task IDs this task depends on"), + priority: z + .string() + .optional() + .describe("Task priority (high, medium, low)"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Adding new task: ${args.prompt}`); + + const cmdArgs = [`--prompt="${args.prompt}"`]; + if (args.dependencies) + cmdArgs.push(`--dependencies=${args.dependencies}`); + if (args.priority) cmdArgs.push(`--priority=${args.priority}`); + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("add-task", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error adding task: ${error.message}`); + return createErrorResponse(`Error adding task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/expandTask.js b/mcp-server/src/tools/expandTask.js new file mode 100644 index 00000000..b94d00d4 --- /dev/null +++ b/mcp-server/src/tools/expandTask.js @@ -0,0 +1,66 @@ +/** + * tools/expandTask.js + * Tool to break down a task into detailed subtasks + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the expandTask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerExpandTaskTool(server) { + server.addTool({ + name: "expandTask", + description: "Break down a task into detailed subtasks", + parameters: z.object({ + id: z.union([z.string(), z.number()]).describe("Task ID to expand"), + num: z.number().optional().describe("Number of subtasks to generate"), + research: z + .boolean() + .optional() + .describe( + "Enable Perplexity AI for research-backed subtask generation" + ), + prompt: z + .string() + .optional() + .describe("Additional context to guide subtask generation"), + force: z + .boolean() + .optional() + .describe( + "Force regeneration of subtasks for tasks that already have them" + ), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Expanding task ${args.id}`); + + const cmdArgs = [`--id=${args.id}`]; + if (args.num) cmdArgs.push(`--num=${args.num}`); + if (args.research) cmdArgs.push("--research"); + if (args.prompt) cmdArgs.push(`--prompt="${args.prompt}"`); + if (args.force) cmdArgs.push("--force"); + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("expand", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error expanding task: ${error.message}`); + return createErrorResponse(`Error expanding task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/index.js b/mcp-server/src/tools/index.js new file mode 100644 index 00000000..97d47438 --- /dev/null +++ b/mcp-server/src/tools/index.js @@ -0,0 +1,29 @@ +/** + * tools/index.js + * Export all Task Master CLI tools for MCP server + */ + +import logger from "../logger.js"; +import { registerListTasksTool } from "./listTasks.js"; +import { registerShowTaskTool } from "./showTask.js"; +import { registerSetTaskStatusTool } from "./setTaskStatus.js"; +import { registerExpandTaskTool } from "./expandTask.js"; +import { registerNextTaskTool } from "./nextTask.js"; +import { registerAddTaskTool } from "./addTask.js"; + +/** + * Register all Task Master tools with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerTaskMasterTools(server) { + registerListTasksTool(server); + registerShowTaskTool(server); + registerSetTaskStatusTool(server); + registerExpandTaskTool(server); + registerNextTaskTool(server); + registerAddTaskTool(server); +} + +export default { + registerTaskMasterTools, +}; diff --git a/mcp-server/src/tools/listTasks.js b/mcp-server/src/tools/listTasks.js new file mode 100644 index 00000000..7da65692 --- /dev/null +++ b/mcp-server/src/tools/listTasks.js @@ -0,0 +1,51 @@ +/** + * tools/listTasks.js + * Tool to list all tasks from Task Master + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the listTasks tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerListTasksTool(server) { + server.addTool({ + name: "listTasks", + description: "List all tasks from Task Master", + parameters: z.object({ + status: z.string().optional().describe("Filter tasks by status"), + withSubtasks: z + .boolean() + .optional() + .describe("Include subtasks in the response"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Listing tasks with filters: ${JSON.stringify(args)}`); + + const cmdArgs = []; + if (args.status) cmdArgs.push(`--status=${args.status}`); + if (args.withSubtasks) cmdArgs.push("--with-subtasks"); + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("list", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error listing tasks: ${error.message}`); + return createErrorResponse(`Error listing tasks: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/nextTask.js b/mcp-server/src/tools/nextTask.js new file mode 100644 index 00000000..4003ce04 --- /dev/null +++ b/mcp-server/src/tools/nextTask.js @@ -0,0 +1,45 @@ +/** + * tools/nextTask.js + * Tool to show the next task to work on based on dependencies and status + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the nextTask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerNextTaskTool(server) { + server.addTool({ + name: "nextTask", + description: + "Show the next task to work on based on dependencies and status", + parameters: z.object({ + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Finding next task to work on`); + + const cmdArgs = []; + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("next", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error finding next task: ${error.message}`); + return createErrorResponse(`Error finding next task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/setTaskStatus.js b/mcp-server/src/tools/setTaskStatus.js new file mode 100644 index 00000000..5681dd7b --- /dev/null +++ b/mcp-server/src/tools/setTaskStatus.js @@ -0,0 +1,52 @@ +/** + * tools/setTaskStatus.js + * Tool to set the status of a task + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the setTaskStatus tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerSetTaskStatusTool(server) { + server.addTool({ + name: "setTaskStatus", + description: "Set the status of a task", + parameters: z.object({ + id: z + .union([z.string(), z.number()]) + .describe("Task ID (can be comma-separated for multiple tasks)"), + status: z + .string() + .describe("New status (todo, in-progress, review, done)"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Setting status of task(s) ${args.id} to: ${args.status}`); + + const cmdArgs = [`--id=${args.id}`, `--status=${args.status}`]; + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("set-status", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error setting task status: ${error.message}`); + return createErrorResponse( + `Error setting task status: ${error.message}` + ); + } + }, + }); +} diff --git a/mcp-server/src/tools/showTask.js b/mcp-server/src/tools/showTask.js new file mode 100644 index 00000000..c44d9463 --- /dev/null +++ b/mcp-server/src/tools/showTask.js @@ -0,0 +1,45 @@ +/** + * tools/showTask.js + * Tool to show detailed information about a specific task + */ + +import { z } from "zod"; +import { + executeTaskMasterCommand, + createContentResponse, + createErrorResponse, +} from "./utils.js"; + +/** + * Register the showTask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerShowTaskTool(server) { + server.addTool({ + name: "showTask", + description: "Show detailed information about a specific task", + parameters: z.object({ + id: z.union([z.string(), z.number()]).describe("Task ID to show"), + file: z.string().optional().describe("Path to the tasks file"), + }), + execute: async (args, { log }) => { + try { + log.info(`Showing task details for ID: ${args.id}`); + + const cmdArgs = [args.id]; + if (args.file) cmdArgs.push(`--file=${args.file}`); + + const result = executeTaskMasterCommand("show", log, cmdArgs); + + if (!result.success) { + throw new Error(result.error); + } + + return createContentResponse(result.stdout); + } catch (error) { + log.error(`Error showing task: ${error.message}`); + return createErrorResponse(`Error showing task: ${error.message}`); + } + }, + }); +} diff --git a/mcp-server/src/tools/utils.js b/mcp-server/src/tools/utils.js new file mode 100644 index 00000000..24745d2e --- /dev/null +++ b/mcp-server/src/tools/utils.js @@ -0,0 +1,90 @@ +/** + * tools/utils.js + * Utility functions for Task Master CLI integration + */ + +import { spawnSync } from "child_process"; + +/** + * Execute a Task Master CLI command using child_process + * @param {string} command - The command to execute + * @param {Object} log - The logger object from FastMCP + * @param {Array} args - Arguments for the command + * @returns {Object} - The result of the command execution + */ +export function executeTaskMasterCommand(command, log, args = []) { + try { + log.info( + `Executing task-master ${command} with args: ${JSON.stringify(args)}` + ); + + // Prepare full arguments array + const fullArgs = [command, ...args]; + + // Execute the command using the global task-master CLI or local script + // Try the global CLI first + let result = spawnSync("task-master", fullArgs, { encoding: "utf8" }); + + // If global CLI is not available, try fallback to the local script + if (result.error && result.error.code === "ENOENT") { + log.info("Global task-master not found, falling back to local script"); + result = spawnSync("node", ["scripts/dev.js", ...fullArgs], { + encoding: "utf8", + }); + } + + if (result.error) { + throw new Error(`Command execution error: ${result.error.message}`); + } + + if (result.status !== 0) { + throw new Error( + `Command failed with exit code ${result.status}: ${result.stderr}` + ); + } + + return { + success: true, + stdout: result.stdout, + stderr: result.stderr, + }; + } catch (error) { + log.error(`Error executing task-master command: ${error.message}`); + return { + success: false, + error: error.message, + }; + } +} + +/** + * Creates standard content response for tools + * @param {string} text - Text content to include in response + * @returns {Object} - Content response object + */ +export function createContentResponse(text) { + return { + content: [ + { + text, + type: "text", + }, + ], + }; +} + +/** + * Creates error response for tools + * @param {string} errorMessage - Error message to include in response + * @returns {Object} - Error content response object + */ +export function createErrorResponse(errorMessage) { + return { + content: [ + { + text: errorMessage, + type: "text", + }, + ], + }; +}