@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.
181 lines (180 loc) • 10.8 kB
JavaScript
import { nanoid } from 'nanoid';
import { KnowledgeService } from '../../../services/neo4j/knowledgeService.js';
import { ProjectService } from '../../../services/neo4j/projectService.js';
import { TaskService } from '../../../services/neo4j/taskService.js'; // Import TaskService
import { BaseErrorCode, McpError } from '../../../types/errors.js';
import { logger } from '../../../utils/logger.js';
import { sanitizeInput } from '../../../utils/security.js';
/**
* Generates a unique ID suitable for knowledge nodes using nanoid.
* Includes a prefix for better identification (e.g., 'plan', 'sub').
*
* @param prefix - The prefix to use for the ID (defaults to 'knw').
* @returns A unique ID string (e.g., 'plan_aBcDeFgHiJkL').
*/
function generateKnowledgeId(prefix = "knw") {
return `${prefix}_${nanoid(12)}`; // Using 12 characters for increased uniqueness
}
/**
* Core implementation logic for the `atlas_deep_research` tool.
* This function orchestrates the creation of a hierarchical knowledge structure
* in Neo4j to represent a research plan based on the provided input.
* It creates a root node for the overall plan and child nodes for each sub-topic.
*
* @param input - The validated input object conforming to `AtlasDeepResearchInput`.
* @returns A promise resolving to a `DeepResearchResult` object containing details
* about the created nodes/tasks and the operation's success status.
* @throws {McpError} If the project ID is invalid, or if any database operation fails.
*/
export async function deepResearch(input) {
logger.info(`Initiating deep research plan creation for project ID: ${input.projectId}, Topic: "${input.researchTopic}"`);
try {
// 1. Validate Project Existence
// Ensure the specified project exists before proceeding.
const project = await ProjectService.getProjectById(input.projectId);
if (!project) {
throw new McpError(BaseErrorCode.NOT_FOUND, `Project with ID "${input.projectId}" not found. Cannot create research plan.`);
}
logger.debug(`Project validation successful for ID: ${input.projectId}`);
// 2. Prepare Root Research Plan Node Data
const planNodeId = input.planNodeId || generateKnowledgeId('plan');
const rootTextParts = [
`Research Plan: ${sanitizeInput.string(input.researchTopic)}`,
`Goal: ${sanitizeInput.string(input.researchGoal)}`,
];
if (input.scopeDefinition) {
rootTextParts.push(`Scope: ${sanitizeInput.string(input.scopeDefinition)}`);
}
const rootText = rootTextParts.join('\n\n'); // Combine parts into the main text content
// Define tags for the root node
const rootTags = [
'research-plan',
'research-root',
'status:active', // Initialize the plan as active
`topic:${sanitizeInput
.string(input.researchTopic)
.toLowerCase()
.replace(/\s+/g, '-') // Convert topic to a URL-friendly tag format
.slice(0, 50)}`, // Limit tag length
...(input.initialTags || []), // Include any user-provided initial tags
];
// 3. Create Root Research Plan Node and link to Project
// Assuming KnowledgeService.addKnowledge handles linking if projectId is provided,
// or we might need a specific method like addKnowledgeAndLinkToProject.
// For now, assume addKnowledge creates the node and links it via projectId.
// A more robust approach might involve explicit relationship creation.
logger.debug(`Attempting to create root research plan node with ID: ${planNodeId}`);
await KnowledgeService.addKnowledge({
id: planNodeId,
projectId: input.projectId,
text: rootText,
domain: input.researchDomain || 'research',
tags: rootTags,
citations: [],
});
// If explicit linking is needed:
// await KnowledgeService.linkKnowledgeToProject(planNodeId, input.projectId, 'CONTAINS_PLAN');
logger.info(`Root research plan node ${planNodeId} created and associated with project.`);
// 4. Create Knowledge Nodes and Optional Tasks for Each Sub-Topic
const createdSubTopicNodes = [];
const tasksToCreate = input.createTasks ?? true; // Default to true if not specified
logger.debug(`Processing ${input.subTopics.length} sub-topics to create knowledge nodes ${tasksToCreate ? 'and tasks' : ''}.`);
for (const subTopic of input.subTopics) {
const subTopicNodeId = subTopic.nodeId || generateKnowledgeId('sub');
let createdTaskId = undefined;
// Sanitize search queries before joining
const searchQueriesString = (subTopic.initialSearchQueries || [])
.map((kw) => sanitizeInput.string(kw))
.join(', ');
// Construct the text content for the sub-topic node
const subTopicText = `Research Question: ${sanitizeInput.string(subTopic.question)}\n\nInitial Search Queries: ${searchQueriesString || 'None provided'}`;
// Define tags for the sub-topic node
const subTopicTags = [
'research-subtopic',
'status:pending', // Initialize sub-topics as pending
// `parent-plan:${planNodeId}`, // Replaced by relationship if implemented
...(subTopic.initialSearchQueries?.map((kw) => `search-query:${sanitizeInput
.string(kw) // Create tags for each search query
.toLowerCase()
.replace(/\s+/g, '-')
.slice(0, 50)}`) || []),
];
logger.debug(`Attempting to create sub-topic node with ID: ${subTopicNodeId} for question: "${subTopic.question}"`);
// Create the sub-topic knowledge node and link it to the parent plan node
// Assuming addKnowledge links to project, now link to parent knowledge node
await KnowledgeService.addKnowledge({
id: subTopicNodeId,
projectId: input.projectId, // Associate with the same project
text: subTopicText,
domain: input.researchDomain || 'research', // Inherit domain from the root plan
tags: subTopicTags,
citations: [], // Sub-topics also start with no citations
});
// Explicitly link sub-topic to parent plan node
await KnowledgeService.linkKnowledgeToKnowledge(subTopicNodeId, planNodeId, 'IS_SUBTOPIC_OF' // Relationship type from child to parent
);
logger.info(`Sub-topic node ${subTopicNodeId} created and linked to plan ${planNodeId}.`);
// Create Task if requested
if (tasksToCreate) {
logger.debug(`Creating task for sub-topic node ${subTopicNodeId}`);
const taskTitle = `Research: ${sanitizeInput.string(subTopic.question)}`;
const taskDescription = `Investigate the research question: "${sanitizeInput.string(subTopic.question)}"\n\nInitial Search Queries: ${searchQueriesString || 'None provided'}\n\nAssociated Knowledge Node: ${subTopicNodeId}`;
// Use TaskService to create the task and link it to the project
const taskResult = await TaskService.createTask({
projectId: input.projectId,
title: taskTitle.slice(0, 150), // Ensure title length constraint
description: taskDescription,
priority: subTopic.priority || 'medium',
status: subTopic.initialStatus || 'todo',
assignedTo: subTopic.assignedTo,
completionRequirements: `Gather relevant information and synthesize findings related to the research question. Update associated knowledge node ${subTopicNodeId}.`,
outputFormat: 'Update to knowledge node, potentially new linked knowledge items.',
taskType: 'research', // Specific task type
// tags: [`research-task`, `plan:${planNodeId}`], // Optional tags for the task
});
createdTaskId = taskResult.id;
logger.info(`Task ${createdTaskId} created for sub-topic ${subTopicNodeId}.`);
// Link Task to the Sub-Topic Knowledge Node
await TaskService.linkTaskToKnowledge(createdTaskId, subTopicNodeId, 'ADDRESSES' // Relationship: Task ADDRESSES Knowledge Node
);
logger.debug(`Linked task ${createdTaskId} to knowledge node ${subTopicNodeId} with ADDRESSES relationship.`);
}
// Record the details of the created sub-topic node and task
createdSubTopicNodes.push({
question: subTopic.question,
nodeId: subTopicNodeId,
taskId: createdTaskId, // Include task ID if created
initialSearchQueries: subTopic.initialSearchQueries || [],
});
}
// 5. Assemble and Return the Result
const taskMessage = tasksToCreate
? `and ${createdSubTopicNodes.length} associated tasks`
: '';
const successMessage = `Successfully created deep research plan "${input.researchTopic}" with root research plan node ${planNodeId}, ${createdSubTopicNodes.length} sub-topic nodes ${taskMessage}.`;
logger.info(successMessage);
return {
success: true,
message: successMessage,
planNodeId: planNodeId,
initialTags: input.initialTags || [], // Return the initial tags applied to the root
subTopicNodes: createdSubTopicNodes, // Return details of created sub-topic nodes and tasks
tasksCreated: tasksToCreate, // Indicate if tasks were created
};
}
catch (error) {
// Log the error with context
logger.error("Error occurred during deep research plan creation", {
errorMessage: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
projectId: input.projectId,
researchTopic: input.researchTopic,
});
// Re-throw McpError instances directly
if (error instanceof McpError) {
throw error;
}
// Wrap unexpected errors in a generic McpError
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to create deep research plan (Project: ${input.projectId}, Topic: "${input.researchTopic}"): ${error instanceof Error ? error.message : String(error)}`);
}
}