feat(wip): set up mcp server and tools, but mcp on cursor not working despite working in inspector
This commit is contained in:
8
.cursor/mcp.json
Normal file
8
.cursor/mcp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"taskMaster": {
|
||||
"command": "node",
|
||||
"args": ["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);
|
||||
|
||||
@@ -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<object>} 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<object>} 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<object>} 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<object>} 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<object>} 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<object>} 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<object>} 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;
|
||||
@@ -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;
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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<object>} 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<Array>} 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<void>}
|
||||
*/
|
||||
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;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
mcp-server/src/logger.js
Normal file
68
mcp-server/src/logger.js
Normal file
@@ -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 };
|
||||
56
mcp-server/src/tools/addTask.js
Normal file
56
mcp-server/src/tools/addTask.js
Normal file
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
66
mcp-server/src/tools/expandTask.js
Normal file
66
mcp-server/src/tools/expandTask.js
Normal file
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
29
mcp-server/src/tools/index.js
Normal file
29
mcp-server/src/tools/index.js
Normal file
@@ -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,
|
||||
};
|
||||
51
mcp-server/src/tools/listTasks.js
Normal file
51
mcp-server/src/tools/listTasks.js
Normal file
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
45
mcp-server/src/tools/nextTask.js
Normal file
45
mcp-server/src/tools/nextTask.js
Normal file
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
52
mcp-server/src/tools/setTaskStatus.js
Normal file
52
mcp-server/src/tools/setTaskStatus.js
Normal file
@@ -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}`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
45
mcp-server/src/tools/showTask.js
Normal file
45
mcp-server/src/tools/showTask.js
Normal file
@@ -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}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
90
mcp-server/src/tools/utils.js
Normal file
90
mcp-server/src/tools/utils.js
Normal file
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user