@mseep/atlas-mcp-server
Version:
A Model Context Protocol (MCP) server for ATLAS, a Neo4j-powered task management system for LLM Agents - implementing a three-tier architecture (Projects, Tasks, Knowledge) to manage complex workflows.
347 lines (346 loc) • 14.4 kB
JavaScript
import { ProjectService } from "../../../services/neo4j/projectService.js";
import { TaskService } from "../../../services/neo4j/taskService.js";
import { BaseErrorCode, McpError, ProjectErrorCode, TaskErrorCode } from "../../../types/errors.js";
import { PriorityLevel, TaskStatus } from "../../../types/mcp.js";
import { logger } from "../../../utils/logger.js";
import { ResourceTemplates, ResourceURIs, toTaskResource } from "../types.js";
/**
* Register Task Resources
*
* This function registers resource endpoints for the Tasks entity
* - GET atlas://tasks - List all tasks
* - GET atlas://tasks/{taskId} - Get specific task by ID
* - GET atlas://projects/{projectId}/tasks - List tasks for a specific project
*
* @param server The MCP server instance
*/
export function registerTaskResources(server) {
// List all tasks
server.resource("tasks-list", ResourceURIs.TASKS, {
name: "All Tasks",
description: "List of all tasks in the Atlas platform with pagination and filtering support",
mimeType: "application/json"
}, async (uri) => {
try {
logger.info("Listing all tasks", { uri: uri.href });
// Parse query parameters
const queryParams = new URLSearchParams(uri.search);
// Default project ID required by task service
const projectId = queryParams.get("projectId") || "*";
const filters = {
projectId
};
// Parse status parameter using TaskStatus enum
const status = queryParams.get("status");
if (status) {
switch (status) {
case TaskStatus.BACKLOG:
filters.status = 'backlog';
break;
case TaskStatus.TODO:
filters.status = 'todo';
break;
case TaskStatus.IN_PROGRESS:
filters.status = 'in-progress';
break;
case TaskStatus.COMPLETED:
filters.status = 'completed';
break;
default:
logger.warn(`Invalid status value: ${status}, ignoring filter`);
}
}
// Parse priority parameter using PriorityLevel enum
const priority = queryParams.get("priority");
if (priority) {
switch (priority) {
case PriorityLevel.LOW:
filters.priority = 'low';
break;
case PriorityLevel.MEDIUM:
filters.priority = 'medium';
break;
case PriorityLevel.HIGH:
filters.priority = 'high';
break;
case PriorityLevel.CRITICAL:
filters.priority = 'critical';
break;
default:
logger.warn(`Invalid priority value: ${priority}, ignoring filter`);
}
}
// Parse assignedTo parameter
const assignedTo = queryParams.get("assignedTo");
if (assignedTo) {
filters.assignedTo = String(assignedTo);
}
// Parse taskType parameter
const taskType = queryParams.get("taskType");
if (taskType) {
filters.taskType = String(taskType);
}
// Parse tags parameter
const tags = queryParams.get("tags");
if (tags) {
// Split comma-separated tags
filters.tags = String(tags).split(',').map(tag => tag.trim());
}
// Parse sort parameters
const sortBy = queryParams.get("sortBy");
if (sortBy) {
// Validate sortBy value
const validSortByValues = ['priority', 'createdAt', 'status'];
if (validSortByValues.includes(sortBy)) {
filters.sortBy = sortBy;
}
else {
logger.warn(`Invalid sortBy value: ${sortBy}, using default sorting`);
}
}
const sortDirection = queryParams.get("sortDirection");
if (sortDirection) {
// Validate sortDirection value
const validDirections = ['asc', 'desc'];
if (validDirections.includes(sortDirection)) {
filters.sortDirection = sortDirection;
}
else {
logger.warn(`Invalid sortDirection value: ${sortDirection}, using default direction`);
}
}
// Parse pagination parameters
const page = queryParams.has("page")
? parseInt(queryParams.get("page") || "1", 10)
: 1;
const limit = queryParams.has("limit")
? parseInt(queryParams.get("limit") || "20", 10)
: 20;
// Add pagination to filters
filters.page = page;
filters.limit = limit;
// Query the database
const result = await TaskService.getTasks(filters);
// Map Neo4j tasks to resource objects
const taskResources = result.data.map(task => toTaskResource(task));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
tasks: taskResources,
pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages
}
}, null, 2)
}
]
};
}
catch (error) {
logger.error("Error listing tasks", {
error,
uri: uri.href
});
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to list tasks: ${error instanceof Error ? error.message : String(error)}`);
}
});
// Get task by ID
server.resource("task-by-id", ResourceTemplates.TASK, {
name: "Task by ID",
description: "Retrieves a single task by its unique identifier",
mimeType: "application/json"
}, async (uri, params) => {
try {
const taskId = params.taskId;
logger.info("Fetching task by ID", {
taskId,
uri: uri.href
});
if (!taskId) {
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Task ID is required");
}
// Query the database
const task = await TaskService.getTaskById(taskId);
if (!task) {
throw new McpError(TaskErrorCode.TASK_NOT_FOUND, `Task with ID ${taskId} not found`, { taskId });
}
// Convert to resource object
const taskResource = toTaskResource(task);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(taskResource, null, 2)
}
]
};
}
catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Error fetching task by ID", {
error,
params
});
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to fetch task: ${error instanceof Error ? error.message : String(error)}`);
}
});
// List tasks by project
server.resource("tasks-by-project", ResourceTemplates.TASKS_BY_PROJECT, {
name: "Tasks by Project",
description: "Retrieves all tasks belonging to a specific project",
mimeType: "application/json"
}, async (uri, params) => {
try {
const projectId = params.projectId;
logger.info("Listing tasks for project", {
projectId,
uri: uri.href
});
if (!projectId) {
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Project ID is required");
}
// Verify the project exists
const project = await ProjectService.getProjectById(projectId);
if (!project) {
throw new McpError(ProjectErrorCode.PROJECT_NOT_FOUND, `Project with ID ${projectId} not found`, { projectId });
}
// Parse query parameters
const queryParams = new URLSearchParams(uri.search);
const filters = {
projectId
};
// Parse status parameter using TaskStatus enum
const status = queryParams.get("status");
if (status) {
switch (status) {
case TaskStatus.BACKLOG:
filters.status = 'backlog';
break;
case TaskStatus.TODO:
filters.status = 'todo';
break;
case TaskStatus.IN_PROGRESS:
filters.status = 'in-progress';
break;
case TaskStatus.COMPLETED:
filters.status = 'completed';
break;
default:
logger.warn(`Invalid status value: ${status}, ignoring filter`);
}
}
// Parse priority parameter using PriorityLevel enum
const priority = queryParams.get("priority");
if (priority) {
switch (priority) {
case PriorityLevel.LOW:
filters.priority = 'low';
break;
case PriorityLevel.MEDIUM:
filters.priority = 'medium';
break;
case PriorityLevel.HIGH:
filters.priority = 'high';
break;
case PriorityLevel.CRITICAL:
filters.priority = 'critical';
break;
default:
logger.warn(`Invalid priority value: ${priority}, ignoring filter`);
}
}
// Parse assignedTo parameter
const assignedTo = queryParams.get("assignedTo");
if (assignedTo) {
filters.assignedTo = String(assignedTo);
}
// Parse taskType parameter
const taskType = queryParams.get("taskType");
if (taskType) {
filters.taskType = String(taskType);
}
// Parse tags parameter
const tags = queryParams.get("tags");
if (tags) {
// Split comma-separated tags
filters.tags = String(tags).split(',').map(tag => tag.trim());
}
// Parse sort parameters
const sortBy = queryParams.get("sortBy");
if (sortBy) {
// Validate sortBy value
const validSortByValues = ['priority', 'createdAt', 'status'];
if (validSortByValues.includes(sortBy)) {
filters.sortBy = sortBy;
}
else {
logger.warn(`Invalid sortBy value: ${sortBy}, using default sorting`);
}
}
const sortDirection = queryParams.get("sortDirection");
if (sortDirection) {
// Validate sortDirection value
const validDirections = ['asc', 'desc'];
if (validDirections.includes(sortDirection)) {
filters.sortDirection = sortDirection;
}
else {
logger.warn(`Invalid sortDirection value: ${sortDirection}, using default direction`);
}
}
// Parse pagination parameters
const page = queryParams.has("page")
? parseInt(queryParams.get("page") || "1", 10)
: 1;
const limit = queryParams.has("limit")
? parseInt(queryParams.get("limit") || "20", 10)
: 20;
// Add pagination to filters
filters.page = page;
filters.limit = limit;
// Query the database
const result = await TaskService.getTasks(filters);
// Map Neo4j tasks to resource objects
const taskResources = result.data.map(task => toTaskResource(task));
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
projectId,
projectName: project.name,
tasks: taskResources,
pagination: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages
}
}, null, 2)
}
]
};
}
catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Error listing tasks for project", {
error,
params
});
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to list tasks for project: ${error instanceof Error ? error.message : String(error)}`);
}
});
}