Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ jobs:
node-version: 'latest'
- run: npm ci
- run: npm install -g tsx
- run: npm run build # Add build step if needed
- run: npm test

publish:
needs: test
if: github.ref == 'refs/heads/main' # Only run this job on main branch
permissions:
packages: write
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
215 changes: 8 additions & 207 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { TaskManager } from "./src/server/TaskManager.js";
import { ALL_TOOLS } from "./src/server/tools.js";
import { ALL_TOOLS, executeToolWithErrorHandling } from "./src/server/tools.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// Create server with capabilities BEFORE setting up handlers
const server = new Server(
{
name: "task-manager-server",
version: "1.0.9"
version: "1.1.0"
},
{
capabilities: {
Expand All @@ -28,7 +28,7 @@ console.error('Server starting with env:', {
NODE_ENV: process.env.NODE_ENV
});

// Initialize task manager
// Create task manager instance
const taskManager = new TaskManager();

// Set up request handlers AFTER capabilities are configured
Expand All @@ -39,210 +39,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name } = request.params;
const args = request.params.arguments || {};

// For validation, ensure args is an object when expected
if (name !== "list_projects" && name !== "list_tasks" && Object.keys(args).length === 0) {
throw new Error("Invalid arguments: expected object with parameters");
}

switch (name) {
// Project tools
case "list_projects": {
const result = await taskManager.listProjects();
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "read_project": {
const projectId = String(args.projectId);
if (!projectId) {
throw new Error("Missing required parameter: projectId");
}
const result = await taskManager.getNextTask(projectId);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "create_project": {
const initialPrompt = String(args.initialPrompt || "");
if (!initialPrompt || !args.tasks || !Array.isArray(args.tasks)) {
throw new Error("Missing required parameters: initialPrompt and/or tasks");
}
const projectPlan = args.projectPlan ? String(args.projectPlan) : undefined;

const result = await taskManager.createProject(
initialPrompt,
args.tasks,
projectPlan
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "delete_project": {
const projectId = String(args.projectId);
if (!projectId) {
throw new Error("Missing required parameter: projectId");
}
// Use the private data and saveTasks via indexing since there's no explicit delete method
const projectIndex = taskManager["data"].projects.findIndex((p) => p.projectId === projectId);
if (projectIndex === -1) {
return {
content: [{ type: "text", text: JSON.stringify({ status: "error", message: "Project not found" }, null, 2) }],
};
}

taskManager["data"].projects.splice(projectIndex, 1);
await taskManager["saveTasks"]();
return {
content: [{ type: "text", text: JSON.stringify({
status: "project_deleted",
message: `Project ${projectId} has been deleted.`
}, null, 2) }],
};
}

case "add_tasks_to_project": {
const projectId = String(args.projectId);
if (!projectId || !args.tasks || !Array.isArray(args.tasks)) {
throw new Error("Missing required parameters: projectId and/or tasks");
}
const result = await taskManager.addTasksToProject(projectId, args.tasks);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "finalize_project": {
const projectId = String(args.projectId);
if (!projectId) {
throw new Error("Missing required parameter: projectId");
}
const result = await taskManager.approveProjectCompletion(projectId);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

// Task tools
case "list_tasks": {
// No explicit list tasks method, so return a message
return {
content: [{ type: "text", text: JSON.stringify({
status: "error",
message: "list_tasks functionality to be implemented in future version"
}, null, 2) }],
};
}

case "read_task": {
const taskId = String(args.taskId);
if (!taskId) {
throw new Error("Missing required parameter: taskId");
}
const result = await taskManager.openTaskDetails(taskId);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "create_task": {
const projectId = String(args.projectId);
const title = String(args.title || "");
const description = String(args.description || "");

if (!projectId || !title || !description) {
throw new Error("Missing required parameters: projectId, title, and/or description");
}

const result = await taskManager.addTasksToProject(projectId, [{
title,
description
}]);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "update_task": {
const projectId = String(args.projectId);
const taskId = String(args.taskId);

if (!projectId || !taskId) {
throw new Error("Missing required parameters: projectId and/or taskId");
}

const updates = Object.fromEntries(
Object.entries({
title: args.title !== undefined ? String(args.title) : undefined,
description: args.description !== undefined ? String(args.description) : undefined,
status: args.status !== undefined ? String(args.status) as "not started" | "in progress" | "done" : undefined,
completedDetails: args.completedDetails !== undefined ? String(args.completedDetails) : undefined,
toolRecommendations: args.toolRecommendations !== undefined ? String(args.toolRecommendations) : undefined,
ruleRecommendations: args.ruleRecommendations !== undefined ? String(args.ruleRecommendations) : undefined
}).filter(([_, value]) => value !== undefined)
);

const result = await taskManager.updateTask(projectId, taskId, updates);

return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "delete_task": {
const projectId = String(args.projectId);
const taskId = String(args.taskId);

if (!projectId || !taskId) {
throw new Error("Missing required parameters: projectId and/or taskId");
}
const result = await taskManager.deleteTask(projectId, taskId);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "approve_task": {
const projectId = String(args.projectId);
const taskId = String(args.taskId);

if (!projectId || !taskId) {
throw new Error("Missing required parameters: projectId and/or taskId");
}
const result = await taskManager.approveTaskCompletion(projectId, taskId);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

case "get_next_task": {
const projectId = String(args.projectId);
if (!projectId) {
throw new Error("Missing required parameter: projectId");
}
const result = await taskManager.getNextTask(projectId);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}

default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
return executeToolWithErrorHandling(
request.params.name,
request.params.arguments || {},
taskManager
);
});

// Start the server
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "taskqueue-mcp",
"version": "1.0.9",
"version": "1.1.0",
"description": "Task Queue MCP Server",
"author": "Christopher C. Smith ([email protected])",
"main": "dist/index.js",
Expand Down
45 changes: 30 additions & 15 deletions src/client/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { Command } from "commander";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import { TaskManagerFile } from "../types/index.js";
import chalk from "chalk";
import { TaskManagerFile, ErrorCode } from "../types/index.js";
import { createError, normalizeError } from "../utils/errors.js";
import { formatCliError } from "./errors.js";

const program = new Command();
const DEFAULT_PATH = path.join(os.homedir(), "Documents", "tasks.json");
Expand All @@ -30,11 +32,21 @@ async function readData(): Promise<TaskManagerFile> {
try {
return JSON.parse(data);
} catch (error) {
throw new Error(`Failed to parse JSON data: ${error instanceof Error ? error.message : String(error)}`);
throw createError(
ErrorCode.FileParseError,
"Failed to parse task file",
{ originalError: error }
);
}
} catch (error) {
console.error(chalk.red(`Error reading task file: ${error instanceof Error ? error.message : String(error)}`));
return { projects: [] };
if (error instanceof Error && error.message.includes("ENOENT")) {
return { projects: [] };
}
throw createError(
ErrorCode.FileReadError,
"Failed to read task file",
{ originalError: error }
);
}
}

Expand All @@ -54,8 +66,11 @@ async function writeData(data: TaskManagerFile): Promise<void> {
await fs.writeFile(TASK_FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
console.log(chalk.green('Data saved successfully'));
} catch (error) {
console.error(chalk.red(`Error writing to task file: ${error instanceof Error ? error.message : String(error)}`));
throw error;
throw createError(
ErrorCode.FileWriteError,
"Failed to write task file",
{ originalError: error }
);
}
}

Expand All @@ -65,10 +80,10 @@ program
.version("1.0.0");

program
.command("approve-task")
.command("approve")
.description("Approve a completed task")
.argument("<projectId>", "ID of the project containing the task")
.argument("<taskId>", "ID of the task to approve")
.argument("<projectId>", "Project ID")
.argument("<taskId>", "Task ID")
.option('-f, --force', 'Force approval even if task is not marked as done')
.action(async (projectId, taskId, options) => {
try {
Expand Down Expand Up @@ -145,15 +160,15 @@ program
console.log(chalk.yellow(`${completedTasks - approvedTasks} tasks remaining to be approved.`));
}
} catch (error) {
console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`));
console.error(chalk.red(formatCliError(normalizeError(error))));
process.exit(1);
}
});

program
.command("approve-project")
.description("Approve project completion")
.argument("<projectId>", "ID of the project to approve")
.command("finalize")
.description("Mark a project as complete")
.argument("<projectId>", "Project ID")
.action(async (projectId) => {
try {
console.log(chalk.blue(`Approving project ${chalk.bold(projectId)}...`));
Expand Down Expand Up @@ -231,7 +246,7 @@ program
console.log(chalk.blue(` task-manager-cli list -p ${projectId}`));

} catch (error) {
console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`));
console.error(chalk.red(formatCliError(normalizeError(error))));
process.exit(1);
}
});
Expand Down Expand Up @@ -366,7 +381,7 @@ program
});
}
} catch (error) {
console.error(chalk.red(`An error occurred: ${error instanceof Error ? error.message : String(error)}`));
console.error(chalk.red(formatCliError(normalizeError(error))));
process.exit(1);
}
});
Expand Down
8 changes: 8 additions & 0 deletions src/client/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { StandardError } from "../types/index.js";
/**
* Formats an error message for CLI output
*/
export function formatCliError(error: StandardError): string {
const details = error.details ? `: ${JSON.stringify(error.details)}` : '';
return `[${error.code}] ${error.message}${details}`;
}
Loading