atlas-mcp-server
Version:
ATLAS (Adaptive Task & Logic Automation System): An MCP server enabling LLM agents to manage projects, tasks, and knowledge via a Neo4j-backed, three-tier architecture. Facilitates complex workflow automation and project management through LLM Agents.
197 lines (196 loc) • 11.9 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, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import { sanitization } from "../../../utils/security/sanitization.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) {
const reqContext = requestContextService.createRequestContext({
operation: "deepResearch",
projectId: input.projectId,
researchTopic: input.researchTopic,
});
logger.info(`Initiating deep research plan creation for project ID: ${input.projectId}, Topic: "${input.researchTopic}"`, reqContext);
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}`, reqContext);
// 2. Prepare Root Research Plan Node Data
const planNodeId = input.planNodeId || generateKnowledgeId("plan");
const rootTextParts = [
`Research Plan: ${sanitization.sanitizeString(input.researchTopic)}`,
`Goal: ${sanitization.sanitizeString(input.researchGoal)}`,
];
if (input.scopeDefinition) {
rootTextParts.push(`Scope: ${sanitization.sanitizeString(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:${sanitization
.sanitizeString(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}`, { ...reqContext, 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.`, { ...reqContext, planNodeId });
// 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" : ""}.`, {
...reqContext,
subTopicCount: input.subTopics.length,
willCreateTasks: tasksToCreate,
});
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) => sanitization.sanitizeString(kw))
.join(", ");
// Construct the text content for the sub-topic node
const subTopicText = `Research Question: ${sanitization.sanitizeString(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:${sanitization
.sanitizeString(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}"`, { ...reqContext, subTopicNodeId, 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");
logger.info(`Sub-topic node ${subTopicNodeId} created and linked to plan ${planNodeId}.`, { ...reqContext, subTopicNodeId, parentPlanNodeId: planNodeId });
// Create Task if requested
if (tasksToCreate) {
logger.debug(`Creating task for sub-topic node ${subTopicNodeId}`, {
...reqContext,
subTopicNodeId,
});
const taskTitle = `Research: ${sanitization.sanitizeString(subTopic.question)}`;
const taskDescription = `Investigate the research question: "${sanitization.sanitizeString(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}.`, { ...reqContext, createdTaskId, subTopicNodeId });
// Link Task to the Sub-Topic Knowledge Node
await TaskService.linkTaskToKnowledge(createdTaskId, subTopicNodeId, "ADDRESSES");
logger.debug(`Linked task ${createdTaskId} to knowledge node ${subTopicNodeId} with ADDRESSES relationship.`, { ...reqContext, createdTaskId, subTopicNodeId });
}
// 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, {
...reqContext,
planNodeId,
subTopicNodeCount: createdSubTopicNodes.length,
tasksCreatedCount: tasksToCreate ? createdSubTopicNodes.length : 0,
});
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
const errorContextDetails = {
// errorMessage is part of the error object passed to logger.error
// stack is part of the error object passed to logger.error
projectId: input.projectId, // Already in reqContext
researchTopic: input.researchTopic, // Already in reqContext
};
logger.error("Error occurred during deep research plan creation", error, { ...reqContext, ...errorContextDetails });
// 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)}`);
}
}