feat(wip): initial commits for sub-tasks 1,2,3 for task 23
This commit is contained in:
170
mcp-server/README.md
Normal file
170
mcp-server/README.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Task Master MCP Server
|
||||
|
||||
This module implements a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for Task Master, allowing external applications to access Task Master functionality and context through a standardized API.
|
||||
|
||||
## Features
|
||||
|
||||
- MCP-compliant server implementation using FastMCP
|
||||
- RESTful API for context management
|
||||
- Authentication and authorization for secure access
|
||||
- Context storage and retrieval with metadata and tagging
|
||||
- Context windowing and truncation for handling size limits
|
||||
- Integration with Task Master for task management operations
|
||||
|
||||
## Installation
|
||||
|
||||
The MCP server is included with Task Master. Install Task Master globally to use the MCP server:
|
||||
|
||||
```bash
|
||||
npm install -g task-master-ai
|
||||
```
|
||||
|
||||
Or use it locally:
|
||||
|
||||
```bash
|
||||
npm install task-master-ai
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
The MCP server can be configured using environment variables or a `.env` file:
|
||||
|
||||
| Variable | Description | Default |
|
||||
| -------------------- | ---------------------------------------- | ----------------------------- |
|
||||
| `MCP_SERVER_PORT` | Port for the MCP server | 3000 |
|
||||
| `MCP_SERVER_HOST` | Host for the MCP server | localhost |
|
||||
| `MCP_CONTEXT_DIR` | Directory for context storage | ./mcp-server/contexts |
|
||||
| `MCP_API_KEYS_FILE` | File for API key storage | ./mcp-server/api-keys.json |
|
||||
| `MCP_JWT_SECRET` | Secret for JWT token generation | task-master-mcp-server-secret |
|
||||
| `MCP_JWT_EXPIRATION` | JWT token expiration time | 24h |
|
||||
| `LOG_LEVEL` | Logging level (debug, info, warn, error) | info |
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Starting the Server
|
||||
|
||||
Start the MCP server as a standalone process:
|
||||
|
||||
```bash
|
||||
npx task-master-mcp-server
|
||||
```
|
||||
|
||||
Or start it programmatically:
|
||||
|
||||
```javascript
|
||||
import { TaskMasterMCPServer } from "task-master-ai/mcp-server";
|
||||
|
||||
const server = new TaskMasterMCPServer();
|
||||
await server.start({ port: 3000, host: "localhost" });
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
The MCP server uses API key authentication with JWT tokens for secure access. A default admin API key is generated on first startup and can be found in the `api-keys.json` file.
|
||||
|
||||
To get a JWT token:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/token \
|
||||
-H "x-api-key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
Use the token for subsequent requests:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/mcp/tools \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
### Creating a New API Key
|
||||
|
||||
Admin users can create new API keys:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/auth/api-keys \
|
||||
-H "Authorization: Bearer ADMIN_JWT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"clientId": "user1", "role": "user"}'
|
||||
```
|
||||
|
||||
## Available MCP Endpoints
|
||||
|
||||
The MCP server implements the following MCP-compliant endpoints:
|
||||
|
||||
### Context Management
|
||||
|
||||
- `GET /mcp/context` - List all contexts
|
||||
- `POST /mcp/context` - Create a new context
|
||||
- `GET /mcp/context/{id}` - Get a specific context
|
||||
- `PUT /mcp/context/{id}` - Update a context
|
||||
- `DELETE /mcp/context/{id}` - Delete a context
|
||||
|
||||
### Models
|
||||
|
||||
- `GET /mcp/models` - List available models
|
||||
- `GET /mcp/models/{id}` - Get model details
|
||||
|
||||
### Execution
|
||||
|
||||
- `POST /mcp/execute` - Execute an operation with context
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
The MCP server provides the following tools:
|
||||
|
||||
### Context Tools
|
||||
|
||||
- `createContext` - Create a new context
|
||||
- `getContext` - Retrieve a context by ID
|
||||
- `updateContext` - Update an existing context
|
||||
- `deleteContext` - Delete a context
|
||||
- `listContexts` - List available contexts
|
||||
- `addTags` - Add tags to a context
|
||||
- `truncateContext` - Truncate a context to a maximum size
|
||||
|
||||
### Task Master Tools
|
||||
|
||||
- `listTasks` - List tasks from Task Master
|
||||
- `getTaskDetails` - Get detailed task information
|
||||
- `executeWithContext` - Execute operations using context
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating a Context
|
||||
|
||||
```javascript
|
||||
// Using the MCP client
|
||||
const client = new MCPClient("http://localhost:3000");
|
||||
await client.authenticate("YOUR_API_KEY");
|
||||
|
||||
const context = await client.createContext("my-context", {
|
||||
title: "My Project",
|
||||
tasks: ["Implement feature X", "Fix bug Y"],
|
||||
});
|
||||
```
|
||||
|
||||
### Executing an Operation with Context
|
||||
|
||||
```javascript
|
||||
// Using the MCP client
|
||||
const result = await client.execute("generateTask", "my-context", {
|
||||
title: "New Task",
|
||||
description: "Create a new task based on context",
|
||||
});
|
||||
```
|
||||
|
||||
## Integration with Other Tools
|
||||
|
||||
The Task Master MCP server can be integrated with other MCP-compatible tools and clients:
|
||||
|
||||
- LLM applications that support the MCP protocol
|
||||
- Task management systems that support context-aware operations
|
||||
- Development environments with MCP integration
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
44
mcp-server/server.js
Executable file
44
mcp-server/server.js
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import TaskMasterMCPServer from "./src/index.js";
|
||||
import dotenv from "dotenv";
|
||||
import { logger } from "../scripts/modules/utils.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
|
||||
*/
|
||||
async function startServer() {
|
||||
const server = new TaskMasterMCPServer();
|
||||
|
||||
// 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");
|
||||
} catch (error) {
|
||||
logger.error(`Failed to start MCP server: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server
|
||||
startServer();
|
||||
970
mcp-server/src/api-handlers.js
Normal file
970
mcp-server/src/api-handlers.js
Normal file
@@ -0,0 +1,970 @@
|
||||
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;
|
||||
285
mcp-server/src/auth.js
Normal file
285
mcp-server/src/auth.js
Normal file
@@ -0,0 +1,285 @@
|
||||
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;
|
||||
873
mcp-server/src/context-manager.js
Normal file
873
mcp-server/src/context-manager.js
Normal file
@@ -0,0 +1,873 @@
|
||||
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;
|
||||
366
mcp-server/src/index.js
Normal file
366
mcp-server/src/index.js
Normal file
@@ -0,0 +1,366 @@
|
||||
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";
|
||||
|
||||
// Load environment variables
|
||||
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 = {}) {
|
||||
this.options = {
|
||||
name: "Task Master MCP Server",
|
||||
version: process.env.PROJECT_VERSION || "1.0.0",
|
||||
...options,
|
||||
};
|
||||
|
||||
this.server = new FastMCP(this.options);
|
||||
this.expressApp = null;
|
||||
this.initialized = false;
|
||||
this.auth = new MCPAuth();
|
||||
this.contextManager = new ContextManager();
|
||||
|
||||
// Bind methods
|
||||
this.init = this.init.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
|
||||
// Setup logging
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the MCP server with necessary tools and routes
|
||||
*/
|
||||
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();
|
||||
|
||||
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 } = {}) {
|
||||
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,
|
||||
});
|
||||
|
||||
this.logger.info(
|
||||
`Task Master MCP server running at http://${host}:${port}`
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the MCP server
|
||||
*/
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TaskMasterMCPServer;
|
||||
1610
package-lock.json
generated
1610
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -6,7 +6,8 @@
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"task-master": "bin/task-master.js",
|
||||
"task-master-init": "bin/task-master-init.js"
|
||||
"task-master-init": "bin/task-master-init.js",
|
||||
"task-master-mcp-server": "mcp-server/server.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||
@@ -14,7 +15,8 @@
|
||||
"test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
|
||||
"prepare-package": "node scripts/prepare-package.js",
|
||||
"prepublishOnly": "npm run prepare-package",
|
||||
"prepare": "chmod +x bin/task-master.js bin/task-master-init.js"
|
||||
"prepare": "chmod +x bin/task-master.js bin/task-master-init.js mcp-server/server.js",
|
||||
"mcp-server": "node mcp-server/server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
@@ -24,7 +26,9 @@
|
||||
"development",
|
||||
"cursor",
|
||||
"anthropic",
|
||||
"llm"
|
||||
"llm",
|
||||
"mcp",
|
||||
"context"
|
||||
],
|
||||
"author": "Eyal Toledano",
|
||||
"license": "MIT",
|
||||
@@ -34,11 +38,17 @@
|
||||
"chalk": "^4.1.2",
|
||||
"cli-table3": "^0.6.5",
|
||||
"commander": "^11.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^1.20.5",
|
||||
"figlet": "^1.8.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"openai": "^4.89.0",
|
||||
"ora": "^8.2.0"
|
||||
"ora": "^8.2.0",
|
||||
"fuse.js": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -59,7 +69,8 @@
|
||||
".cursor/**",
|
||||
"README-task-master.md",
|
||||
"index.js",
|
||||
"bin/**"
|
||||
"bin/**",
|
||||
"mcp-server/**"
|
||||
],
|
||||
"overrides": {
|
||||
"node-fetch": "^3.3.2",
|
||||
@@ -72,4 +83,4 @@
|
||||
"mock-fs": "^5.5.0",
|
||||
"supertest": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,3 +56,118 @@ Testing for the MCP server functionality should include:
|
||||
- Test for common API vulnerabilities (injection, CSRF, etc.)
|
||||
|
||||
All tests should be automated and included in the CI/CD pipeline. Documentation should include examples of how to test the MCP server functionality manually using tools like curl or Postman.
|
||||
|
||||
# Subtasks:
|
||||
## 1. Create Core MCP Server Module and Basic Structure [done]
|
||||
### Dependencies: None
|
||||
### Description: Create the foundation for the MCP server implementation by setting up the core module structure, configuration, and server initialization.
|
||||
### Details:
|
||||
Implementation steps:
|
||||
1. Create a new module `mcp-server.js` with the basic server structure
|
||||
2. Implement configuration options to enable/disable the MCP server
|
||||
3. Set up Express.js routes for the required MCP endpoints (/context, /models, /execute)
|
||||
4. Create middleware for request validation and response formatting
|
||||
5. Implement basic error handling according to MCP specifications
|
||||
6. Add logging infrastructure for MCP operations
|
||||
7. Create initialization and shutdown procedures for the MCP server
|
||||
8. Set up integration with the main Task Master application
|
||||
|
||||
Testing approach:
|
||||
- Unit tests for configuration loading and validation
|
||||
- Test server initialization and shutdown procedures
|
||||
- Verify that routes are properly registered
|
||||
- Test basic error handling with invalid requests
|
||||
|
||||
## 2. Implement Context Management System [done]
|
||||
### Dependencies: [32m[1m23.1[22m[39m
|
||||
### Description: Develop a robust context management system that can efficiently store, retrieve, and manipulate context data according to the MCP specification.
|
||||
### Details:
|
||||
Implementation steps:
|
||||
1. Design and implement data structures for context storage
|
||||
2. Create methods for context creation, retrieval, updating, and deletion
|
||||
3. Implement context windowing and truncation algorithms for handling size limits
|
||||
4. Add support for context metadata and tagging
|
||||
5. Create utilities for context serialization and deserialization
|
||||
6. Implement efficient indexing for quick context lookups
|
||||
7. Add support for context versioning and history
|
||||
8. Develop mechanisms for context persistence (in-memory, disk-based, or database)
|
||||
|
||||
Testing approach:
|
||||
- Unit tests for all context operations (CRUD)
|
||||
- Performance tests for context retrieval with various sizes
|
||||
- Test context windowing and truncation with edge cases
|
||||
- Verify metadata handling and tagging functionality
|
||||
- Test persistence mechanisms with simulated failures
|
||||
|
||||
## 3. Implement MCP Endpoints and API Handlers [done]
|
||||
### Dependencies: [32m[1m23.1[22m[39m, [32m[1m23.2[22m[39m
|
||||
### Description: Develop the complete API handlers for all required MCP endpoints, ensuring they follow the protocol specification and integrate with the context management system.
|
||||
### Details:
|
||||
Implementation steps:
|
||||
1. Implement the `/context` endpoint for:
|
||||
- GET: retrieving existing context
|
||||
- POST: creating new context
|
||||
- PUT: updating existing context
|
||||
- DELETE: removing context
|
||||
2. Implement the `/models` endpoint to list available models
|
||||
3. Develop the `/execute` endpoint for performing operations with context
|
||||
4. Create request validators for each endpoint
|
||||
5. Implement response formatters according to MCP specifications
|
||||
6. Add detailed error handling for each endpoint
|
||||
7. Set up proper HTTP status codes for different scenarios
|
||||
8. Implement pagination for endpoints that return lists
|
||||
|
||||
Testing approach:
|
||||
- Unit tests for each endpoint handler
|
||||
- Integration tests with mock context data
|
||||
- Test various request formats and edge cases
|
||||
- Verify response formats match MCP specifications
|
||||
- Test error handling with invalid inputs
|
||||
- Benchmark endpoint performance
|
||||
|
||||
## 4. Implement Authentication and Authorization System [pending]
|
||||
### Dependencies: [32m[1m23.1[22m[39m, [32m[1m23.3[22m[39m
|
||||
### Description: Create a secure authentication and authorization mechanism for MCP clients to ensure only authorized applications can access the MCP server functionality.
|
||||
### Details:
|
||||
Implementation steps:
|
||||
1. Design authentication scheme (API keys, OAuth, JWT, etc.)
|
||||
2. Implement authentication middleware for all MCP endpoints
|
||||
3. Create an API key management system for client applications
|
||||
4. Develop role-based access control for different operations
|
||||
5. Implement rate limiting to prevent abuse
|
||||
6. Add secure token validation and handling
|
||||
7. Create endpoints for managing client credentials
|
||||
8. Implement audit logging for authentication events
|
||||
|
||||
Testing approach:
|
||||
- Security testing for authentication mechanisms
|
||||
- Test access control with various permission levels
|
||||
- Verify rate limiting functionality
|
||||
- Test token validation with valid and invalid tokens
|
||||
- Simulate unauthorized access attempts
|
||||
- Verify audit logs contain appropriate information
|
||||
|
||||
## 5. Optimize Performance and Finalize Documentation [pending]
|
||||
### Dependencies: [32m[1m23.1[22m[39m, [32m[1m23.2[22m[39m, [32m[1m23.3[22m[39m, [31m[1m23.4[22m[39m
|
||||
### Description: Optimize the MCP server implementation for performance, especially for context retrieval operations, and create comprehensive documentation for users.
|
||||
### Details:
|
||||
Implementation steps:
|
||||
1. Profile the MCP server to identify performance bottlenecks
|
||||
2. Implement caching mechanisms for frequently accessed contexts
|
||||
3. Optimize context serialization and deserialization
|
||||
4. Add connection pooling for database operations (if applicable)
|
||||
5. Implement request batching for bulk operations
|
||||
6. Create comprehensive API documentation with examples
|
||||
7. Add setup and configuration guides to the Task Master documentation
|
||||
8. Create example client implementations
|
||||
9. Add monitoring endpoints for server health and metrics
|
||||
10. Implement graceful degradation under high load
|
||||
|
||||
Testing approach:
|
||||
- Load testing with simulated concurrent clients
|
||||
- Measure response times for various operations
|
||||
- Test with large context sizes to verify performance
|
||||
- Verify documentation accuracy with sample requests
|
||||
- Test monitoring endpoints
|
||||
- Perform stress testing to identify failure points
|
||||
|
||||
|
||||
@@ -1343,8 +1343,68 @@
|
||||
22
|
||||
],
|
||||
"priority": "medium",
|
||||
"details": "This task involves implementing the Model Context Protocol server capabilities within Task Master using FastMCP. The implementation should:\n\n1. Use FastMCP to create the MCP server module (`mcp-server.ts` or equivalent)\n2. Implement the required MCP endpoints using FastMCP:\n - `/context` - For retrieving and updating context\n - `/models` - For listing available models\n - `/execute` - For executing operations with context\n3. Utilize FastMCP's built-in features for context management, including:\n - Efficient context storage and retrieval\n - Context windowing and truncation\n - Metadata and tagging support\n4. Add authentication and authorization mechanisms using FastMCP capabilities\n5. Implement error handling and response formatting as per MCP specifications\n6. Configure Task Master to enable/disable MCP server functionality via FastMCP settings\n7. Add documentation on using Task Master as an MCP server with FastMCP\n8. Ensure compatibility with existing MCP clients by adhering to FastMCP's compliance features\n9. Optimize performance using FastMCP tools, especially for context retrieval operations\n10. Add logging for MCP server operations using FastMCP's logging utilities\n\nThe implementation should follow RESTful API design principles and leverage FastMCP's concurrency handling for multiple client requests. Consider using TypeScript for better type safety and integration with FastMCP[1][2].",
|
||||
"testStrategy": "Testing for the MCP server functionality should include:\n\n1. Unit tests:\n - Test each MCP endpoint handler function independently using FastMCP\n - Verify context storage and retrieval mechanisms provided by FastMCP\n - Test authentication and authorization logic\n - Validate error handling for various failure scenarios\n\n2. Integration tests:\n - Set up a test MCP server instance using FastMCP\n - Test complete request/response cycles for each endpoint\n - Verify context persistence across multiple requests\n - Test with various payload sizes and content types\n\n3. Compatibility tests:\n - Test with existing MCP client libraries\n - Verify compliance with the MCP specification\n - Ensure backward compatibility with any MCP versions supported by FastMCP\n\n4. Performance tests:\n - Measure response times for context operations with various context sizes\n - Test concurrent request handling using FastMCP's concurrency tools\n - Verify memory usage remains within acceptable limits during extended operation\n\n5. Security tests:\n - Verify authentication mechanisms cannot be bypassed\n - Test for common API vulnerabilities (injection, CSRF, etc.)\n\nAll tests should be automated and included in the CI/CD pipeline. Documentation should include examples of how to test the MCP server functionality manually using tools like curl or Postman."
|
||||
"details": "This task involves implementing the Model Context Protocol server capabilities within Task Master. The implementation should:\n\n1. Create a new module `mcp-server.js` that implements the core MCP server functionality\n2. Implement the required MCP endpoints:\n - `/context` - For retrieving and updating context\n - `/models` - For listing available models\n - `/execute` - For executing operations with context\n3. Develop a context management system that can:\n - Store and retrieve context data efficiently\n - Handle context windowing and truncation when limits are reached\n - Support context metadata and tagging\n4. Add authentication and authorization mechanisms for MCP clients\n5. Implement proper error handling and response formatting according to MCP specifications\n6. Create configuration options in Task Master to enable/disable the MCP server functionality\n7. Add documentation for how to use Task Master as an MCP server\n8. Ensure the implementation is compatible with existing MCP clients\n9. Optimize for performance, especially for context retrieval operations\n10. Add logging for MCP server operations\n\nThe implementation should follow RESTful API design principles and should be able to handle concurrent requests from multiple clients.",
|
||||
"testStrategy": "Testing for the MCP server functionality should include:\n\n1. Unit tests:\n - Test each MCP endpoint handler function independently\n - Verify context storage and retrieval mechanisms\n - Test authentication and authorization logic\n - Validate error handling for various failure scenarios\n\n2. Integration tests:\n - Set up a test MCP server instance\n - Test complete request/response cycles for each endpoint\n - Verify context persistence across multiple requests\n - Test with various payload sizes and content types\n\n3. Compatibility tests:\n - Test with existing MCP client libraries\n - Verify compliance with the MCP specification\n - Ensure backward compatibility with any MCP versions supported\n\n4. Performance tests:\n - Measure response times for context operations with various context sizes\n - Test concurrent request handling\n - Verify memory usage remains within acceptable limits during extended operation\n\n5. Security tests:\n - Verify authentication mechanisms cannot be bypassed\n - Test for common API vulnerabilities (injection, CSRF, etc.)\n\nAll tests should be automated and included in the CI/CD pipeline. Documentation should include examples of how to test the MCP server functionality manually using tools like curl or Postman.",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Create Core MCP Server Module and Basic Structure",
|
||||
"description": "Create the foundation for the MCP server implementation by setting up the core module structure, configuration, and server initialization.",
|
||||
"dependencies": [],
|
||||
"details": "Implementation steps:\n1. Create a new module `mcp-server.js` with the basic server structure\n2. Implement configuration options to enable/disable the MCP server\n3. Set up Express.js routes for the required MCP endpoints (/context, /models, /execute)\n4. Create middleware for request validation and response formatting\n5. Implement basic error handling according to MCP specifications\n6. Add logging infrastructure for MCP operations\n7. Create initialization and shutdown procedures for the MCP server\n8. Set up integration with the main Task Master application\n\nTesting approach:\n- Unit tests for configuration loading and validation\n- Test server initialization and shutdown procedures\n- Verify that routes are properly registered\n- Test basic error handling with invalid requests",
|
||||
"status": "done",
|
||||
"parentTaskId": 23
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Implement Context Management System",
|
||||
"description": "Develop a robust context management system that can efficiently store, retrieve, and manipulate context data according to the MCP specification.",
|
||||
"dependencies": [
|
||||
1
|
||||
],
|
||||
"details": "Implementation steps:\n1. Design and implement data structures for context storage\n2. Create methods for context creation, retrieval, updating, and deletion\n3. Implement context windowing and truncation algorithms for handling size limits\n4. Add support for context metadata and tagging\n5. Create utilities for context serialization and deserialization\n6. Implement efficient indexing for quick context lookups\n7. Add support for context versioning and history\n8. Develop mechanisms for context persistence (in-memory, disk-based, or database)\n\nTesting approach:\n- Unit tests for all context operations (CRUD)\n- Performance tests for context retrieval with various sizes\n- Test context windowing and truncation with edge cases\n- Verify metadata handling and tagging functionality\n- Test persistence mechanisms with simulated failures",
|
||||
"status": "done",
|
||||
"parentTaskId": 23
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Implement MCP Endpoints and API Handlers",
|
||||
"description": "Develop the complete API handlers for all required MCP endpoints, ensuring they follow the protocol specification and integrate with the context management system.",
|
||||
"dependencies": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"details": "Implementation steps:\n1. Implement the `/context` endpoint for:\n - GET: retrieving existing context\n - POST: creating new context\n - PUT: updating existing context\n - DELETE: removing context\n2. Implement the `/models` endpoint to list available models\n3. Develop the `/execute` endpoint for performing operations with context\n4. Create request validators for each endpoint\n5. Implement response formatters according to MCP specifications\n6. Add detailed error handling for each endpoint\n7. Set up proper HTTP status codes for different scenarios\n8. Implement pagination for endpoints that return lists\n\nTesting approach:\n- Unit tests for each endpoint handler\n- Integration tests with mock context data\n- Test various request formats and edge cases\n- Verify response formats match MCP specifications\n- Test error handling with invalid inputs\n- Benchmark endpoint performance",
|
||||
"status": "done",
|
||||
"parentTaskId": 23
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Implement Authentication and Authorization System",
|
||||
"description": "Create a secure authentication and authorization mechanism for MCP clients to ensure only authorized applications can access the MCP server functionality.",
|
||||
"dependencies": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"details": "Implementation steps:\n1. Design authentication scheme (API keys, OAuth, JWT, etc.)\n2. Implement authentication middleware for all MCP endpoints\n3. Create an API key management system for client applications\n4. Develop role-based access control for different operations\n5. Implement rate limiting to prevent abuse\n6. Add secure token validation and handling\n7. Create endpoints for managing client credentials\n8. Implement audit logging for authentication events\n\nTesting approach:\n- Security testing for authentication mechanisms\n- Test access control with various permission levels\n- Verify rate limiting functionality\n- Test token validation with valid and invalid tokens\n- Simulate unauthorized access attempts\n- Verify audit logs contain appropriate information",
|
||||
"status": "pending",
|
||||
"parentTaskId": 23
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Optimize Performance and Finalize Documentation",
|
||||
"description": "Optimize the MCP server implementation for performance, especially for context retrieval operations, and create comprehensive documentation for users.",
|
||||
"dependencies": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"details": "Implementation steps:\n1. Profile the MCP server to identify performance bottlenecks\n2. Implement caching mechanisms for frequently accessed contexts\n3. Optimize context serialization and deserialization\n4. Add connection pooling for database operations (if applicable)\n5. Implement request batching for bulk operations\n6. Create comprehensive API documentation with examples\n7. Add setup and configuration guides to the Task Master documentation\n8. Create example client implementations\n9. Add monitoring endpoints for server health and metrics\n10. Implement graceful degradation under high load\n\nTesting approach:\n- Load testing with simulated concurrent clients\n- Measure response times for various operations\n- Test with large context sizes to verify performance\n- Verify documentation accuracy with sample requests\n- Test monitoring endpoints\n- Perform stress testing to identify failure points",
|
||||
"status": "pending",
|
||||
"parentTaskId": 23
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
|
||||
Reference in New Issue
Block a user