#!/usr/bin/env node
/**
* MCP Server for Task Management.
*
* This server provides tools to manage tasks, including creating, completing,
* and deleting tasks. Tasks are also exposed as resources.
*
* Usage: bun run src/index.ts --service <memory>
*/
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import type { TaskService } from "./services/TaskService.js";
import { InMemoryTaskService } from "./services/InMemoryTaskService.js";
import {
CreateTaskInputSchema,
CompleteTaskInputSchema,
DeleteTaskInputSchema,
ListTasksInputSchema,
TaskStatus,
ResponseFormat,
type Task,
type CreateTaskInput,
type CompleteTaskInput,
type DeleteTaskInput,
type ListTasksInput,
} from "./types.js";
// Available task service implementations
const SERVICE_IMPLEMENTATIONS: Record<string, () => TaskService> = {
memory: () => new InMemoryTaskService(),
};
/**
* Parse CLI arguments to determine which task service to use
*/
function parseArgs(): { service: string } {
const args = process.argv.slice(2);
let service = "memory"; // default
for (let i = 0; i < args.length; i++) {
if (args[i] === "--service" && args[i + 1]) {
service = args[i + 1];
i++;
} else if (args[i] === "--help" || args[i] === "-h") {
console.error(`
Tasks MCP Server
Usage: tasks-mcp-server [options]
Options:
--service <type> Task service implementation (default: memory)
Available: ${Object.keys(SERVICE_IMPLEMENTATIONS).join(", ")}
--help, -h Show this help message
Examples:
tasks-mcp-server --service memory
`);
process.exit(0);
}
}
return { service };
}
/**
* Create the task service based on CLI arguments
*/
function createTaskService(serviceType: string): TaskService {
const factory = SERVICE_IMPLEMENTATIONS[serviceType];
if (!factory) {
console.error(
`Error: Unknown service type '${serviceType}'. Available: ${Object.keys(SERVICE_IMPLEMENTATIONS).join(", ")}`
);
process.exit(1);
}
return factory();
}
/**
* Format a task for display
*/
function formatTask(task: Task): string {
const status = task.status === TaskStatus.COMPLETED ? "[x]" : "[ ]";
const desc = task.description ? `\n ${task.description}` : "";
const completed = task.completedAt
? `\n Completed: ${task.completedAt.toISOString()}`
: "";
return `${status} ${task.title} (${task.id})${desc}${completed}`;
}
/**
* Format tasks list as markdown
*/
function formatTasksMarkdown(tasks: Task[]): string {
if (tasks.length === 0) {
return "No tasks found.";
}
const pending = tasks.filter((t) => t.status === TaskStatus.PENDING);
const completed = tasks.filter((t) => t.status === TaskStatus.COMPLETED);
const lines: string[] = ["# Tasks", ""];
if (pending.length > 0) {
lines.push("## Pending", "");
pending.forEach((t) => lines.push(formatTask(t), ""));
}
if (completed.length > 0) {
lines.push("## Completed", "");
completed.forEach((t) => lines.push(formatTask(t), ""));
}
return lines.join("\n");
}
/**
* Main function to run the MCP server
*/
async function main() {
const { service: serviceType } = parseArgs();
const taskService = createTaskService(serviceType);
console.error(`Starting Tasks MCP Server with ${serviceType} service...`);
// Create MCP server
const server = new McpServer({
name: "tasks-mcp-server",
version: "1.0.0",
});
// Register tool: tasks_list
server.registerTool(
"tasks_list",
{
title: "List Tasks",
description: `List all tasks, optionally filtered by status.
Args:
- status (optional): Filter by 'pending' or 'completed'
- response_format: Output format ('markdown' or 'json', default: 'markdown')
Returns:
List of tasks with their IDs, titles, descriptions, and status.`,
inputSchema: ListTasksInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
async (params: ListTasksInput) => {
const tasks = await taskService.list(params.status);
if (params.response_format === ResponseFormat.JSON) {
return {
content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
};
}
return {
content: [{ type: "text", text: formatTasksMarkdown(tasks) }],
};
}
);
// Register tool: tasks_create
server.registerTool(
"tasks_create",
{
title: "Create Task",
description: `Create a new task.
Args:
- title (required): Task title (1-200 characters)
- description (optional): Task description (up to 1000 characters)
Returns:
The created task with its assigned ID.`,
inputSchema: CreateTaskInputSchema,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
},
},
async (params: CreateTaskInput) => {
const task = await taskService.create(params);
return {
content: [
{
type: "text",
text: `Task created successfully:\n\n${formatTask(task)}`,
},
],
};
}
);
// Register tool: tasks_complete
server.registerTool(
"tasks_complete",
{
title: "Complete Task",
description: `Mark a task as completed.
Args:
- id (required): Task ID to complete
Returns:
The updated task, or an error if the task was not found.`,
inputSchema: CompleteTaskInputSchema,
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
async (params: CompleteTaskInput) => {
const task = await taskService.complete(params.id);
if (!task) {
return {
isError: true,
content: [
{
type: "text",
text: `Error: Task with ID '${params.id}' not found.`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Task completed successfully:\n\n${formatTask(task)}`,
},
],
};
}
);
// Register tool: tasks_delete
server.registerTool(
"tasks_delete",
{
title: "Delete Task",
description: `Delete a task permanently.
Args:
- id (required): Task ID to delete
Returns:
Confirmation message, or an error if the task was not found.`,
inputSchema: DeleteTaskInputSchema,
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
},
},
async (params: DeleteTaskInput) => {
const deleted = await taskService.delete(params.id);
if (!deleted) {
return {
isError: true,
content: [
{
type: "text",
text: `Error: Task with ID '${params.id}' not found.`,
},
],
};
}
return {
content: [
{
type: "text",
text: `Task '${params.id}' deleted successfully.`,
},
],
};
}
);
// Resource template for individual tasks
const taskTemplate = new ResourceTemplate("task://{id}", {
list: async () => {
const tasks = await taskService.list();
return {
resources: tasks.map((task) => ({
uri: `task://${task.id}`,
name: task.title,
description: task.description,
mimeType: "application/json",
})),
};
},
});
// Register resource template for individual tasks
server.registerResource(
"Task",
taskTemplate,
{ description: "Access a specific task by its ID", mimeType: "application/json" },
async (uri: URL, variables) => {
const id = Array.isArray(variables.id) ? variables.id[0] : variables.id;
const task = await taskService.get(id);
if (!task) {
throw new Error(`Task with ID '${id}' not found`);
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(task, null, 2),
},
],
};
}
);
// Register static resource for listing all tasks
server.registerResource(
"All Tasks",
"task://list",
{ description: "List of all tasks", mimeType: "application/json" },
async (uri: URL) => {
const tasks = await taskService.list();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(tasks, null, 2),
},
],
};
}
);
// Connect via stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Tasks MCP Server running via stdio");
}
// Run the server
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});