fix(mcp): get everything working, cleanup, and test all tools
This commit is contained in:
@@ -1,170 +0,0 @@
|
||||
# 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.
|
||||
@@ -29,6 +29,11 @@ export function registerAddTaskTool(server) {
|
||||
.optional()
|
||||
.describe("Task priority (high, medium, low)"),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
try {
|
||||
@@ -40,7 +45,12 @@ export function registerAddTaskTool(server) {
|
||||
if (args.priority) cmdArgs.push(`--priority=${args.priority}`);
|
||||
if (args.file) cmdArgs.push(`--file=${args.file}`);
|
||||
|
||||
const result = executeTaskMasterCommand("add-task", log, cmdArgs);
|
||||
const result = executeTaskMasterCommand(
|
||||
"add-task",
|
||||
log,
|
||||
cmdArgs,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
|
||||
@@ -19,7 +19,7 @@ export function registerExpandTaskTool(server) {
|
||||
name: "expandTask",
|
||||
description: "Break down a task into detailed subtasks",
|
||||
parameters: z.object({
|
||||
id: z.union([z.string(), z.number()]).describe("Task ID to expand"),
|
||||
id: z.string().describe("Task ID to expand"),
|
||||
num: z.number().optional().describe("Number of subtasks to generate"),
|
||||
research: z
|
||||
.boolean()
|
||||
@@ -38,6 +38,11 @@ export function registerExpandTaskTool(server) {
|
||||
"Force regeneration of subtasks for tasks that already have them"
|
||||
),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
try {
|
||||
@@ -50,7 +55,14 @@ export function registerExpandTaskTool(server) {
|
||||
if (args.force) cmdArgs.push("--force");
|
||||
if (args.file) cmdArgs.push(`--file=${args.file}`);
|
||||
|
||||
const result = executeTaskMasterCommand("expand", log, cmdArgs);
|
||||
const projectRoot = args.projectRoot;
|
||||
|
||||
const result = executeTaskMasterCommand(
|
||||
"expand",
|
||||
log,
|
||||
cmdArgs,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
|
||||
@@ -25,6 +25,11 @@ export function registerListTasksTool(server) {
|
||||
.optional()
|
||||
.describe("Include subtasks in the response"),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
try {
|
||||
@@ -35,12 +40,21 @@ export function registerListTasksTool(server) {
|
||||
if (args.withSubtasks) cmdArgs.push("--with-subtasks");
|
||||
if (args.file) cmdArgs.push(`--file=${args.file}`);
|
||||
|
||||
const result = executeTaskMasterCommand("list", log, cmdArgs);
|
||||
const projectRoot = args.projectRoot;
|
||||
|
||||
const result = executeTaskMasterCommand(
|
||||
"list",
|
||||
log,
|
||||
cmdArgs,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
log.info(`Listing tasks result: ${result.stdout}`, result.stdout);
|
||||
|
||||
return createContentResponse(result.stdout);
|
||||
} catch (error) {
|
||||
log.error(`Error listing tasks: ${error.message}`);
|
||||
|
||||
@@ -21,6 +21,11 @@ export function registerNextTaskTool(server) {
|
||||
"Show the next task to work on based on dependencies and status",
|
||||
parameters: z.object({
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
try {
|
||||
@@ -29,7 +34,14 @@ export function registerNextTaskTool(server) {
|
||||
const cmdArgs = [];
|
||||
if (args.file) cmdArgs.push(`--file=${args.file}`);
|
||||
|
||||
const result = executeTaskMasterCommand("next", log, cmdArgs);
|
||||
const projectRoot = args.projectRoot;
|
||||
|
||||
const result = executeTaskMasterCommand(
|
||||
"next",
|
||||
log,
|
||||
cmdArgs,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
|
||||
@@ -20,12 +20,17 @@ export function registerSetTaskStatusTool(server) {
|
||||
description: "Set the status of a task",
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.union([z.string(), z.number()])
|
||||
.string()
|
||||
.describe("Task ID (can be comma-separated for multiple tasks)"),
|
||||
status: z
|
||||
.string()
|
||||
.describe("New status (todo, in-progress, review, done)"),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
try {
|
||||
@@ -34,7 +39,14 @@ export function registerSetTaskStatusTool(server) {
|
||||
const cmdArgs = [`--id=${args.id}`, `--status=${args.status}`];
|
||||
if (args.file) cmdArgs.push(`--file=${args.file}`);
|
||||
|
||||
const result = executeTaskMasterCommand("set-status", log, cmdArgs);
|
||||
const projectRoot = args.projectRoot;
|
||||
|
||||
const result = executeTaskMasterCommand(
|
||||
"set-status",
|
||||
log,
|
||||
cmdArgs,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
|
||||
@@ -19,17 +19,29 @@ export function registerShowTaskTool(server) {
|
||||
name: "showTask",
|
||||
description: "Show detailed information about a specific task",
|
||||
parameters: z.object({
|
||||
id: z.union([z.string(), z.number()]).describe("Task ID to show"),
|
||||
id: z.string().describe("Task ID to show"),
|
||||
file: z.string().optional().describe("Path to the tasks file"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
"Root directory of the project (default: current working directory)"
|
||||
),
|
||||
}),
|
||||
execute: async (args, { log }) => {
|
||||
try {
|
||||
log.info(`Showing task details for ID: ${args.id}`);
|
||||
|
||||
const cmdArgs = [args.id];
|
||||
const cmdArgs = [`--id=${args.id}`];
|
||||
if (args.file) cmdArgs.push(`--file=${args.file}`);
|
||||
|
||||
const result = executeTaskMasterCommand("show", log, cmdArgs);
|
||||
const projectRoot = args.projectRoot;
|
||||
|
||||
const result = executeTaskMasterCommand(
|
||||
"show",
|
||||
log,
|
||||
cmdArgs,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
|
||||
@@ -10,27 +10,39 @@ import { spawnSync } from "child_process";
|
||||
* @param {string} command - The command to execute
|
||||
* @param {Object} log - The logger object from FastMCP
|
||||
* @param {Array} args - Arguments for the command
|
||||
* @param {string} cwd - Working directory for command execution (defaults to current project root)
|
||||
* @returns {Object} - The result of the command execution
|
||||
*/
|
||||
export function executeTaskMasterCommand(command, log, args = []) {
|
||||
export function executeTaskMasterCommand(
|
||||
command,
|
||||
log,
|
||||
args = [],
|
||||
cwd = process.cwd()
|
||||
) {
|
||||
try {
|
||||
log.info(
|
||||
`Executing task-master ${command} with args: ${JSON.stringify(args)}`
|
||||
`Executing task-master ${command} with args: ${JSON.stringify(
|
||||
args
|
||||
)} in directory: ${cwd}`
|
||||
);
|
||||
|
||||
// Prepare full arguments array
|
||||
const fullArgs = [command, ...args];
|
||||
|
||||
// Common options for spawn
|
||||
const spawnOptions = {
|
||||
encoding: "utf8",
|
||||
cwd: cwd,
|
||||
};
|
||||
|
||||
// Execute the command using the global task-master CLI or local script
|
||||
// Try the global CLI first
|
||||
let result = spawnSync("task-master", fullArgs, { encoding: "utf8" });
|
||||
let result = spawnSync("task-master", fullArgs, spawnOptions);
|
||||
|
||||
// If global CLI is not available, try fallback to the local script
|
||||
if (result.error && result.error.code === "ENOENT") {
|
||||
log.info("Global task-master not found, falling back to local script");
|
||||
result = spawnSync("node", ["scripts/dev.js", ...fullArgs], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
result = spawnSync("node", ["scripts/dev.js", ...fullArgs], spawnOptions);
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
@@ -38,8 +50,14 @@ export function executeTaskMasterCommand(command, log, args = []) {
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
// Improve error handling by combining stderr and stdout if stderr is empty
|
||||
const errorOutput = result.stderr
|
||||
? result.stderr.trim()
|
||||
: result.stdout
|
||||
? result.stdout.trim()
|
||||
: "Unknown error";
|
||||
throw new Error(
|
||||
`Command failed with exit code ${result.status}: ${result.stderr}`
|
||||
`Command failed with exit code ${result.status}: ${errorOutput}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user