This is page 2 of 6. Use http://codebase.md/cyanheads/atlas-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .clinerules
├── .dockerignore
├── .env.example
├── .github
│ ├── FUNDING.yml
│ └── workflows
│ └── publish.yml
├── .gitignore
├── .ncurc.json
├── .repomixignore
├── automated-tests
│ └── AGENT_TEST_05282025.md
├── CHANGELOG.md
├── CLAUDE.md
├── docker-compose.yml
├── docs
│ └── tree.md
├── examples
│ ├── backup-example
│ │ ├── knowledges.json
│ │ ├── projects.json
│ │ ├── relationships.json
│ │ └── tasks.json
│ ├── deep-research-example
│ │ ├── covington_community_grant_research.md
│ │ └── full-export.json
│ ├── README.md
│ └── webui-example.png
├── LICENSE
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── repomix.config.json
├── scripts
│ ├── clean.ts
│ ├── fetch-openapi-spec.ts
│ ├── make-executable.ts
│ └── tree.ts
├── smithery.yaml
├── src
│ ├── config
│ │ └── index.ts
│ ├── index.ts
│ ├── mcp
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── knowledge
│ │ │ │ └── knowledgeResources.ts
│ │ │ ├── projects
│ │ │ │ └── projectResources.ts
│ │ │ ├── tasks
│ │ │ │ └── taskResources.ts
│ │ │ └── types.ts
│ │ ├── server.ts
│ │ ├── tools
│ │ │ ├── atlas_database_clean
│ │ │ │ ├── cleanDatabase.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_deep_research
│ │ │ │ ├── deepResearch.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_knowledge_add
│ │ │ │ ├── addKnowledge.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_knowledge_delete
│ │ │ │ ├── deleteKnowledge.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_knowledge_list
│ │ │ │ ├── index.ts
│ │ │ │ ├── listKnowledge.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_create
│ │ │ │ ├── createProject.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_delete
│ │ │ │ ├── deleteProject.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_list
│ │ │ │ ├── index.ts
│ │ │ │ ├── listProjects.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_project_update
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── updateProject.ts
│ │ │ ├── atlas_task_create
│ │ │ │ ├── createTask.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_task_delete
│ │ │ │ ├── deleteTask.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_task_list
│ │ │ │ ├── index.ts
│ │ │ │ ├── listTasks.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ └── types.ts
│ │ │ ├── atlas_task_update
│ │ │ │ ├── index.ts
│ │ │ │ ├── responseFormat.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── updateTask.ts
│ │ │ └── atlas_unified_search
│ │ │ ├── index.ts
│ │ │ ├── responseFormat.ts
│ │ │ ├── types.ts
│ │ │ └── unifiedSearch.ts
│ │ └── transports
│ │ ├── authentication
│ │ │ └── authMiddleware.ts
│ │ ├── httpTransport.ts
│ │ └── stdioTransport.ts
│ ├── services
│ │ └── neo4j
│ │ ├── backupRestoreService
│ │ │ ├── backupRestoreTypes.ts
│ │ │ ├── backupUtils.ts
│ │ │ ├── exportLogic.ts
│ │ │ ├── importLogic.ts
│ │ │ ├── index.ts
│ │ │ └── scripts
│ │ │ ├── db-backup.ts
│ │ │ └── db-import.ts
│ │ ├── driver.ts
│ │ ├── events.ts
│ │ ├── helpers.ts
│ │ ├── index.ts
│ │ ├── knowledgeService.ts
│ │ ├── projectService.ts
│ │ ├── searchService
│ │ │ ├── fullTextSearchLogic.ts
│ │ │ ├── index.ts
│ │ │ ├── searchTypes.ts
│ │ │ └── unifiedSearchLogic.ts
│ │ ├── taskService.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── types
│ │ ├── errors.ts
│ │ ├── mcp.ts
│ │ └── tool.ts
│ ├── utils
│ │ ├── index.ts
│ │ ├── internal
│ │ │ ├── errorHandler.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ └── requestContext.ts
│ │ ├── metrics
│ │ │ ├── index.ts
│ │ │ └── tokenCounter.ts
│ │ ├── parsing
│ │ │ ├── dateParser.ts
│ │ │ ├── index.ts
│ │ │ └── jsonParser.ts
│ │ └── security
│ │ ├── idGenerator.ts
│ │ ├── index.ts
│ │ ├── rateLimiter.ts
│ │ └── sanitization.ts
│ └── webui
│ ├── index.html
│ ├── logic
│ │ ├── api-service.js
│ │ ├── app-state.js
│ │ ├── config.js
│ │ ├── dom-elements.js
│ │ ├── main.js
│ │ └── ui-service.js
│ └── styling
│ ├── base.css
│ ├── components.css
│ ├── layout.css
│ └── theme.css
├── tsconfig.json
├── tsconfig.typedoc.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_delete/deleteProject.ts:
--------------------------------------------------------------------------------
```typescript
import { ProjectService } from "../../../services/neo4j/projectService.js";
import {
BaseErrorCode,
McpError,
ProjectErrorCode,
} from "../../../types/errors.js";
import { ResponseFormat, createToolResponse } from "../../../types/mcp.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import { ToolContext } from "../../../types/tool.js";
import { AtlasProjectDeleteInput, AtlasProjectDeleteSchema } from "./types.js";
import { formatProjectDeleteResponse } from "./responseFormat.js";
export const atlasDeleteProject = async (
input: unknown,
context: ToolContext,
) => {
let validatedInput: AtlasProjectDeleteInput | undefined;
const reqContext =
context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasDeleteProject",
});
try {
// Parse and validate input against schema definition
validatedInput = AtlasProjectDeleteSchema.parse(input);
// Select operation strategy based on request mode
if (validatedInput.mode === "bulk") {
// Process bulk removal operation
const { projectIds } = validatedInput;
logger.info("Initiating batch project removal", {
...reqContext,
count: projectIds.length,
projectIds,
});
const results = {
success: true,
message: `Successfully removed ${projectIds.length} projects`,
deleted: [] as string[],
errors: [] as {
projectId: string;
error: {
code: string;
message: string;
details?: any;
};
}[],
};
// Process removal operations sequentially to maintain data integrity
for (const projectId of projectIds) {
try {
const deleted = await ProjectService.deleteProject(projectId);
if (deleted) {
results.deleted.push(projectId);
} else {
// Project not found
results.success = false;
results.errors.push({
projectId,
error: {
code: ProjectErrorCode.PROJECT_NOT_FOUND,
message: `Project with ID ${projectId} not found`,
},
});
}
} catch (error) {
results.success = false;
results.errors.push({
projectId,
error: {
code:
error instanceof McpError
? error.code
: BaseErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "Unknown error",
details: error instanceof McpError ? error.details : undefined,
},
});
}
}
if (results.errors.length > 0) {
results.message = `Removed ${results.deleted.length} of ${projectIds.length} projects with ${results.errors.length} errors`;
}
logger.info("Batch removal operation completed", {
...reqContext,
successCount: results.deleted.length,
errorCount: results.errors.length,
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(results, null, 2));
} else {
return formatProjectDeleteResponse(results);
}
} else {
// Process single entity removal
const { id } = validatedInput;
logger.info("Removing project entity", {
...reqContext,
projectId: id,
});
const deleted = await ProjectService.deleteProject(id);
if (!deleted) {
logger.warning("Target project not found for removal operation", {
...reqContext,
projectId: id,
});
throw new McpError(
ProjectErrorCode.PROJECT_NOT_FOUND,
`Project with identifier ${id} not found`,
{ projectId: id },
);
}
logger.info("Project successfully removed", {
...reqContext,
projectId: id,
});
const result = {
id,
success: true,
message: `Project with ID ${id} removed successfully`,
};
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(result, null, 2));
} else {
return formatProjectDeleteResponse(result);
}
}
} catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Project removal operation failed", error as Error, {
...reqContext,
inputReceived: validatedInput ?? input,
});
// Translate unknown errors to structured McpError format
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Failed to remove project(s): ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
};
```
--------------------------------------------------------------------------------
/src/utils/metrics/tokenCounter.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Provides utility functions for counting tokens in text and chat messages
* using the `tiktoken` library, specifically configured for 'gpt-4o' tokenization.
* These functions are essential for managing token limits and estimating costs
* when interacting with language models.
* @module src/utils/metrics/tokenCounter
*/
import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { encoding_for_model, Tiktoken, TiktokenModel } from "tiktoken";
import { BaseErrorCode, McpError } from "../../types/errors.js";
import { ErrorHandler, logger, RequestContext } from "../index.js";
/**
* The specific Tiktoken model used for all tokenization operations in this module.
* This ensures consistent token counting.
* @private
*/
const TOKENIZATION_MODEL: TiktokenModel = "gpt-4o";
/**
* Calculates the number of tokens for a given text string using the
* tokenizer specified by `TOKENIZATION_MODEL`.
* Wraps tokenization in `ErrorHandler.tryCatch` for robust error management.
*
* @param text - The input text to tokenize.
* @param context - Optional request context for logging and error handling.
* @returns A promise that resolves with the number of tokens in the text.
* @throws {McpError} If tokenization fails.
*/
export async function countTokens(
text: string,
context?: RequestContext,
): Promise<number> {
return ErrorHandler.tryCatch(
() => {
let encoding: Tiktoken | null = null;
try {
encoding = encoding_for_model(TOKENIZATION_MODEL);
const tokens = encoding.encode(text);
return tokens.length;
} finally {
encoding?.free();
}
},
{
operation: "countTokens",
context: context,
input: { textSample: text.substring(0, 50) + "..." },
errorCode: BaseErrorCode.INTERNAL_ERROR,
},
);
}
/**
* Calculates the estimated number of tokens for an array of chat messages.
* Uses the tokenizer specified by `TOKENIZATION_MODEL` and accounts for
* special tokens and message overhead according to OpenAI's guidelines.
*
* For multi-part content, only text parts are currently tokenized.
*
* Reference: {@link https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb}
*
* @param messages - An array of chat messages.
* @param context - Optional request context for logging and error handling.
* @returns A promise that resolves with the estimated total number of tokens.
* @throws {McpError} If tokenization fails.
*/
export async function countChatTokens(
messages: ReadonlyArray<ChatCompletionMessageParam>,
context?: RequestContext,
): Promise<number> {
return ErrorHandler.tryCatch(
() => {
let encoding: Tiktoken | null = null;
let num_tokens = 0;
try {
encoding = encoding_for_model(TOKENIZATION_MODEL);
const tokens_per_message = 3; // For gpt-4o, gpt-4, gpt-3.5-turbo
const tokens_per_name = 1; // For gpt-4o, gpt-4, gpt-3.5-turbo
for (const message of messages) {
num_tokens += tokens_per_message;
num_tokens += encoding.encode(message.role).length;
if (typeof message.content === "string") {
num_tokens += encoding.encode(message.content).length;
} else if (Array.isArray(message.content)) {
for (const part of message.content) {
if (part.type === "text") {
num_tokens += encoding.encode(part.text).length;
} else {
logger.warning(
`Non-text content part found (type: ${part.type}), token count contribution ignored.`,
context,
);
}
}
}
if ("name" in message && message.name) {
num_tokens += tokens_per_name;
num_tokens += encoding.encode(message.name).length;
}
if (
message.role === "assistant" &&
"tool_calls" in message &&
message.tool_calls
) {
for (const tool_call of message.tool_calls) {
if (tool_call.function.name) {
num_tokens += encoding.encode(tool_call.function.name).length;
}
if (tool_call.function.arguments) {
num_tokens += encoding.encode(
tool_call.function.arguments,
).length;
}
}
}
if (
message.role === "tool" &&
"tool_call_id" in message &&
message.tool_call_id
) {
num_tokens += encoding.encode(message.tool_call_id).length;
}
}
num_tokens += 3; // Every reply is primed with <|start|>assistant<|message|>
return num_tokens;
} finally {
encoding?.free();
}
},
{
operation: "countChatTokens",
context: context,
input: { messageCount: messages.length },
errorCode: BaseErrorCode.INTERNAL_ERROR,
},
);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_delete/deleteKnowledge.ts:
--------------------------------------------------------------------------------
```typescript
import { KnowledgeService } from "../../../services/neo4j/knowledgeService.js";
import { BaseErrorCode, McpError } from "../../../types/errors.js";
import { ResponseFormat, createToolResponse } from "../../../types/mcp.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import { ToolContext } from "../../../types/tool.js";
import {
AtlasKnowledgeDeleteInput,
AtlasKnowledgeDeleteSchema,
} from "./types.js";
import { formatKnowledgeDeleteResponse } from "./responseFormat.js";
export const atlasDeleteKnowledge = async (
input: unknown,
context: ToolContext,
) => {
let validatedInput: AtlasKnowledgeDeleteInput | undefined;
const reqContext =
context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasDeleteKnowledge",
});
try {
// Parse and validate input against schema definition
validatedInput = AtlasKnowledgeDeleteSchema.parse(input);
// Select operation strategy based on request mode
if (validatedInput.mode === "bulk") {
// Process bulk removal operation
const { knowledgeIds } = validatedInput;
logger.info("Initiating batch knowledge item removal", {
...reqContext,
count: knowledgeIds.length,
knowledgeIds,
});
const results = {
success: true,
message: `Successfully removed ${knowledgeIds.length} knowledge items`,
deleted: [] as string[],
errors: [] as {
knowledgeId: string;
error: {
code: string;
message: string;
details?: any;
};
}[],
};
// Process removal operations sequentially to maintain data integrity
for (const knowledgeId of knowledgeIds) {
try {
const deleted = await KnowledgeService.deleteKnowledge(knowledgeId);
if (deleted) {
results.deleted.push(knowledgeId);
} else {
// Knowledge item not found
results.success = false;
results.errors.push({
knowledgeId,
error: {
code: BaseErrorCode.NOT_FOUND,
message: `Knowledge item with ID ${knowledgeId} not found`,
},
});
}
} catch (error) {
results.success = false;
results.errors.push({
knowledgeId,
error: {
code:
error instanceof McpError
? error.code
: BaseErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "Unknown error",
details: error instanceof McpError ? error.details : undefined,
},
});
}
}
if (results.errors.length > 0) {
results.message = `Removed ${results.deleted.length} of ${knowledgeIds.length} knowledge items with ${results.errors.length} errors`;
}
logger.info("Batch knowledge removal operation completed", {
...reqContext,
successCount: results.deleted.length,
errorCount: results.errors.length,
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(results, null, 2));
} else {
return formatKnowledgeDeleteResponse(results);
}
} else {
// Process single entity removal
const { id } = validatedInput;
logger.info("Removing knowledge item", {
...reqContext,
knowledgeId: id,
});
const deleted = await KnowledgeService.deleteKnowledge(id);
if (!deleted) {
logger.warning(
"Target knowledge item not found for removal operation",
{
...reqContext,
knowledgeId: id,
},
);
throw new McpError(
BaseErrorCode.NOT_FOUND,
`Knowledge item with identifier ${id} not found`,
{ knowledgeId: id },
);
}
logger.info("Knowledge item successfully removed", {
...reqContext,
knowledgeId: id,
});
const result = {
id,
success: true,
message: `Knowledge item with ID ${id} removed successfully`,
};
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(result, null, 2));
} else {
return formatKnowledgeDeleteResponse(result);
}
}
} catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Knowledge item removal operation failed", error as Error, {
...reqContext,
inputReceived: validatedInput ?? input,
});
// Translate unknown errors to structured McpError format
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Failed to remove knowledge item(s): ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
};
```
--------------------------------------------------------------------------------
/src/utils/parsing/dateParser.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Provides utility functions for parsing natural language date strings
* into Date objects or detailed parsing results using the `chrono-node` library.
* @module src/utils/parsing/dateParser
*/
import * as chrono from "chrono-node";
import { BaseErrorCode, McpError } from "../../types/errors.js";
import { ErrorHandler, logger, RequestContext } from "../index.js";
/**
* Parses a natural language date string into a JavaScript Date object.
* Uses `chrono.parseDate` for lenient parsing of various date formats.
*
* @param text - The natural language date string to parse.
* @param context - The request context for logging and error tracking.
* @param refDate - Optional reference date for parsing relative dates. Defaults to current date/time.
* @returns A promise resolving with a Date object or `null` if parsing fails.
* @throws {McpError} If an unexpected error occurs during parsing.
* @private
*/
async function parseDateString(
text: string,
context: RequestContext,
refDate?: Date,
): Promise<Date | null> {
const operation = "parseDateString";
const logContext = { ...context, operation, inputText: text, refDate };
logger.debug(`Attempting to parse date string: "${text}"`, logContext);
return await ErrorHandler.tryCatch(
async () => {
const parsedDate = chrono.parseDate(text, refDate, { forwardDate: true });
if (parsedDate) {
logger.debug(
`Successfully parsed "${text}" to ${parsedDate.toISOString()}`,
logContext,
);
return parsedDate;
} else {
logger.warning(`Failed to parse date string: "${text}"`, logContext);
return null;
}
},
{
operation,
context: logContext,
input: { text, refDate },
// errorCode: BaseErrorCode.PARSING_ERROR, // PARSING_ERROR is not in BaseErrorCode
errorCode: BaseErrorCode.VALIDATION_ERROR, // Using VALIDATION_ERROR as a substitute
},
);
}
/**
* Parses a natural language date string and returns detailed parsing results.
* Provides more information than just the Date object, including matched text and components.
*
* @param text - The natural language date string to parse.
* @param context - The request context for logging and error tracking.
* @param refDate - Optional reference date for parsing relative dates. Defaults to current date/time.
* @returns A promise resolving with an array of `chrono.ParsedResult` objects. Empty if no dates found.
* @throws {McpError} If an unexpected error occurs during parsing.
* @private
*/
async function parseDateStringDetailed(
text: string,
context: RequestContext,
refDate?: Date,
): Promise<chrono.ParsedResult[]> {
const operation = "parseDateStringDetailed";
const logContext = { ...context, operation, inputText: text, refDate };
logger.debug(
`Attempting detailed parse of date string: "${text}"`,
logContext,
);
return await ErrorHandler.tryCatch(
async () => {
const results = chrono.parse(text, refDate, { forwardDate: true });
logger.debug(
`Detailed parse of "${text}" resulted in ${results.length} result(s)`,
logContext,
);
return results;
},
{
operation,
context: logContext,
input: { text, refDate },
// errorCode: BaseErrorCode.PARSING_ERROR, // PARSING_ERROR is not in BaseErrorCode
errorCode: BaseErrorCode.VALIDATION_ERROR, // Using VALIDATION_ERROR as a substitute
},
);
}
/**
* An object providing date parsing functionalities.
*
* @example
* ```typescript
* import { dateParser, requestContextService } from './utils'; // Assuming utils/index.js exports these
* const context = requestContextService.createRequestContext({ operation: 'TestDateParsing' });
*
* async function testParsing() {
* const dateObj = await dateParser.parseDate("next Friday at 3pm", context);
* if (dateObj) {
* console.log("Parsed Date:", dateObj.toISOString());
* }
*
* const detailedResults = await dateParser.parse("Meeting on 2024-12-25 and another one tomorrow", context);
* detailedResults.forEach(result => {
* console.log("Detailed Result:", result.text, result.start.date());
* });
* }
* testParsing();
* ```
*/
export const dateParser = {
/**
* Parses a natural language date string and returns detailed parsing results
* from `chrono-node`.
* @param text - The natural language date string to parse.
* @param context - The request context for logging and error tracking.
* @param refDate - Optional reference date for parsing relative dates.
* @returns A promise resolving with an array of `chrono.ParsedResult` objects.
*/
parse: parseDateStringDetailed,
/**
* Parses a natural language date string into a single JavaScript Date object.
* @param text - The natural language date string to parse.
* @param context - The request context for logging and error tracking.
* @param refDate - Optional reference date for parsing relative dates.
* @returns A promise resolving with a Date object or `null`.
*/
parseDate: parseDateString,
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_add/addKnowledge.ts:
--------------------------------------------------------------------------------
```typescript
import { KnowledgeService } from "../../../services/neo4j/knowledgeService.js";
import { ProjectService } from "../../../services/neo4j/projectService.js";
import {
BaseErrorCode,
McpError,
ProjectErrorCode,
} from "../../../types/errors.js";
import { ResponseFormat, createToolResponse } from "../../../types/mcp.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import { ToolContext } from "../../../types/tool.js";
import { AtlasKnowledgeAddInput, AtlasKnowledgeAddSchema } from "./types.js";
import { formatKnowledgeAddResponse } from "./responseFormat.js";
export const atlasAddKnowledge = async (
input: unknown,
context: ToolContext,
) => {
let validatedInput: AtlasKnowledgeAddInput | undefined;
const reqContext =
context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasAddKnowledge",
});
try {
// Parse and validate input against schema
validatedInput = AtlasKnowledgeAddSchema.parse(input);
// Handle single vs bulk knowledge addition based on mode
if (validatedInput.mode === "bulk") {
// Execute bulk addition operation
logger.info("Adding multiple knowledge items", {
...reqContext,
count: validatedInput.knowledge.length,
});
const results = {
success: true,
message: `Successfully added ${validatedInput.knowledge.length} knowledge items`,
created: [] as any[],
errors: [] as any[],
};
// Process each knowledge item sequentially
for (let i = 0; i < validatedInput.knowledge.length; i++) {
const knowledgeData = validatedInput.knowledge[i];
try {
const createdKnowledge = await KnowledgeService.addKnowledge({
projectId: knowledgeData.projectId,
text: knowledgeData.text,
tags: knowledgeData.tags || [],
domain: knowledgeData.domain,
citations: knowledgeData.citations || [],
id: knowledgeData.id, // Use client-provided ID if available
});
results.created.push(createdKnowledge);
} catch (error) {
results.success = false;
results.errors.push({
index: i,
knowledge: knowledgeData,
error: {
code:
error instanceof McpError
? error.code
: BaseErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "Unknown error",
details: error instanceof McpError ? error.details : undefined,
},
});
}
}
if (results.errors.length > 0) {
results.message = `Added ${results.created.length} of ${validatedInput.knowledge.length} knowledge items with ${results.errors.length} errors`;
}
logger.info("Bulk knowledge addition completed", {
...reqContext,
successCount: results.created.length,
errorCount: results.errors.length,
knowledgeIds: results.created.map((k) => k.id),
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(results, null, 2));
} else {
return formatKnowledgeAddResponse(results);
}
} else {
// Process single knowledge item addition
const { mode, id, projectId, text, tags, domain, citations } =
validatedInput;
logger.info("Adding new knowledge item", {
...reqContext,
projectId,
domain,
});
const knowledge = await KnowledgeService.addKnowledge({
id, // Use client-provided ID if available
projectId,
text,
tags: tags || [],
domain,
citations: citations || [],
});
logger.info("Knowledge item added successfully", {
...reqContext,
knowledgeId: knowledge.id,
projectId,
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(knowledge, null, 2));
} else {
return formatKnowledgeAddResponse(knowledge);
}
}
} catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Failed to add knowledge item(s)", error as Error, {
...reqContext,
inputReceived: validatedInput ?? input,
});
// Handle project not found error specifically
if (error instanceof Error && error.message.includes("Project with ID")) {
const projectId =
validatedInput?.mode === "single"
? validatedInput?.projectId
: validatedInput?.knowledge?.[0]?.projectId;
throw new McpError(
ProjectErrorCode.PROJECT_NOT_FOUND,
`Project not found: ${projectId}`,
{ projectId },
);
}
// Convert other errors to McpError
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Error adding knowledge item(s): ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_update/updateProject.ts:
--------------------------------------------------------------------------------
```typescript
import { ProjectService } from "../../../services/neo4j/projectService.js";
import {
BaseErrorCode,
McpError,
ProjectErrorCode,
} from "../../../types/errors.js";
import { ResponseFormat, createToolResponse } from "../../../types/mcp.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import { ToolContext } from "../../../types/tool.js";
import { AtlasProjectUpdateInput, AtlasProjectUpdateSchema } from "./types.js";
import { formatProjectUpdateResponse } from "./responseFormat.js";
export const atlasUpdateProject = async (
input: unknown,
context: ToolContext,
) => {
let validatedInput: AtlasProjectUpdateInput | undefined;
const reqContext =
context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasUpdateProject",
});
try {
// Parse and validate the input against schema
validatedInput = AtlasProjectUpdateSchema.parse(input);
// Process according to operation mode (single or bulk)
if (validatedInput.mode === "bulk") {
// Execute bulk update operation
logger.info("Applying updates to multiple projects", {
...reqContext,
count: validatedInput.projects.length,
});
const results = {
success: true,
message: `Successfully updated ${validatedInput.projects.length} projects`,
updated: [] as any[],
errors: [] as any[],
};
// Process each project update sequentially to maintain data consistency
for (let i = 0; i < validatedInput.projects.length; i++) {
const projectUpdate = validatedInput.projects[i];
try {
// First check if project exists
const projectExists = await ProjectService.getProjectById(
projectUpdate.id,
);
if (!projectExists) {
throw new McpError(
ProjectErrorCode.PROJECT_NOT_FOUND,
`Project with ID ${projectUpdate.id} not found`,
);
}
// Update the project
const updatedProject = await ProjectService.updateProject(
projectUpdate.id,
projectUpdate.updates,
);
results.updated.push(updatedProject);
} catch (error) {
results.success = false;
results.errors.push({
index: i,
project: projectUpdate,
error: {
code:
error instanceof McpError
? error.code
: BaseErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "Unknown error",
details: error instanceof McpError ? error.details : undefined,
},
});
}
}
if (results.errors.length > 0) {
results.message = `Updated ${results.updated.length} of ${validatedInput.projects.length} projects with ${results.errors.length} errors`;
}
logger.info("Bulk project modification completed", {
...reqContext,
successCount: results.updated.length,
errorCount: results.errors.length,
projectIds: results.updated.map((p) => p.id),
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(results, null, 2));
} else {
return formatProjectUpdateResponse(results);
}
} else {
// Process single project modification
const { mode, id, updates } = validatedInput;
logger.info("Modifying project attributes", {
...reqContext,
id,
fields: Object.keys(updates),
});
// First check if project exists
const projectExists = await ProjectService.getProjectById(id);
if (!projectExists) {
throw new McpError(
ProjectErrorCode.PROJECT_NOT_FOUND,
`Project with ID ${id} not found`,
);
}
// Update the project
const updatedProject = await ProjectService.updateProject(id, updates);
logger.info("Project modifications applied successfully", {
...reqContext,
projectId: id,
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(updatedProject, null, 2));
} else {
return formatProjectUpdateResponse(updatedProject);
}
}
} catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Failed to modify project(s)", error as Error, {
...reqContext,
inputReceived: validatedInput ?? input,
});
// Handle not found error specifically
if (error instanceof Error && error.message.includes("not found")) {
throw new McpError(
ProjectErrorCode.PROJECT_NOT_FOUND,
`Project not found: ${error.message}`,
);
}
// Convert generic errors to properly formatted McpError
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Failed to modify project(s): ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
};
```
--------------------------------------------------------------------------------
/src/services/neo4j/backupRestoreService/backupUtils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Provides utility functions for the backup and restore service,
* including secure path resolution and backup rotation management.
* @module src/services/neo4j/backupRestoreService/backupUtils
*/
import { existsSync, mkdirSync, readdirSync, rmSync } from "fs";
import { stat } from "fs/promises";
import path from "path";
import { config } from "../../../config/index.js";
import { logger, requestContextService } from "../../../utils/index.js";
// Define the validated root backup path from config
export const validatedBackupRoot = config.backup.backupPath;
/**
* Securely resolves a path against a base directory and ensures it stays within that base.
* @param basePath The absolute, validated base path.
* @param targetPath The relative or absolute path to resolve.
* @returns The resolved absolute path if it's within the base path, otherwise null.
*/
export const secureResolve = (
basePath: string,
targetPath: string,
): string | null => {
const resolvedTarget = path.resolve(basePath, targetPath);
if (
resolvedTarget.startsWith(basePath + path.sep) ||
resolvedTarget === basePath
) {
return resolvedTarget;
}
const errorContext = requestContextService.createRequestContext({
operation: "secureResolve.PathViolation",
targetPath,
resolvedTarget,
basePath,
});
logger.error(
`Security Violation: Path "${targetPath}" resolves to "${resolvedTarget}", which is outside the allowed base directory "${basePath}".`,
new Error("Path security violation"),
errorContext,
);
return null;
};
/**
* Manages backup rotation, deleting the oldest backups if the count exceeds the limit.
*/
export const manageBackupRotation = async (): Promise<void> => {
const maxBackups = config.backup.maxBackups;
const operationName = "manageBackupRotation";
const baseContext = requestContextService.createRequestContext({
operation: operationName,
});
if (!existsSync(validatedBackupRoot)) {
logger.warning(
`Backup root directory does not exist: ${validatedBackupRoot}. Skipping rotation.`,
{ ...baseContext, pathChecked: validatedBackupRoot },
);
return;
}
try {
logger.debug(
`Checking backup rotation in ${validatedBackupRoot}. Max backups: ${maxBackups}`,
baseContext,
);
const dirNames = readdirSync(validatedBackupRoot);
const processedDirs = await Promise.all(
dirNames.map(
async (name): Promise<{ path: string; time: number } | null> => {
const potentialDirPath = secureResolve(validatedBackupRoot, name);
if (!potentialDirPath) return null;
try {
const stats = await stat(potentialDirPath);
if (stats.isDirectory()) {
return { path: potentialDirPath, time: stats.mtime.getTime() };
}
} catch (statError: any) {
if (statError.code !== "ENOENT") {
logger.warning(
`Could not stat potential backup directory ${potentialDirPath}: ${statError.message}. Skipping.`,
{
...baseContext,
path: potentialDirPath,
errorCode: statError.code,
},
);
}
}
return null;
},
),
);
const validBackupDirs = processedDirs
.filter((dir): dir is { path: string; time: number } => dir !== null)
.sort((a, b) => a.time - b.time);
const backupsToDeleteCount = validBackupDirs.length - maxBackups;
if (backupsToDeleteCount > 0) {
logger.info(
`Found ${validBackupDirs.length} valid backups. Deleting ${backupsToDeleteCount} oldest backups to maintain limit of ${maxBackups}.`,
baseContext,
);
for (let i = 0; i < backupsToDeleteCount; i++) {
const dirToDelete = validBackupDirs[i].path;
if (!dirToDelete.startsWith(validatedBackupRoot + path.sep)) {
logger.error(
`Security Error: Attempting to delete directory outside backup root: ${dirToDelete}. Aborting deletion.`,
new Error("Backup deletion security violation"),
{ ...baseContext, dirToDelete },
);
continue;
}
try {
rmSync(dirToDelete, { recursive: true, force: true });
logger.info(`Deleted old backup directory: ${dirToDelete}`, {
...baseContext,
deletedPath: dirToDelete,
});
} catch (rmError) {
const errorMsg =
rmError instanceof Error ? rmError.message : String(rmError);
logger.error(
`Failed to delete old backup directory ${dirToDelete}: ${errorMsg}`,
rmError as Error,
{ ...baseContext, dirToDelete },
);
}
}
} else {
logger.debug(
`Backup count (${validBackupDirs.length}) is within the limit (${maxBackups}). No rotation needed.`,
baseContext,
);
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error(
`Error during backup rotation management: ${errorMsg}`,
error as Error,
baseContext,
);
}
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_unified_search/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { SearchResultItem } from "../../../services/neo4j/index.js";
import { McpToolResponse, createToolResponse } from "../../../types/mcp.js";
import { UnifiedSearchResponse } from "./types.js";
/**
* Formatter for unified search responses
*/
class UnifiedSearchFormatter {
// The input 'responseData' should match the UnifiedSearchResponse type structure.
format(responseData: UnifiedSearchResponse): string {
// Destructure the 'results' property as defined in UnifiedSearchResponse
const { results, total, page, limit, totalPages } = responseData;
// Create a summary section with pagination info
const summary =
`Search Results\n\n` +
`Found ${total ?? 0} result(s)\n` + // Use nullish coalescing for safety
`Page ${page ?? 1} of ${totalPages ?? 1} (${limit ?? 0} per page)\n`; // Use nullish coalescing
// Add a robust check for results being a valid array before accessing length
if (!Array.isArray(results) || results.length === 0) {
return `${summary}\nNo matches found for the specified search criteria.`;
}
// Group results by entity type for better organization
const groupedResults: Record<string, SearchResultItem[]> = {};
results.forEach((result: SearchResultItem) => {
// Add explicit type here
if (!groupedResults[result.type]) {
groupedResults[result.type] = [];
}
groupedResults[result.type].push(result);
});
// Build formatted output for each entity type group
let resultsOutput = "";
Object.entries(groupedResults).forEach(([type, items]) => {
// Add section heading for this entity type
resultsOutput += `\n${this.capitalizeFirstLetter(type)} Results (${items.length})\n\n`;
// Format each result item
items.forEach((item, index) => {
const score = Math.round(item.score * 10) / 10; // Round to 1 decimal place
const relevanceIndicator = this.getRelevanceIndicator(score);
resultsOutput += `${index + 1}. ${relevanceIndicator} ${item.title}\n`;
// Add relevant metadata based on entity type
if (item.type === "project") {
resultsOutput += ` ID: ${item.id}\n`;
resultsOutput += ` Type: ${item.entityType}\n`;
resultsOutput += ` Match: Found in ${item.matchedProperty}\n`;
} else if (item.type === "task") {
resultsOutput += ` ID: ${item.id}\n`;
resultsOutput += ` Project: ${item.projectName || "Unknown"}\n`;
resultsOutput += ` Type: ${item.entityType}\n`;
resultsOutput += ` Match: Found in ${item.matchedProperty}\n`;
} else if (item.type === "knowledge") {
resultsOutput += ` ID: ${item.id}\n`;
resultsOutput += ` Project: ${item.projectName || "Unknown"}\n`;
resultsOutput += ` Domain: ${item.entityType}\n`;
resultsOutput += ` Match: Found in ${item.matchedProperty}\n`;
}
// Add a snippet of the matched content
if (item.matchedValue) {
const matchSnippet = this.truncateText(item.matchedValue, 100);
resultsOutput += ` Content: "${matchSnippet}"\n`;
}
// Conditionally add created date if available
if (item.createdAt) {
resultsOutput += ` Created: ${new Date(item.createdAt).toLocaleString()}\n`;
}
// Add a blank line after each item
resultsOutput += `\n`;
});
});
// Add help text for pagination
let paginationHelp = "";
if (totalPages > 1) {
paginationHelp = `\nTo view more results, use 'page' parameter (current: ${page}, total pages: ${totalPages}).`;
}
return `${summary}${resultsOutput}${paginationHelp}`;
}
/**
* Capitalize the first letter of a string
*/
private capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Get a visual indicator for the relevance score
*/
private getRelevanceIndicator(score: number): string {
if (score >= 8) return "🔍 [Highly Relevant]";
if (score >= 6) return "🔍 [Relevant]";
if (score >= 4) return "🔍 [Moderately Relevant]";
return "🔍 [Potentially Relevant]";
}
/**
* Truncate text to a specified length with ellipsis
*/
private truncateText(
text: string | null | undefined,
maxLength: number,
): string {
// Add check to ensure text is a string and handle null/undefined
if (typeof text !== "string" || text.length === 0) {
return ""; // Return empty string if text is not valid
}
if (text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - 3) + "...";
}
}
/**
* Create a formatted, human-readable response for the atlas_unified_search tool
*
* @param data The search response data
* @param isError Whether this response represents an error condition
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatUnifiedSearchResponse(
data: UnifiedSearchResponse,
isError = false,
): McpToolResponse {
const formatter = new UnifiedSearchFormatter();
const formattedText = formatter.format(data);
return createToolResponse(formattedText, isError);
}
```
--------------------------------------------------------------------------------
/src/utils/parsing/jsonParser.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Provides a utility class for parsing potentially partial JSON strings.
* It wraps the 'partial-json' npm library and includes functionality to handle
* optional <think>...</think> blocks often found at the beginning of LLM outputs.
* @module src/utils/parsing/jsonParser
*/
import {
parse as parsePartialJson,
Allow as PartialJsonAllow,
} from "partial-json";
import { BaseErrorCode, McpError } from "../../types/errors.js";
import { logger, RequestContext, requestContextService } from "../index.js";
/**
* Enum mirroring `partial-json`'s `Allow` constants. These specify
* what types of partial JSON structures are permissible during parsing.
* They can be combined using bitwise OR (e.g., `Allow.STR | Allow.OBJ`).
*
* The available properties are:
* - `STR`: Allow partial string.
* - `NUM`: Allow partial number.
* - `ARR`: Allow partial array.
* - `OBJ`: Allow partial object.
* - `NULL`: Allow partial null.
* - `BOOL`: Allow partial boolean.
* - `NAN`: Allow partial NaN. (Note: Standard JSON does not support NaN)
* - `INFINITY`: Allow partial Infinity. (Note: Standard JSON does not support Infinity)
* - `_INFINITY`: Allow partial -Infinity. (Note: Standard JSON does not support -Infinity)
* - `INF`: Allow both partial Infinity and -Infinity.
* - `SPECIAL`: Allow all special values (NaN, Infinity, -Infinity).
* - `ATOM`: Allow all atomic values (strings, numbers, booleans, null, special values).
* - `COLLECTION`: Allow all collection values (objects, arrays).
* - `ALL`: Allow all value types to be partial (default for `partial-json`'s parse).
* @see {@link https://github.com/promplate/partial-json-parser-js} for more details.
*/
export const Allow = PartialJsonAllow;
/**
* Regular expression to find a <think> block at the start of a string.
* Captures content within <think>...</think> (Group 1) and the rest of the string (Group 2).
* @private
*/
const thinkBlockRegex = /^<think>([\s\S]*?)<\/think>\s*([\s\S]*)$/;
/**
* Utility class for parsing potentially partial JSON strings.
* Wraps the 'partial-json' library for robust JSON parsing, handling
* incomplete structures and optional <think> blocks from LLMs.
*/
export class JsonParser {
/**
* Parses a JSON string, which may be partial or prefixed with a <think> block.
* If a <think> block is present, its content is logged, and parsing proceeds on the
* remainder. Uses 'partial-json' to handle incomplete JSON.
*
* @template T The expected type of the parsed JSON object. Defaults to `any`.
* @param jsonString - The JSON string to parse.
* @param allowPartial - Bitwise OR combination of `Allow` constants specifying permissible
* partial JSON types. Defaults to `Allow.ALL`.
* @param context - Optional `RequestContext` for logging and error correlation.
* @returns The parsed JavaScript value.
* @throws {McpError} If the string is empty after processing or if `partial-json` fails.
*/
parse<T = any>(
jsonString: string,
allowPartial: number = Allow.ALL,
context?: RequestContext,
): T {
let stringToParse = jsonString;
const match = jsonString.match(thinkBlockRegex);
if (match) {
const thinkContent = match[1].trim();
const restOfString = match[2];
const logContext =
context ||
requestContextService.createRequestContext({
operation: "JsonParser.thinkBlock",
});
if (thinkContent) {
logger.debug("LLM <think> block detected and logged.", {
...logContext,
thinkContent,
});
} else {
logger.debug("Empty LLM <think> block detected.", logContext);
}
stringToParse = restOfString;
}
stringToParse = stringToParse.trim();
if (!stringToParse) {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"JSON string is empty after removing <think> block and trimming.",
context,
);
}
try {
return parsePartialJson(stringToParse, allowPartial) as T;
} catch (error: any) {
const errorLogContext =
context ||
requestContextService.createRequestContext({
operation: "JsonParser.parseError",
});
logger.error("Failed to parse JSON content.", {
...errorLogContext,
errorDetails: error.message,
contentAttempted: stringToParse.substring(0, 200),
});
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
`Failed to parse JSON: ${error.message}`,
{
...context,
originalContentSample:
stringToParse.substring(0, 200) +
(stringToParse.length > 200 ? "..." : ""),
rawError: error instanceof Error ? error.stack : String(error),
},
);
}
}
}
/**
* Singleton instance of the `JsonParser`.
* Use this instance to parse JSON strings, with support for partial JSON and <think> blocks.
* @example
* ```typescript
* import { jsonParser, Allow, requestContextService } from './utils';
* const context = requestContextService.createRequestContext({ operation: 'TestJsonParsing' });
*
* const fullJson = '{"key": "value"}';
* const parsedFull = jsonParser.parse(fullJson, Allow.ALL, context);
* console.log(parsedFull); // Output: { key: 'value' }
*
* const partialObject = '<think>This is a thought.</think>{"key": "value", "arr": [1,';
* try {
* const parsedPartial = jsonParser.parse(partialObject, undefined, context);
* console.log(parsedPartial);
* } catch (e) {
* console.error("Parsing partial object failed:", e);
* }
* ```
*/
export const jsonParser = new JsonParser();
```
--------------------------------------------------------------------------------
/src/services/neo4j/backupRestoreService/scripts/db-import.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { existsSync, lstatSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { importDatabase } from "../index.js"; // Adjusted path
import { closeNeo4jConnection } from "../../index.js"; // Adjusted path
import { logger, requestContextService } from "../../../../utils/index.js"; // Adjusted path
import { config } from "../../../../config/index.js"; // Added config import
import { McpLogLevel } from "../../../../utils/internal/logger.js"; // Added McpLogLevel import
/**
* DB Import Script
* ================
*
* Description:
* Imports data from a specified backup directory into the Neo4j database,
* overwriting existing data. Validates paths for security.
*
* Usage:
* - Update package.json script for db:import to point to the new path.
* - Run directly: npm run db:import <path_to_backup_directory>
* - Example: npm run db:import ./backups/atlas-backup-20230101120000
*/
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Project root is now 5 levels up from src/services/neo4j/backupRestoreService/scripts/
const projectRoot = path.resolve(__dirname, "../../../../../");
/**
* Validates the provided backup directory path.
* Ensures the path is within the project root and is a directory.
* @param backupDir Path to the backup directory.
* @returns True if valid, false otherwise.
*/
const isValidBackupDir = (backupDir: string): boolean => {
const resolvedBackupDir = path.resolve(backupDir);
const reqContext = requestContextService.createRequestContext({
operation: "isValidBackupDir.validation",
backupDir: resolvedBackupDir,
projectRoot,
});
if (
!resolvedBackupDir.startsWith(projectRoot + path.sep) &&
resolvedBackupDir !== projectRoot
) {
logger.error(
`Invalid backup directory: Path is outside the project boundary.`,
new Error("Path security violation: outside project boundary."),
{ ...reqContext, pathChecked: resolvedBackupDir },
);
return false;
}
if (
!existsSync(resolvedBackupDir) ||
!lstatSync(resolvedBackupDir).isDirectory()
) {
logger.error(
`Invalid backup directory: Path does not exist or is not a directory.`,
new Error("Path validation failed: not a directory or does not exist."),
{ ...reqContext, pathChecked: resolvedBackupDir },
);
return false;
}
const expectedFiles = [
"projects.json",
"tasks.json",
"knowledge.json",
"relationships.json",
"full-export.json", // Check for full-export as well, though import logic handles its absence
];
let foundAtLeastOne = false;
for (const file of expectedFiles) {
const filePath = path.join(resolvedBackupDir, file);
const resolvedFilePath = path.resolve(filePath);
if (
!resolvedFilePath.startsWith(resolvedBackupDir + path.sep) &&
resolvedFilePath !== resolvedBackupDir
) {
logger.warning(
`Skipping check for potentially unsafe file path: ${resolvedFilePath} (outside ${resolvedBackupDir})`,
{ ...reqContext, filePath: resolvedFilePath },
);
continue;
}
if (existsSync(resolvedFilePath)) {
foundAtLeastOne = true;
// For full-export, its presence is enough. For others, we just note if they are missing.
if (file !== "full-export.json" && !existsSync(resolvedFilePath)) {
logger.warning(
`Expected backup file not found: ${resolvedFilePath}. Import might be incomplete if not using full-export.json.`,
{ ...reqContext, missingFile: resolvedFilePath },
);
}
} else if (file !== "full-export.json") {
// Only warn if individual files are missing and full-export isn't the one being checked
logger.warning(
`Expected backup file not found: ${resolvedFilePath}. Import might be incomplete if not using full-export.json.`,
{ ...reqContext, missingFile: resolvedFilePath },
);
}
}
// If neither full-export.json nor any of the individual main files are found, it's likely not a valid backup.
// However, the import logic itself checks for full-export first, then individual files.
// This validation is more of a sanity check.
return true;
};
/**
* Manual import script entry point.
*/
const runManualImport = async () => {
await logger.initialize(config.logLevel as McpLogLevel);
const args = process.argv.slice(2);
if (args.length !== 1) {
logger.error(
"Usage: npm run db:import <path_to_backup_directory>",
new Error("Invalid arguments"),
requestContextService.createRequestContext({
operation: "runManualImport.argCheck",
}),
);
process.exit(1);
}
const userInputPath = args[0];
const resolvedBackupDir = path.resolve(userInputPath);
logger.info(`Attempting manual database import from: ${resolvedBackupDir}`);
logger.warning(
"!!! THIS WILL OVERWRITE ALL EXISTING DATA IN THE DATABASE !!!",
);
if (!isValidBackupDir(resolvedBackupDir)) {
process.exit(1);
}
try {
await importDatabase(resolvedBackupDir); // importDatabase itself uses validatedBackupRoot internally
logger.info(
`Manual import from ${resolvedBackupDir} completed successfully.`,
);
} catch (error) {
const reqContext = requestContextService.createRequestContext({
operation: "runManualImport.catch",
backupDir: resolvedBackupDir,
});
const errorToLog =
error instanceof Error ? error : new Error(JSON.stringify(error));
logger.error("Manual database import failed:", errorToLog, reqContext);
process.exitCode = 1;
} finally {
logger.info("Closing Neo4j connection...");
await closeNeo4jConnection();
logger.info("Neo4j connection closed.");
}
};
runManualImport();
```
--------------------------------------------------------------------------------
/src/mcp/resources/projects/projectResources.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ProjectService } from "../../../services/neo4j/projectService.js";
import {
BaseErrorCode,
McpError,
ProjectErrorCode,
} from "../../../types/errors.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import {
ResourceTemplates,
ResourceURIs,
toProjectResource,
} from "../types.js";
/**
* Register Project Resources
*
* This function registers resource endpoints for the Projects entity
* - GET atlas://projects - List all projects
* - GET atlas://projects/{projectId} - Get specific project by ID
*
* @param server The MCP server instance
*/
export function registerProjectResources(server: McpServer) {
// List all projects
server.resource(
"projects-list",
ResourceURIs.PROJECTS,
{
name: "All Projects",
description:
"List of all projects in the Atlas platform with pagination support",
mimeType: "application/json",
},
async (uri) => {
const reqContext = requestContextService.createRequestContext({
operation: "listAllProjects",
resourceUri: uri.href,
});
try {
logger.info("Listing projects", { ...reqContext, uri: uri.href });
// Parse query parameters
const queryParams = new URLSearchParams(uri.search);
const filters: Record<string, any> = {};
// Parse status parameter
const status = queryParams.get("status");
if (status) {
filters.status = String(status);
}
// Parse taskType parameter
const taskType = queryParams.get("taskType");
if (taskType) {
filters.taskType = String(taskType);
}
// 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;
// Use ProjectService instead of direct database access
const result = await ProjectService.getProjects(filters);
// Convert Neo4j projects to project resources
const projectResources = result.data.map(toProjectResource);
// Create pagination metadata using result from service
const pagination = {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
};
logger.info(
`Found ${result.total} projects, returning page ${result.page} with ${projectResources.length} items`,
{
...reqContext,
totalProjects: result.total,
returnedCount: projectResources.length,
page: result.page,
},
);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(
{
projects: projectResources,
pagination,
},
null,
2,
),
},
],
};
} catch (error) {
logger.error("Error listing projects", error as Error, {
...reqContext,
// error is now part of the Error object passed to logger
uri: uri.href,
});
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Failed to list projects: ${error instanceof Error ? error.message : String(error)}`,
);
}
},
);
// Get project by ID
server.resource(
"project-by-id",
ResourceTemplates.PROJECT,
{
name: "Project by ID",
description: "Retrieves a single project by its unique identifier",
mimeType: "application/json",
},
async (uri, params) => {
const reqContext = requestContextService.createRequestContext({
operation: "getProjectById",
resourceUri: uri.href,
projectIdParam: params.projectId,
});
try {
const projectId = params.projectId as string;
logger.info("Fetching project by ID", {
...reqContext,
projectId, // Already in reqContext
uri: uri.href, // Already in reqContext
});
if (!projectId) {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"Project ID is required",
);
}
// Use ProjectService instead of direct database access
const project = await ProjectService.getProjectById(projectId);
if (!project) {
throw new McpError(
ProjectErrorCode.PROJECT_NOT_FOUND,
`Project with ID ${projectId} not found`,
{ projectId },
);
}
// Convert to resource format
const projectResource = toProjectResource(project);
logger.info("Retrieved project successfully", {
...reqContext,
projectId: project.id, // Already in reqContext
});
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(projectResource, null, 2),
},
],
};
} catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Error fetching project by ID", error as Error, {
...reqContext,
// error is now part of the Error object passed to logger
parameters: params,
});
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Failed to fetch project: ${error instanceof Error ? error.message : String(error)}`,
);
}
},
);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_create/types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import {
McpToolResponse,
ProjectStatus,
ResponseFormat,
TaskType,
createProjectStatusEnum,
createResponseFormatEnum,
createTaskTypeEnum,
} from "../../../types/mcp.js";
export const ProjectSchema = z.object({
id: z
.string()
.optional()
.describe("Optional client-generated project ID or identifier"),
name: z
.string()
.min(1)
.max(100)
.describe("Clear, descriptive project name (1-100 characters)"),
description: z
.string()
.describe(
"Comprehensive project overview with scope, goals, and implementation details",
),
status: createProjectStatusEnum()
.default(ProjectStatus.ACTIVE)
.describe("Current project state for tracking progress (Default: active)"),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional()
.describe(
"Links to relevant documentation, specifications, and resources (e.g., 'https://example.com' or 'file://path/to/index.ts')",
),
completionRequirements: z
.string()
.describe("Clear definition of done with measurable success criteria"),
dependencies: z
.array(z.string())
.optional()
.describe(
"Project IDs that must be completed before this project can begin",
),
outputFormat: z
.string()
.describe("Required format and structure for project deliverables"),
taskType: createTaskTypeEnum()
.or(z.string())
.describe(
"Classification of project purpose for organization and workflow",
),
});
const SingleProjectSchema = z
.object({
mode: z.literal("single"),
id: z.string().optional(),
name: z.string().min(1).max(100),
description: z.string(),
status: createProjectStatusEnum().optional().default(ProjectStatus.ACTIVE),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional(),
completionRequirements: z.string(),
dependencies: z.array(z.string()).optional(),
outputFormat: z.string(),
taskType: createTaskTypeEnum().or(z.string()),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
})
.describe("Creates a single project with comprehensive details and metadata");
const BulkProjectSchema = z
.object({
mode: z.literal("bulk"),
projects: z
.array(ProjectSchema)
.min(1)
.max(100)
.describe(
"Collection of project definitions to create in a single operation",
),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
})
.describe(
"Create multiple related projects in a single efficient transaction",
);
// Schema shapes for tool registration
export const AtlasProjectCreateSchemaShape = {
mode: z
.enum(["single", "bulk"])
.describe(
"Operation mode - 'single' for creating one detailed project with full metadata, 'bulk' for efficiently initializing multiple related projects in a single transaction",
),
id: z
.string()
.optional()
.describe(
"Client-generated unique project identifier for consistent cross-referencing (recommended for mode='single', system will generate if not provided)",
),
name: z
.string()
.min(1)
.max(100)
.optional()
.describe(
"Clear, descriptive project name for display and identification (1-100 characters) (required for mode='single')",
),
description: z
.string()
.optional()
.describe(
"Comprehensive project overview detailing scope, objectives, approach, and implementation details (required for mode='single')",
),
status: createProjectStatusEnum()
.optional()
.describe(
"Project lifecycle state for tracking progress and filtering (Default: active)",
),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional()
.describe(
"Array of titled links to relevant documentation, specifications, code repositories, and external resources (supports web URLs and file paths)",
),
completionRequirements: z
.string()
.optional()
.describe(
"Quantifiable success criteria and acceptance requirements that define when the project is considered complete (required for mode='single')",
),
dependencies: z
.array(z.string())
.optional()
.describe(
"Array of project IDs that must be completed before this project can begin, establishing workflow prerequisites and sequencing",
),
outputFormat: z
.string()
.optional()
.describe(
"Expected format and structure specification for the project's final deliverables and artifacts (required for mode='single')",
),
taskType: createTaskTypeEnum()
.or(z.string())
.optional()
.describe(
"Project type classification for workflow organization, filtering, and reporting (options: research, generation, analysis, integration, or custom type) (required for mode='single')",
),
projects: z
.array(ProjectSchema)
.min(1)
.max(100)
.optional()
.describe(
"Array of complete project definition objects to create in a single transaction (supports 1-100 projects, required for mode='bulk')",
),
responseFormat: createResponseFormatEnum()
.optional()
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
} as const;
// Schema for validation
export const AtlasProjectCreateSchema = z.discriminatedUnion("mode", [
SingleProjectSchema,
BulkProjectSchema,
]);
export type AtlasProjectCreateInput = z.infer<typeof AtlasProjectCreateSchema>;
export type ProjectInput = z.infer<typeof ProjectSchema>;
export type AtlasProjectCreateResponse = McpToolResponse;
```
--------------------------------------------------------------------------------
/src/webui/logic/main.js:
--------------------------------------------------------------------------------
```javascript
/**
* @fileoverview Main application entry point. Initializes the app and sets up event handlers.
* @module src/webui/logic/main
*/
import { dom } from "./dom-elements.js";
import { state } from "./app-state.js"; // utils is also exported from app-state but not directly used here
import { uiHelpers, renderHelpers } from "./ui-service.js";
import { api } from "./api-service.js";
/**
* Manages application event handling.
* @type {object}
*/
const eventHandlers = {
/**
* Handles changes to the project selection dropdown.
* @param {Event} event - The change event.
*/
handleProjectSelectChange: (event) => {
if (event.target && event.target.value) {
const projectId = event.target.value;
api.fetchProjectDetails(projectId);
localStorage.setItem("lastSelectedProjectId", projectId);
}
},
/**
* Handles clicks on the refresh button.
*/
handleRefreshClick: () => {
api.fetchProjects();
},
/**
* Handles changes to the theme toggle checkbox.
*/
handleThemeToggleChange: () => {
uiHelpers.toggleTheme();
},
/**
* Handles clicks on the task view mode toggle button.
*/
handleTaskViewModeToggle: () => {
state.tasksViewMode =
state.tasksViewMode === "detailed" ? "compact" : "detailed";
uiHelpers.updateToggleButton(
dom.taskViewModeToggle,
state.tasksViewMode === "compact",
"Detailed View",
"Compact View",
);
if (dom.tasksContent) {
// Ensure element exists
renderHelpers.tasks(
state.currentTasks,
dom.tasksContent,
state.tasksViewMode,
);
}
},
/**
* Handles clicks on the knowledge view mode toggle button.
*/
handleKnowledgeViewModeToggle: () => {
state.knowledgeViewMode =
state.knowledgeViewMode === "detailed" ? "compact" : "detailed";
uiHelpers.updateToggleButton(
dom.knowledgeViewModeToggle,
state.knowledgeViewMode === "compact",
"Detailed View",
"Compact View",
);
if (dom.knowledgeContent) {
// Ensure element exists
renderHelpers.knowledgeItems(
state.currentKnowledgeItems,
dom.knowledgeContent,
state.knowledgeViewMode,
);
}
},
/**
* Handles clicks on the task flow toggle button.
*/
handleTaskFlowToggle: () => {
state.showingTaskFlow = !state.showingTaskFlow;
uiHelpers.setDisplay(dom.tasksContent, !state.showingTaskFlow);
uiHelpers.setDisplay(dom.taskFlowContainer, state.showingTaskFlow);
uiHelpers.updateToggleButton(
dom.taskFlowToggle,
state.showingTaskFlow,
"View Task List",
"View Task Flow",
);
if (state.showingTaskFlow && dom.taskFlowContainer) {
// Ensure element exists
renderHelpers.taskFlow(state.currentTasks, dom.taskFlowContainer);
}
},
/**
* Sets up all event listeners for the application.
*/
setup: () => {
// Ensure DOM elements exist before adding listeners
if (dom.projectSelect) {
dom.projectSelect.addEventListener(
"change",
eventHandlers.handleProjectSelectChange,
);
}
if (dom.refreshButton) {
dom.refreshButton.addEventListener(
"click",
eventHandlers.handleRefreshClick,
);
}
if (dom.themeCheckbox) {
dom.themeCheckbox.addEventListener(
"change",
eventHandlers.handleThemeToggleChange,
);
}
if (dom.themeLabel) {
// Allow clicking label to toggle checkbox
dom.themeLabel.addEventListener("click", () => {
if (dom.themeCheckbox) dom.themeCheckbox.click();
});
}
if (dom.taskViewModeToggle) {
dom.taskViewModeToggle.addEventListener(
"click",
eventHandlers.handleTaskViewModeToggle,
);
}
if (dom.knowledgeViewModeToggle) {
dom.knowledgeViewModeToggle.addEventListener(
"click",
eventHandlers.handleKnowledgeViewModeToggle,
);
}
if (dom.taskFlowToggle) {
dom.taskFlowToggle.addEventListener(
"click",
eventHandlers.handleTaskFlowToggle,
);
}
},
};
/**
* Waits for the Neo4j global driver to be available.
* @param {number} [timeout=5000] - Maximum time to wait in milliseconds.
* @returns {Promise<void>} Resolves when neo4j is available, or rejects on timeout.
* @private
*/
async function waitForNeo4j(timeout = 5000) {
const startTime = Date.now();
while (typeof neo4j === "undefined") {
if (Date.now() - startTime > timeout) {
console.error("Neo4j driver failed to load within timeout.");
throw new Error("Neo4j driver failed to load within timeout.");
}
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait 100ms
}
console.log("Neo4j driver detected.");
}
/**
* Initializes the application.
* Loads theme, sets up event handlers, connects to Neo4j, and fetches initial data.
* @async
*/
async function initApp() {
uiHelpers.loadTheme(); // Apply saved theme and initialize Mermaid
eventHandlers.setup(); // Setup event listeners
// Initialize toggle button texts, ensuring buttons exist
uiHelpers.updateToggleButton(
dom.taskViewModeToggle,
state.tasksViewMode === "compact",
"Detailed View",
"Compact View",
);
uiHelpers.updateToggleButton(
dom.knowledgeViewModeToggle,
state.knowledgeViewMode === "compact",
"Detailed View",
"Compact View",
);
uiHelpers.updateToggleButton(
dom.taskFlowToggle,
state.showingTaskFlow,
"View Task List",
"View Task Flow",
);
try {
await waitForNeo4j(); // Wait for the driver to be loaded
const connected = await api.connect();
if (connected) {
api.fetchProjects();
}
} catch (error) {
console.error("Initialization error:", error);
// Ensure uiHelpers is available and dom.errorMessageDiv is checked within showError
uiHelpers.showError(
`App Initialization Error: ${error.message}. Check console.`,
true,
);
}
}
// Start the application once the DOM is fully loaded
document.addEventListener("DOMContentLoaded", initApp);
```
--------------------------------------------------------------------------------
/src/mcp/resources/types.ts:
--------------------------------------------------------------------------------
```typescript
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
Neo4jKnowledge,
Neo4jProject,
Neo4jTask,
} from "../../services/neo4j/types.js";
import { logger, requestContextService } from "../../utils/index.js"; // Import requestContextService
/**
* Resource URIs for the Atlas MCP resources
*/
export const ResourceURIs = {
// Project resources
PROJECTS: "atlas://projects",
PROJECT_TEMPLATE: "atlas://projects/{projectId}",
// Task resources
TASKS: "atlas://tasks",
TASKS_BY_PROJECT: "atlas://projects/{projectId}/tasks",
TASK_TEMPLATE: "atlas://tasks/{taskId}",
// Knowledge resources
KNOWLEDGE: "atlas://knowledge",
KNOWLEDGE_BY_PROJECT: "atlas://projects/{projectId}/knowledge",
KNOWLEDGE_TEMPLATE: "atlas://knowledge/{knowledgeId}",
};
/**
* Resource templates for the Atlas MCP resources
*/
export const ResourceTemplates = {
// Project resource templates
PROJECT: new ResourceTemplate(ResourceURIs.PROJECT_TEMPLATE, {
list: () => ({
resources: [
{
uri: ResourceURIs.PROJECTS,
name: "All Projects",
description: "List of all projects in the Atlas platform",
},
],
}),
}),
// Task resource templates
TASK: new ResourceTemplate(ResourceURIs.TASK_TEMPLATE, {
list: () => ({
resources: [
{
uri: ResourceURIs.TASKS,
name: "All Tasks",
description: "List of all tasks in the Atlas platform",
},
],
}),
}),
TASKS_BY_PROJECT: new ResourceTemplate(ResourceURIs.TASKS_BY_PROJECT, {
list: undefined,
}),
// Knowledge resource templates
KNOWLEDGE: new ResourceTemplate(ResourceURIs.KNOWLEDGE_TEMPLATE, {
list: () => ({
resources: [
{
uri: ResourceURIs.KNOWLEDGE,
name: "All Knowledge",
description: "List of all knowledge items in the Atlas platform",
},
],
}),
}),
KNOWLEDGE_BY_PROJECT: new ResourceTemplate(
ResourceURIs.KNOWLEDGE_BY_PROJECT,
{
list: undefined,
},
),
};
/**
* Project resource response interface
*/
export interface ProjectResource {
id: string;
name: string;
description: string;
status: string;
urls: Array<{ title: string; url: string }>;
completionRequirements: string;
outputFormat: string;
taskType: string;
createdAt: string;
updatedAt: string;
}
/**
* Task resource response interface
*/
export interface TaskResource {
id: string;
projectId: string;
title: string;
description: string;
priority: string;
status: string;
assignedTo: string | null;
urls: Array<{ title: string; url: string }>;
tags: string[];
completionRequirements: string;
outputFormat: string;
taskType: string;
createdAt: string;
updatedAt: string;
}
/**
* Knowledge resource response interface
*/
export interface KnowledgeResource {
id: string;
projectId: string;
text: string;
tags: string[];
domain: string;
citations: string[];
createdAt: string;
updatedAt: string;
}
/**
* Convert Neo4j Project to Project Resource
*/
export function toProjectResource(project: Neo4jProject): ProjectResource {
const reqContext = requestContextService.createRequestContext({
operation: "toProjectResource",
projectId: project.id,
});
// Log the incoming project structure for debugging
logger.debug("Converting project to resource:", {
...reqContext,
projectData: project,
});
// Ensure all fields are properly extracted
const resource: ProjectResource = {
id: project.id,
name: project.name,
description: project.description,
status: project.status,
urls: project.urls || [],
completionRequirements: project.completionRequirements,
outputFormat: project.outputFormat,
taskType: project.taskType,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
};
logger.debug("Created project resource:", {
...reqContext,
projectResource: resource,
});
return resource;
}
/**
* Convert Neo4j Task (with added assignedToUserId) to Task Resource
*/
export function toTaskResource(
task: Neo4jTask & { assignedToUserId: string | null },
): TaskResource {
const reqContext = requestContextService.createRequestContext({
operation: "toTaskResource",
taskId: task.id,
});
// Log the incoming task structure for debugging
logger.debug("Converting task to resource:", {
...reqContext,
taskData: task,
});
const resource: TaskResource = {
id: task.id,
projectId: task.projectId,
title: task.title,
description: task.description,
priority: task.priority,
status: task.status,
assignedTo: task.assignedToUserId, // Use assignedToUserId from the input object
urls: task.urls || [],
tags: task.tags || [],
completionRequirements: task.completionRequirements,
outputFormat: task.outputFormat,
taskType: task.taskType,
createdAt: task.createdAt,
updatedAt: task.updatedAt,
};
logger.debug("Created task resource:", {
...reqContext,
taskResource: resource,
});
return resource;
}
/**
* Convert Neo4j Knowledge (with added domain/citations) to Knowledge Resource
*/
export function toKnowledgeResource(
knowledge: Neo4jKnowledge & { domain: string | null; citations: string[] },
): KnowledgeResource {
const reqContext = requestContextService.createRequestContext({
operation: "toKnowledgeResource",
knowledgeId: knowledge.id,
});
// Log the incoming knowledge structure for debugging
logger.debug("Converting knowledge to resource:", {
...reqContext,
knowledgeData: knowledge,
});
const resource: KnowledgeResource = {
id: knowledge.id,
projectId: knowledge.projectId,
text: knowledge.text,
tags: knowledge.tags || [],
domain: knowledge.domain || "", // Use domain from the input object, default to empty string if null
citations: knowledge.citations || [], // Use citations from the input object
createdAt: knowledge.createdAt,
updatedAt: knowledge.updatedAt,
};
logger.debug("Created knowledge resource:", {
...reqContext,
knowledgeResource: resource,
});
return resource;
}
```
--------------------------------------------------------------------------------
/src/webui/index.html:
--------------------------------------------------------------------------------
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Atlas Projects Live View</title>
<link rel="stylesheet" href="styling/theme.css" />
<link rel="stylesheet" href="styling/base.css" />
<link rel="stylesheet" href="styling/layout.css" />
<link rel="stylesheet" href="styling/components.css" />
<!-- Mermaid JS CDN -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@latest/dist/mermaid.min.js"></script>
</head>
<body>
<div id="app">
<header class="app-header">
<div class="theme-switch-wrapper">
<label
class="theme-switch"
for="theme-checkbox"
aria-label="Toggle color theme"
>
<input type="checkbox" id="theme-checkbox" />
<div class="slider round"></div>
</label>
<span class="theme-label" id="theme-label-text">Toggle Theme</span>
</div>
<h1>Atlas Projects Live View</h1>
</header>
<main>
<section class="controls-section" aria-labelledby="controls-heading">
<h2 id="controls-heading" class="visually-hidden">
Project Controls
</h2>
<div class="controls-container">
<label for="project-select">Select a Project:</label>
<select
id="project-select"
aria-label="Select a project to view its details"
>
<option value="">Loading projects...</option>
</select>
<button id="refresh-button" type="button">Refresh Projects</button>
</div>
</section>
<section
id="project-details-container"
class="data-section hidden"
aria-labelledby="project-details-heading"
>
<h2 id="project-details-heading">Project Details</h2>
<div id="details-content" class="details-grid"></div>
</section>
<section
id="tasks-container"
class="data-section hidden"
aria-labelledby="tasks-heading"
>
<div class="section-header">
<h3 id="tasks-heading">Tasks</h3>
<div class="view-controls">
<button
class="view-toggle-button"
id="task-view-mode-toggle"
type="button"
aria-pressed="false"
aria-controls="tasks-content"
>
Compact View
</button>
<button
class="view-toggle-button"
id="task-flow-toggle"
type="button"
aria-pressed="false"
aria-controls="task-flow-container tasks-content"
>
View Task Flow
</button>
</div>
</div>
<div id="tasks-content" role="region" aria-live="polite"></div>
<div
id="task-flow-container"
class="hidden mermaid-container"
role="region"
aria-live="polite"
>
<!-- Mermaid chart will be rendered here -->
</div>
</section>
<section
id="project-task-board-container"
class="data-section hidden"
aria-labelledby="project-task-board-heading"
>
<h2 id="project-task-board-heading">Project Task Board</h2>
<div
id="project-task-board-content"
class="task-board-grid"
role="region"
aria-live="polite"
>
<!-- Task board columns will be rendered here -->
</div>
</section>
<section
id="knowledge-container"
class="data-section hidden"
aria-labelledby="knowledge-heading"
>
<div class="section-header">
<h3 id="knowledge-heading">Knowledge Items</h3>
<div class="view-controls">
<button
class="view-toggle-button"
id="knowledge-view-mode-toggle"
type="button"
aria-pressed="false"
aria-controls="knowledge-content"
>
Compact View
</button>
</div>
</div>
<div id="knowledge-content" role="region" aria-live="polite"></div>
</section>
<section
id="data-explorer-container"
class="data-section hidden"
aria-labelledby="data-explorer-heading"
>
<h2 id="data-explorer-heading">Data Explorer</h2>
<div class="controls-container">
<label for="node-label-select">Select Node Type:</label>
<select
id="node-label-select"
aria-label="Select a node type to explore"
>
<option value="">Loading node types...</option>
</select>
<button id="refresh-node-types-button" type="button">
Refresh Types
</button>
</div>
<div id="data-explorer-content" role="region" aria-live="polite">
<!-- Nodes of selected type will be rendered here -->
</div>
<div
id="data-explorer-details"
class="details-grid"
role="region"
aria-live="polite"
>
<!-- Details of a selected node will be rendered here -->
</div>
</section>
</main>
<footer class="app-footer">
<div
id="error-message"
class="error hidden"
role="alert"
aria-live="assertive"
></div>
<div id="connection-status">
Connection Status: <span id="neo4j-status">Not Connected</span>
</div>
</footer>
</div>
<!-- Neo4j Browser Driver via CDN -->
<script src="https://unpkg.com/neo4j-driver@5/lib/browser/neo4j-web.min.js"></script>
<!--
Optional: User can manually provide credentials here if not included in prompt
These will be overridden by script.js if window.NEO4J_... variables are already set there.
<script>
// window.NEO4J_URI = "bolt://localhost:7687";
// window.NEO4J_USER = "neo4j";
// window.NEO4J_PASSWORD = "password2";
</script>
-->
<script type="module" src="logic/main.js"></script>
</body>
</html>
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_deep_research/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { McpToolResponse, createToolResponse } from "../../../types/mcp.js"; // Import createToolResponse
import { AtlasDeepResearchInput, DeepResearchResult } from "./types.js";
/**
* Defines a generic interface for formatting data into a string.
* This was previously imported but is now defined locally as the original seems to be removed.
*/
interface ResponseFormatter<T> {
format(data: T): string;
}
/**
* Base response formatter for the `atlas_deep_research` tool.
* This formatter provides a basic structure for the output, primarily using
* the data returned by the core `deepResearch` function.
* It's designed to be used within `formatDeepResearchResponse` which adds
* contextual information from the original tool input.
*/
export const DeepResearchBaseFormatter: ResponseFormatter<DeepResearchResult> =
{
format: (data: DeepResearchResult): string => {
// This base format method only uses the 'data' part of the result.
// Context from the 'input' is added by the calling function below.
if (!data.success) {
// Basic error formatting if the operation failed
return `Error initiating deep research: ${data.message}`;
}
// Start building the Markdown output
const lines: string[] = [
`## Deep Research Plan Initiated`,
`**Status:** ${data.message}`, // Display the success message from the core logic
`**Plan Node ID:** \`${data.planNodeId}\``, // Show the ID of the created root node
];
// Add details about the created sub-topic nodes
if (data.subTopicNodes && data.subTopicNodes.length > 0) {
lines.push(
`\n### Sub-Topics Created (${data.subTopicNodes.length})${
data.tasksCreated ? " (with Tasks)" : ""
}:`,
);
data.subTopicNodes.forEach((node) => {
const taskInfo = node.taskId
? `\n - **Task ID:** \`${node.taskId}\``
: "";
// Basic info available directly from the result data
lines.push(
`- **Question:** ${node.question}\n - **Node ID:** \`${node.nodeId}\`${taskInfo}`,
// Note: Initial Search Queries are added by the contextual formatter below
);
});
} else {
lines.push("\nNo sub-topics were specified or created.");
}
return lines.join("\n"); // Combine lines into a single Markdown string
},
};
/**
* Creates the final formatted `McpToolResponse` for the `atlas_deep_research` tool.
* This function takes the raw result from the core logic (`deepResearch`) and the
* original tool input, then uses a *contextual* formatter to generate the final
* Markdown output. The contextual formatter enhances the base format by including
* details from the input (like topic, goal, scope, tags, and search queries).
*
* @param rawData - The `DeepResearchResult` object returned by the `deepResearch` function.
* @param input - The original `AtlasDeepResearchInput` provided to the tool.
* @returns The final `McpToolResponse` object ready to be sent back to the client.
*/
export function formatDeepResearchResponse(
rawData: DeepResearchResult,
input: AtlasDeepResearchInput,
): McpToolResponse {
// Define a contextual formatter *inside* this function.
// This allows the formatter's `format` method to access the `input` variable via closure.
const contextualFormatter: ResponseFormatter<DeepResearchResult> = {
format: (data: DeepResearchResult): string => {
// Handle error case first
if (!data.success) {
return `Error initiating deep research: ${data.message}`;
}
// Start building the Markdown output, including details from the input
const lines: string[] = [
`## Deep Research Plan Initiated`,
`**Topic:** ${input.researchTopic}`, // Include Topic from input
`**Goal:** ${input.researchGoal}`, // Include Goal from input
];
if (input.scopeDefinition) {
lines.push(`**Scope:** ${input.scopeDefinition}`); // Include Scope if provided
}
lines.push(`**Project ID:** \`${input.projectId}\``); // Include Project ID
if (input.researchDomain) {
lines.push(`**Domain:** ${input.researchDomain}`); // Include Domain if provided
}
lines.push(`**Status:** ${data.message}`); // Status message from result
lines.push(`**Plan Node ID:** \`${data.planNodeId}\``); // Root node ID from result
if (input.initialTags && input.initialTags.length > 0) {
lines.push(`**Initial Tags:** ${input.initialTags.join(", ")}`); // Include initial tags
}
// Add details about sub-topic nodes, including search queries from input
if (data.subTopicNodes && data.subTopicNodes.length > 0) {
lines.push(`\n### Sub-Topics Created (${data.subTopicNodes.length}):`);
data.subTopicNodes.forEach((node) => {
// Find the corresponding sub-topic in the input to retrieve initial search queries
// Find the corresponding sub-topic in the input to retrieve initial search queries and task details
const inputSubTopic = input.subTopics.find(
(st) => st.question === node.question,
);
const searchQueries =
inputSubTopic?.initialSearchQueries?.join(", ") || "N/A"; // Format queries or show N/A
const taskInfo = node.taskId
? `\n - **Task ID:** \`${node.taskId}\``
: ""; // Add Task ID if present
const priorityInfo = inputSubTopic?.priority
? `\n - **Task Priority:** ${inputSubTopic.priority}`
: "";
const assigneeInfo = inputSubTopic?.assignedTo
? `\n - **Task Assignee:** ${inputSubTopic.assignedTo}`
: "";
const statusInfo = inputSubTopic?.initialStatus
? `\n - **Task Status:** ${inputSubTopic.initialStatus}`
: "";
lines.push(
`- **Question:** ${node.question}\n - **Node ID:** \`${node.nodeId}\`${taskInfo}${priorityInfo}${assigneeInfo}${statusInfo}\n - **Initial Search Queries:** ${searchQueries}`, // Add search queries and task details
);
});
} else {
lines.push("\nNo sub-topics were specified or created.");
}
return lines.join("\n"); // Combine all lines into the final Markdown string
},
};
const formattedText = contextualFormatter.format(rawData);
return createToolResponse(formattedText, !rawData.success);
}
```
--------------------------------------------------------------------------------
/src/services/neo4j/backupRestoreService/exportLogic.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Implements the database export logic for Neo4j.
* @module src/services/neo4j/backupRestoreService/exportLogic
*/
import { format } from "date-fns";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
import { Session } from "neo4j-driver";
import { logger, requestContextService } from "../../../utils/index.js";
import { neo4jDriver } from "../driver.js";
import { FullExport } from "./backupRestoreTypes.js";
import {
manageBackupRotation,
secureResolve,
validatedBackupRoot,
} from "./backupUtils.js";
/**
* Exports all Project, Task, and Knowledge nodes and relationships to JSON files.
* Also creates a full-export.json file containing all data in a single file.
* Manages backup rotation before creating the new backup.
* @returns The path to the directory containing the backup files.
* @throws Error if the export step fails. Rotation errors are logged but don't throw.
*/
export const _exportDatabase = async (): Promise<string> => {
const operationName = "_exportDatabase"; // Renamed
const baseContext = requestContextService.createRequestContext({
operation: operationName,
});
await manageBackupRotation();
let session: Session | null = null;
const timestamp = format(new Date(), "yyyyMMddHHmmss");
const backupDirName = `atlas-backup-${timestamp}`;
const backupDir = secureResolve(validatedBackupRoot, backupDirName);
if (!backupDir) {
throw new Error(
`Failed to create secure backup directory path for ${backupDirName} within ${validatedBackupRoot}`,
);
}
const fullExport: FullExport = {
nodes: {},
relationships: [],
};
try {
session = await neo4jDriver.getSession();
logger.info(`Starting database export to ${backupDir}...`, baseContext);
if (!existsSync(backupDir)) {
mkdirSync(backupDir, { recursive: true });
logger.debug(`Created backup directory: ${backupDir}`, baseContext);
}
logger.debug("Fetching all node labels from database...", baseContext);
const labelsResult = await session.run(
"CALL db.labels() YIELD label RETURN label",
);
const nodeLabels: string[] = labelsResult.records.map((record) =>
record.get("label"),
);
logger.info(`Found labels: ${nodeLabels.join(", ")}`, {
...baseContext,
labels: nodeLabels,
});
for (const label of nodeLabels) {
logger.debug(`Exporting nodes with label: ${label}`, {
...baseContext,
currentLabel: label,
});
const escapedLabel = `\`${label.replace(/`/g, "``")}\``;
const nodeResult = await session.run(
`MATCH (n:${escapedLabel}) RETURN n`,
);
const nodes = nodeResult.records.map(
(record) => record.get("n").properties,
);
const fileName = `${label.toLowerCase()}s.json`;
const filePath = secureResolve(backupDir, fileName);
if (!filePath) {
logger.error(
`Skipping export for label ${label}: Could not create secure path for ${fileName} in ${backupDir}`,
new Error("Secure path resolution failed"),
{ ...baseContext, label, fileName, targetDir: backupDir },
);
continue;
}
writeFileSync(filePath, JSON.stringify(nodes, null, 2));
logger.info(
`Successfully exported ${nodes.length} ${label} nodes to ${filePath}`,
{ ...baseContext, label, count: nodes.length, filePath },
);
fullExport.nodes[label] = nodes;
}
logger.debug("Exporting relationships...", baseContext);
const relResult = await session.run(`
MATCH (start)-[r]->(end)
WHERE start.id IS NOT NULL AND end.id IS NOT NULL
RETURN
start.id as startNodeAppId,
end.id as endNodeAppId,
type(r) as relType,
properties(r) as relProps
`);
const relationships = relResult.records.map((record) => ({
startNodeId: record.get("startNodeAppId"),
endNodeId: record.get("endNodeAppId"),
type: record.get("relType"),
properties: record.get("relProps") || {},
}));
const relFileName = "relationships.json";
const relFilePath = secureResolve(backupDir, relFileName);
if (!relFilePath) {
throw new Error(
`Failed to create secure path for ${relFileName} in ${backupDir}`,
);
}
writeFileSync(relFilePath, JSON.stringify(relationships, null, 2));
logger.info(
`Successfully exported ${relationships.length} relationships to ${relFilePath}`,
{ ...baseContext, count: relationships.length, filePath: relFilePath },
);
fullExport.relationships = relationships;
const fullExportFileName = "full-export.json";
const fullExportPath = secureResolve(backupDir, fullExportFileName);
if (!fullExportPath) {
throw new Error(
`Failed to create secure path for ${fullExportFileName} in ${backupDir}`,
);
}
writeFileSync(fullExportPath, JSON.stringify(fullExport, null, 2));
logger.info(
`Successfully created full database export to ${fullExportPath}`,
{ ...baseContext, filePath: fullExportPath },
);
logger.info(
`Database export completed successfully to ${backupDir}`,
baseContext,
);
return backupDir;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(
`Database export failed: ${errorMessage}`,
error as Error,
baseContext,
);
if (backupDir && existsSync(backupDir)) {
if (!backupDir.startsWith(validatedBackupRoot + require("path").sep)) {
// Use require("path") for sep
logger.error(
`Security Error: Attempting cleanup of directory outside backup root: ${backupDir}. Aborting cleanup.`,
new Error("Cleanup security violation"),
{ ...baseContext, cleanupDir: backupDir },
);
} else {
try {
rmSync(backupDir, { recursive: true, force: true });
logger.warning(
`Removed partially created backup directory due to export failure: ${backupDir}`,
{ ...baseContext, cleanupDir: backupDir },
);
} catch (rmError) {
const rmErrorMsg =
rmError instanceof Error ? rmError.message : String(rmError);
logger.error(
`Failed to remove partial backup directory ${backupDir}: ${rmErrorMsg}`,
rmError as Error,
{ ...baseContext, cleanupDir: backupDir },
);
}
}
}
throw new Error(`Database export failed: ${errorMessage}`);
} finally {
if (session) {
await session.close();
}
}
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_update/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import {
TaskResponse,
McpToolResponse,
createToolResponse,
} from "../../../types/mcp.js";
/**
* Extends the TaskResponse to include Neo4j properties structure
*/
interface SingleTaskResponse extends TaskResponse {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
}
/**
* Interface for bulk task update response
*/
interface BulkTaskResponse {
success: boolean;
message: string;
updated: (TaskResponse & {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
})[];
errors: {
index: number;
task: {
id: string;
updates: any;
};
error: {
code: string;
message: string;
details?: any;
};
}[];
}
/**
* Formatter for individual task update responses
*/
class SingleTaskUpdateFormatter {
format(data: SingleTaskResponse): string {
// Extract task properties from Neo4j structure
const taskData = data.properties || data;
const { title, id, projectId, status, priority, taskType, updatedAt } =
taskData;
// Create a structured summary section
const summary =
`Task Updated Successfully\n\n` +
`Task: ${title || "Unnamed Task"}\n` +
`ID: ${id || "Unknown ID"}\n` +
`Project ID: ${projectId || "Unknown Project"}\n` +
`Status: ${status || "Unknown Status"}\n` +
`Priority: ${priority || "Unknown Priority"}\n` +
`Type: ${taskType || "Unknown Type"}\n` +
`Updated: ${updatedAt ? new Date(updatedAt).toLocaleString() : "Unknown Date"}\n`;
// Create a comprehensive details section with all task attributes
let details = `Task Details\n\n`;
// Add each property with proper formatting
if (taskData.id) details += `ID: ${taskData.id}\n`;
if (taskData.projectId) details += `Project ID: ${taskData.projectId}\n`;
if (taskData.title) details += `Title: ${taskData.title}\n`;
if (taskData.description)
details += `Description: ${taskData.description}\n`;
if (taskData.priority) details += `Priority: ${taskData.priority}\n`;
if (taskData.status) details += `Status: ${taskData.status}\n`;
if (taskData.assignedTo) details += `Assigned To: ${taskData.assignedTo}\n`;
// Format URLs array
if (taskData.urls) {
const urlsValue =
Array.isArray(taskData.urls) && taskData.urls.length > 0
? JSON.stringify(taskData.urls)
: "None";
details += `URLs: ${urlsValue}\n`;
}
// Format tags array
if (taskData.tags) {
const tagsValue =
Array.isArray(taskData.tags) && taskData.tags.length > 0
? taskData.tags.join(", ")
: "None";
details += `Tags: ${tagsValue}\n`;
}
if (taskData.completionRequirements)
details += `Completion Requirements: ${taskData.completionRequirements}\n`;
if (taskData.outputFormat)
details += `Output Format: ${taskData.outputFormat}\n`;
if (taskData.taskType) details += `Task Type: ${taskData.taskType}\n`;
// Format dates
if (taskData.createdAt) {
const createdDate =
typeof taskData.createdAt === "string" &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(taskData.createdAt)
? new Date(taskData.createdAt).toLocaleString()
: taskData.createdAt;
details += `Created At: ${createdDate}\n`;
}
if (taskData.updatedAt) {
const updatedDate =
typeof taskData.updatedAt === "string" &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(taskData.updatedAt)
? new Date(taskData.updatedAt).toLocaleString()
: taskData.updatedAt;
details += `Updated At: ${updatedDate}\n`;
}
return `${summary}\n\n${details}`;
}
}
/**
* Formatter for bulk task update responses
*/
class BulkTaskUpdateFormatter {
format(data: BulkTaskResponse): string {
const { success, message, updated, errors } = data;
// Create a summary section
const summary =
`${success ? "Tasks Updated Successfully" : "Task Updates Completed with Errors"}\n\n` +
`Status: ${success ? "✅ Success" : "⚠️ Partial Success"}\n` +
`Summary: ${message}\n` +
`Updated: ${updated.length} task(s)\n` +
`Errors: ${errors.length} error(s)\n`;
// List all successfully modified tasks
let updatedSection = "";
if (updated.length > 0) {
updatedSection = `Updated Tasks\n\n`;
updatedSection += updated
.map((task, index) => {
// Extract task properties from Neo4j structure
const taskData = task.properties || task;
return (
`${index + 1}. ${taskData.title || "Unnamed Task"}\n\n` +
`ID: ${taskData.id || "Unknown ID"}\n` +
`Project ID: ${taskData.projectId || "Unknown Project"}\n` +
`Type: ${taskData.taskType || "Unknown Type"}\n` +
`Status: ${taskData.status || "Unknown Status"}\n` +
`Priority: ${taskData.priority || "Unknown Priority"}\n` +
`Updated: ${taskData.updatedAt ? new Date(taskData.updatedAt).toLocaleString() : "Unknown Date"}\n`
);
})
.join("\n\n");
}
// List any errors that occurred
let errorsSection = "";
if (errors.length > 0) {
errorsSection = `Errors\n\n`;
errorsSection += errors
.map((error, index) => {
return (
`${index + 1}. Error updating Task ID: "${error.task.id}"\n\n` +
`Error Code: ${error.error.code}\n` +
`Message: ${error.error.message}\n` +
(error.error.details
? `Details: ${JSON.stringify(error.error.details)}\n`
: "")
);
})
.join("\n\n");
}
return `${summary}\n\n${updatedSection}\n\n${errorsSection}`.trim();
}
}
/**
* Create a formatted, human-readable response for the atlas_task_update tool
*
* @param data The raw task update response
* @param isError Whether this response represents an error condition
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatTaskUpdateResponse(
data: any,
isError = false,
): McpToolResponse {
// Determine if this is a single or bulk task response
const isBulkResponse =
data.hasOwnProperty("success") && data.hasOwnProperty("updated");
let formattedText: string;
if (isBulkResponse) {
const formatter = new BulkTaskUpdateFormatter();
formattedText = formatter.format(data as BulkTaskResponse);
} else {
const formatter = new SingleTaskUpdateFormatter();
formattedText = formatter.format(data as SingleTaskResponse);
}
return createToolResponse(formattedText, isError);
}
```
--------------------------------------------------------------------------------
/src/webui/styling/layout.css:
--------------------------------------------------------------------------------
```css
/* ==========================================================================
Main Application Layout (#app, header, main, footer)
========================================================================== */
#app {
position: relative; /* For theme toggle positioning */
max-width: 1000px;
margin: var(--spacing-xl) auto;
padding: var(--spacing-lg) var(--spacing-xl);
background-color: var(--card-bg-color);
border-radius: var(--border-radius-lg);
box-shadow: 0 8px 24px var(--shadow-color);
transition: background-color 0.2s ease-out;
}
.app-header {
margin-bottom: var(--spacing-xl);
}
.app-footer {
margin-top: var(--spacing-xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
}
/* ==========================================================================
Controls Section (Project Select, Refresh Button)
========================================================================== */
.controls-section {
margin-bottom: var(--spacing-xl);
}
.controls-container {
display: flex;
align-items: center;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.controls-container label {
margin-bottom: 0; /* Align with controls */
flex-shrink: 0; /* Prevent label from shrinking */
}
/* ==========================================================================
Data Sections (Project Details, Tasks, Knowledge)
========================================================================== */
.data-section {
margin-top: var(--spacing-xl);
padding: var(--spacing-lg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-md);
background-color: var(--data-section-bg);
transition:
background-color 0.2s ease-out,
border-color 0.2s ease-out;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
flex-wrap: wrap; /* Allow wrapping for view controls */
gap: var(--spacing-md);
}
.section-header h3 {
margin-bottom: 0; /* Remove bottom margin as it's handled by section-header */
}
/* Project Details Grid Specifics */
#details-content.details-grid {
display: grid;
grid-template-columns: auto 1fr; /* Label and value */
gap: var(--spacing-sm) var(--spacing-md);
align-items: start; /* Align items to the start of their grid cell */
}
#details-content.details-grid > .data-item {
display: contents; /* Allow children (strong, div/pre/ul) to participate in the grid */
}
#details-content.details-grid > .data-item > strong {
/* Label */
font-weight: 500;
color: var(--secondary-text-color);
padding-top: var(--spacing-xs); /* Align with multi-line values better */
grid-column: 1;
}
#details-content.details-grid > .data-item > div,
#details-content.details-grid > .data-item > pre,
#details-content.details-grid > .data-item > ul {
/* Value */
grid-column: 2;
margin-bottom: 0; /* Remove default margin from these elements when in grid */
}
#details-content.details-grid > .data-item > ul {
list-style-position: outside; /* More standard list appearance */
padding-left: var(--spacing-md); /* Indent list items */
margin-top: 0;
}
#details-content.details-grid > .data-item > ul li {
margin-bottom: var(--spacing-xs);
}
/* General Data Items (Used for Tasks, Knowledge in non-grid layout) */
.data-item {
padding-bottom: var(--spacing-md);
margin-bottom: var(--spacing-md);
border-bottom: 1px solid var(--data-item-border-color);
transition: border-color 0.2s ease-out;
}
.data-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.data-item strong {
/* Used in task/knowledge titles */
color: var(--text-color);
font-weight: 600;
display: block; /* Make title take full width */
margin-bottom: var(--spacing-xs);
}
.data-item div {
/* General content div within a data item */
margin-bottom: var(--spacing-xs);
}
/* ==========================================================================
Mermaid Diagram Container
========================================================================== */
.mermaid-container {
width: 100%;
min-height: 300px; /* Adjust as needed */
overflow: auto; /* For larger diagrams */
margin-top: var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-md);
padding: var(--spacing-md);
background-color: var(
--card-bg-color
); /* Match card background for consistency */
box-sizing: border-box; /* Ensure padding and border are included in width/height */
}
.mermaid-container svg {
display: block; /* Remove extra space below SVG */
margin: auto; /* Center if smaller than container */
max-width: 100%; /* Ensure SVG scales down if too wide */
}
/* ==========================================================================
Task Board Styles
========================================================================== */
.task-board-grid {
display: flex;
gap: var(--spacing-md);
overflow-x: auto; /* Allow horizontal scrolling for columns */
padding-bottom: var(--spacing-md); /* Space for scrollbar */
min-height: 300px; /* Ensure columns have some height */
}
.task-board-column {
flex: 0 0 280px; /* Fixed width for columns, adjust as needed */
max-width: 280px;
background-color: var(
--bg-color
); /* Slightly different from card for distinction */
border-radius: var(--border-radius-md);
padding: var(--spacing-md);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: background-color 0.2s ease-out;
}
.task-board-column h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
text-align: center;
}
.task-board-column-content {
flex-grow: 1;
overflow-y: auto; /* Allow scrolling within a column if many tasks */
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
/* ==========================================================================
Data Explorer Styles
========================================================================== */
#data-explorer-container .controls-container {
margin-bottom: var(--spacing-lg); /* Space between controls and content */
}
.explorer-node-list {
max-height: 400px; /* Limit height and allow scrolling */
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-md);
padding: var(--spacing-sm);
margin-bottom: var(--spacing-lg); /* Space before details section */
}
#data-explorer-details {
/* Uses .details-grid, so existing styles apply.
Can add specific overrides if needed */
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_update/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { ProjectResponse } from "../../../types/mcp.js";
import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
/**
* Defines a generic interface for formatting data into a string.
*/
interface ResponseFormatter<T> {
format(data: T): string;
}
/**
* Extends the ProjectResponse to include Neo4j properties structure
*/
interface SingleProjectResponse extends ProjectResponse {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
}
/**
* Interface for bulk project update response
*/
interface BulkProjectResponse {
success: boolean;
message: string;
updated: (ProjectResponse & {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
})[];
errors: {
index: number;
project: {
// Original input for the failed update
id: string;
updates: any;
};
error: {
code: string;
message: string;
details?: any;
};
}[];
}
/**
* Formatter for individual project modification responses
*/
export class SingleProjectUpdateFormatter
implements ResponseFormatter<SingleProjectResponse>
{
format(data: SingleProjectResponse): string {
// Extract project properties from Neo4j structure or direct data
const projectData = data.properties || data;
const {
name,
id,
status,
taskType,
updatedAt,
description,
urls,
completionRequirements,
outputFormat,
createdAt,
} = projectData;
// Create a structured summary section
const summary =
`Project Modified Successfully\n\n` +
`Project: ${name || "Unnamed Project"}\n` +
`ID: ${id || "Unknown ID"}\n` +
`Status: ${status || "Unknown Status"}\n` +
`Type: ${taskType || "Unknown Type"}\n` +
`Updated: ${updatedAt ? new Date(updatedAt).toLocaleString() : "Unknown Date"}\n`;
// Create a comprehensive details section
let details = `Project Details:\n`;
const fieldLabels: Record<keyof SingleProjectResponse, string> = {
id: "ID",
name: "Name",
description: "Description",
status: "Status",
urls: "URLs",
completionRequirements: "Completion Requirements",
outputFormat: "Output Format",
taskType: "Task Type",
createdAt: "Created At",
updatedAt: "Updated At",
properties: "Raw Properties",
identity: "Neo4j Identity",
labels: "Neo4j Labels",
elementId: "Neo4j Element ID",
dependencies: "Dependencies",
};
const relevantKeys: (keyof SingleProjectResponse)[] = [
"id",
"name",
"description",
"status",
"taskType",
"completionRequirements",
"outputFormat",
"urls",
"createdAt",
"updatedAt",
];
relevantKeys.forEach((key) => {
if (projectData[key] !== undefined && projectData[key] !== null) {
let value = projectData[key];
if (Array.isArray(value)) {
value =
value.length > 0
? value
.map((item) =>
typeof item === "object" ? JSON.stringify(item) : item,
)
.join(", ")
: "None";
} else if (
typeof value === "string" &&
(key === "createdAt" || key === "updatedAt")
) {
try {
value = new Date(value).toLocaleString();
} catch (e) {
/* Keep original */
}
}
details += ` ${fieldLabels[key] || key}: ${value}\n`;
}
});
return `${summary}\n${details}`;
}
}
/**
* Formatter for bulk project update responses
*/
export class BulkProjectUpdateFormatter
implements ResponseFormatter<BulkProjectResponse>
{
format(data: BulkProjectResponse): string {
const { success, message, updated, errors } = data;
const summary =
`${success && errors.length === 0 ? "Projects Updated Successfully" : "Project Updates Completed"}\n\n` +
`Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
`Summary: ${message}\n` +
`Updated: ${updated.length} project(s)\n` +
`Errors: ${errors.length} error(s)\n`;
let updatedSection = "";
if (updated.length > 0) {
updatedSection = `\n--- Modified Projects (${updated.length}) ---\n\n`;
updatedSection += updated
.map((project, index) => {
const projectData = project.properties || project;
return (
`${index + 1}. ${projectData.name || "Unnamed Project"} (ID: ${projectData.id || "N/A"})\n` +
` Status: ${projectData.status || "N/A"}\n` +
` Updated: ${projectData.updatedAt ? new Date(projectData.updatedAt).toLocaleString() : "N/A"}`
);
})
.join("\n\n");
}
let errorsSection = "";
if (errors.length > 0) {
errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
errorsSection += errors
.map((errorItem, index) => {
return (
`${index + 1}. Error updating Project ID: "${errorItem.project.id}"\n` +
` Error Code: ${errorItem.error.code}\n` +
` Message: ${errorItem.error.message}` +
(errorItem.error.details
? `\n Details: ${JSON.stringify(errorItem.error.details)}`
: "")
);
})
.join("\n\n");
}
return `${summary}${updatedSection}${errorsSection}`.trim();
}
}
/**
* Create a formatted, human-readable response for the atlas_project_update tool
*
* @param data The raw project modification response (SingleProjectResponse or BulkProjectResponse)
* @param isError Whether this response represents an error condition (primarily for single responses)
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatProjectUpdateResponse(data: any, isError = false): any {
const isBulkResponse =
data.hasOwnProperty("success") &&
data.hasOwnProperty("updated") &&
data.hasOwnProperty("errors");
let formattedText: string;
let finalIsError: boolean;
if (isBulkResponse) {
const formatter = new BulkProjectUpdateFormatter();
const bulkData = data as BulkProjectResponse;
formattedText = formatter.format(bulkData);
finalIsError = !bulkData.success || bulkData.errors.length > 0;
} else {
const formatter = new SingleProjectUpdateFormatter();
// For single response, 'data' is the updated project object.
// 'isError' must be determined by the caller if an error occurred before this point.
formattedText = formatter.format(data as SingleProjectResponse);
finalIsError = isError;
}
return createToolResponse(formattedText, finalIsError);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_update/types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import {
McpToolResponse,
PriorityLevel,
ResponseFormat,
TaskStatus,
TaskType,
createPriorityLevelEnum,
createResponseFormatEnum,
createTaskStatusEnum,
createTaskTypeEnum,
} from "../../../types/mcp.js";
export const TaskUpdateSchema = z.object({
id: z.string().describe("Identifier of the existing task to be modified"),
updates: z
.object({
title: z
.string()
.min(5)
.max(150)
.optional()
.describe("Modified task title (5-150 characters)"),
description: z
.string()
.optional()
.describe("Revised task description and requirements"),
priority: createPriorityLevelEnum()
.optional()
.describe("Updated priority level reflecting current importance"),
status: createTaskStatusEnum()
.optional()
.describe("Updated task status reflecting current progress"),
assignedTo: z.string().nullable().optional().describe(
// Allow null for unassignment
"Updated assignee ID for task responsibility (null to unassign)",
),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional()
.describe("Modified reference materials and documentation links"),
tags: z
.array(z.string())
.optional()
.describe("Updated categorical labels for task organization"),
completionRequirements: z
.string()
.optional()
.describe("Revised success criteria for task completion"),
outputFormat: z
.string()
.optional()
.describe("Modified deliverable specification for task output"),
taskType: createTaskTypeEnum()
.optional()
.describe("Revised classification for task categorization"),
})
.describe(
"Partial update object containing only fields that need modification",
),
});
const SingleTaskUpdateSchema = z
.object({
mode: z.literal("single"),
id: z.string(),
updates: z.object({
title: z.string().min(5).max(150).optional(),
description: z.string().optional(),
priority: createPriorityLevelEnum().optional(),
status: createTaskStatusEnum().optional(),
assignedTo: z.string().nullable().optional(), // Allow null
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional(),
tags: z.array(z.string()).optional(),
completionRequirements: z.string().optional(),
outputFormat: z.string().optional(),
taskType: createTaskTypeEnum().optional(),
}),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
})
.describe("Update an individual task with selective field modifications");
const BulkTaskUpdateSchema = z
.object({
mode: z.literal("bulk"),
tasks: z
.array(
z.object({
id: z.string().describe("Identifier of the task to update"),
updates: z.object({
title: z.string().min(5).max(150).optional(),
description: z.string().optional(),
priority: createPriorityLevelEnum().optional(),
status: createTaskStatusEnum().optional(),
assignedTo: z.string().nullable().optional(), // Allow null
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional(),
tags: z.array(z.string()).optional(),
completionRequirements: z.string().optional(),
outputFormat: z.string().optional(),
taskType: createTaskTypeEnum().optional(),
}),
}),
)
.min(1)
.max(100)
.describe(
"Collection of task updates to be applied in a single transaction",
),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
})
.describe("Update multiple related tasks in a single efficient transaction");
// Schema shapes for tool registration
export const AtlasTaskUpdateSchemaShape = {
mode: z
.enum(["single", "bulk"])
.describe(
"Operation mode - 'single' for one task, 'bulk' for multiple tasks",
),
id: z
.string()
.optional()
.describe("Existing task ID to update (required for mode='single')"),
updates: z
.object({
title: z.string().min(5).max(150).optional(),
description: z.string().optional(),
priority: createPriorityLevelEnum().optional(),
status: createTaskStatusEnum().optional(),
assignedTo: z.string().nullable().optional(), // Allow null
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional(),
tags: z.array(z.string()).optional(),
completionRequirements: z.string().optional(),
outputFormat: z.string().optional(),
taskType: createTaskTypeEnum().optional(),
})
.optional()
.describe(
"Object containing fields to modify (only specified fields will be updated) (required for mode='single')",
),
tasks: z
.array(
z.object({
id: z.string(),
updates: z.object({
title: z.string().min(5).max(150).optional(),
description: z.string().optional(),
priority: createPriorityLevelEnum().optional(),
status: createTaskStatusEnum().optional(),
assignedTo: z.string().nullable().optional(), // Allow null
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional(),
tags: z.array(z.string()).optional(),
completionRequirements: z.string().optional(),
outputFormat: z.string().optional(),
taskType: createTaskTypeEnum().optional(),
}),
}),
)
.optional()
.describe(
"Array of task updates, each containing an ID and updates object (required for mode='bulk')",
),
responseFormat: createResponseFormatEnum()
.optional()
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
} as const;
// Schema for validation
export const AtlasTaskUpdateSchema = z.discriminatedUnion("mode", [
SingleTaskUpdateSchema,
BulkTaskUpdateSchema,
]);
export type AtlasTaskUpdateInput = z.infer<typeof AtlasTaskUpdateSchema>;
export type TaskUpdateInput = z.infer<typeof TaskUpdateSchema>;
export type AtlasTaskUpdateResponse = McpToolResponse;
```
--------------------------------------------------------------------------------
/src/utils/security/rateLimiter.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Provides a generic `RateLimiter` class for implementing rate limiting logic.
* It supports configurable time windows, request limits, and automatic cleanup of expired entries.
* @module src/utils/security/rateLimiter
*/
import { environment } from "../../config/index.js";
import { BaseErrorCode, McpError } from "../../types/errors.js";
import { logger, RequestContext, requestContextService } from "../index.js";
/**
* Defines configuration options for the {@link RateLimiter}.
*/
export interface RateLimitConfig {
/** Time window in milliseconds. */
windowMs: number;
/** Maximum number of requests allowed in the window. */
maxRequests: number;
/** Custom error message template. Can include `{waitTime}` placeholder. */
errorMessage?: string;
/** If true, skip rate limiting in development. */
skipInDevelopment?: boolean;
/** Optional function to generate a custom key for rate limiting. */
keyGenerator?: (identifier: string, context?: RequestContext) => string;
/** How often, in milliseconds, to clean up expired entries. */
cleanupInterval?: number;
}
/**
* Represents an individual entry for tracking requests against a rate limit key.
*/
export interface RateLimitEntry {
/** Current request count. */
count: number;
/** When the window resets (timestamp in milliseconds). */
resetTime: number;
}
/**
* A generic rate limiter class using an in-memory store.
* Controls frequency of operations based on unique keys.
*/
export class RateLimiter {
/**
* Stores current request counts and reset times for each key.
* @private
*/
private limits: Map<string, RateLimitEntry>;
/**
* Timer ID for periodic cleanup.
* @private
*/
private cleanupTimer: NodeJS.Timeout | null = null;
/**
* Default configuration values.
* @private
*/
private static DEFAULT_CONFIG: RateLimitConfig = {
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 100,
errorMessage:
"Rate limit exceeded. Please try again in {waitTime} seconds.",
skipInDevelopment: false,
cleanupInterval: 5 * 60 * 1000, // 5 minutes
};
/**
* Creates a new `RateLimiter` instance.
* @param config - Configuration options, merged with defaults.
*/
constructor(private config: RateLimitConfig) {
this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config };
this.limits = new Map();
this.startCleanupTimer();
}
/**
* Starts the periodic timer to clean up expired rate limit entries.
* @private
*/
private startCleanupTimer(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
const interval =
this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval;
if (interval && interval > 0) {
this.cleanupTimer = setInterval(() => {
this.cleanupExpiredEntries();
}, interval);
if (this.cleanupTimer.unref) {
this.cleanupTimer.unref(); // Allow Node.js process to exit if only timer active
}
}
}
/**
* Removes expired rate limit entries from the store.
* @private
*/
private cleanupExpiredEntries(): void {
const now = Date.now();
let expiredCount = 0;
for (const [key, entry] of this.limits.entries()) {
if (now >= entry.resetTime) {
this.limits.delete(key);
expiredCount++;
}
}
if (expiredCount > 0) {
const logContext = requestContextService.createRequestContext({
operation: "RateLimiter.cleanupExpiredEntries",
cleanedCount: expiredCount,
totalRemainingAfterClean: this.limits.size,
});
logger.debug(
`Cleaned up ${expiredCount} expired rate limit entries`,
logContext,
);
}
}
/**
* Updates the configuration of the rate limiter instance.
* @param config - New configuration options to merge.
*/
public configure(config: Partial<RateLimitConfig>): void {
this.config = { ...this.config, ...config };
if (config.cleanupInterval !== undefined) {
this.startCleanupTimer();
}
}
/**
* Retrieves a copy of the current rate limiter configuration.
* @returns The current configuration.
*/
public getConfig(): RateLimitConfig {
return { ...this.config };
}
/**
* Resets all rate limits by clearing the internal store.
*/
public reset(): void {
this.limits.clear();
const logContext = requestContextService.createRequestContext({
operation: "RateLimiter.reset",
});
logger.debug("Rate limiter reset, all limits cleared", logContext);
}
/**
* Checks if a request exceeds the configured rate limit.
* Throws an `McpError` if the limit is exceeded.
*
* @param key - A unique identifier for the request source.
* @param context - Optional request context for custom key generation.
* @throws {McpError} If the rate limit is exceeded.
*/
public check(key: string, context?: RequestContext): void {
if (this.config.skipInDevelopment && environment === "development") {
return;
}
const limitKey = this.config.keyGenerator
? this.config.keyGenerator(key, context)
: key;
const now = Date.now();
const entry = this.limits.get(limitKey);
if (!entry || now >= entry.resetTime) {
this.limits.set(limitKey, {
count: 1,
resetTime: now + this.config.windowMs,
});
return;
}
if (entry.count >= this.config.maxRequests) {
const waitTime = Math.ceil((entry.resetTime - now) / 1000);
const errorMessage = (
this.config.errorMessage || RateLimiter.DEFAULT_CONFIG.errorMessage!
).replace("{waitTime}", waitTime.toString());
throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, {
waitTimeSeconds: waitTime,
key: limitKey,
limit: this.config.maxRequests,
windowMs: this.config.windowMs,
});
}
entry.count++;
}
/**
* Retrieves the current rate limit status for a specific key.
* @param key - The rate limit key.
* @returns Status object or `null` if no entry exists.
*/
public getStatus(key: string): {
current: number;
limit: number;
remaining: number;
resetTime: number;
} | null {
const entry = this.limits.get(key);
if (!entry) {
return null;
}
return {
current: entry.count,
limit: this.config.maxRequests,
remaining: Math.max(0, this.config.maxRequests - entry.count),
resetTime: entry.resetTime,
};
}
/**
* Stops the cleanup timer and clears all rate limit entries.
* Call when the rate limiter is no longer needed.
*/
public dispose(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
this.limits.clear();
}
}
/**
* Default singleton instance of the `RateLimiter`.
* Initialized with default configuration. Use `rateLimiter.configure({})` to customize.
*/
export const rateLimiter = new RateLimiter({
windowMs: 15 * 60 * 1000, // Default: 15 minutes
maxRequests: 100, // Default: 100 requests per window
});
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_add/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
/**
* Defines a generic interface for formatting data into a string.
*/
interface ResponseFormatter<T> {
format(data: T): string;
}
/**
* Interface for a single knowledge item response
*/
interface SingleKnowledgeResponse {
id: string;
projectId: string;
text: string;
tags?: string[];
domain: string;
citations?: string[];
createdAt: string;
updatedAt: string;
properties?: any; // Neo4j properties if not fully mapped
identity?: number; // Neo4j internal ID
labels?: string[]; // Neo4j labels
elementId?: string; // Neo4j element ID
}
/**
* Interface for bulk knowledge addition response
*/
interface BulkKnowledgeResponse {
success: boolean;
message: string;
created: (SingleKnowledgeResponse & {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
})[];
errors: {
index: number;
knowledge: any; // Original input for the failed item
error: {
code: string;
message: string;
details?: any;
};
}[];
}
/**
* Formatter for single knowledge item addition responses
*/
export class SingleKnowledgeFormatter
implements ResponseFormatter<SingleKnowledgeResponse>
{
format(data: SingleKnowledgeResponse): string {
// Extract knowledge properties from Neo4j structure or direct data
const knowledgeData = data.properties || data;
const {
id,
projectId,
domain,
createdAt,
text,
tags,
citations,
updatedAt,
} = knowledgeData;
// Create a summary section
const summary =
`Knowledge Item Added Successfully\n\n` +
`ID: ${id || "Unknown ID"}\n` +
`Project ID: ${projectId || "Unknown Project"}\n` +
`Domain: ${domain || "Uncategorized"}\n` +
`Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
// Create a comprehensive details section
const fieldLabels: Record<keyof SingleKnowledgeResponse, string> = {
id: "ID",
projectId: "Project ID",
text: "Content",
tags: "Tags",
domain: "Domain",
citations: "Citations",
createdAt: "Created At",
updatedAt: "Updated At",
// Neo4j specific fields are generally not for direct user display unless needed
properties: "Raw Properties",
identity: "Neo4j Identity",
labels: "Neo4j Labels",
elementId: "Neo4j Element ID",
};
let details = `Knowledge Item Details\n\n`;
// Build details as key-value pairs for relevant fields
(Object.keys(fieldLabels) as Array<keyof SingleKnowledgeResponse>).forEach(
(key) => {
if (
knowledgeData[key] !== undefined &&
["properties", "identity", "labels", "elementId"].indexOf(
key as string,
) === -1
) {
// Exclude raw Neo4j fields from default display
let value = knowledgeData[key];
if (Array.isArray(value)) {
value = value.length > 0 ? value.join(", ") : "None";
} else if (
typeof value === "string" &&
(key === "createdAt" || key === "updatedAt")
) {
try {
value = new Date(value).toLocaleString();
} catch (e) {
/* Keep original if parsing fails */
}
}
if (
key === "text" &&
typeof value === "string" &&
value.length > 100
) {
value = value.substring(0, 100) + "... (truncated)";
}
details += `${fieldLabels[key]}: ${value}\n`;
}
},
);
return `${summary}\n${details}`;
}
}
/**
* Formatter for bulk knowledge addition responses
*/
export class BulkKnowledgeFormatter
implements ResponseFormatter<BulkKnowledgeResponse>
{
format(data: BulkKnowledgeResponse): string {
const { success, message, created, errors } = data;
const summary =
`${success && errors.length === 0 ? "Knowledge Items Added Successfully" : "Knowledge Addition Completed"}\n\n` +
`Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
`Summary: ${message}\n` +
`Added: ${created.length} item(s)\n` +
`Errors: ${errors.length} error(s)\n`;
let createdSection = "";
if (created.length > 0) {
createdSection = `\n--- Added Knowledge Items (${created.length}) ---\n\n`;
createdSection += created
.map((item, index) => {
const itemData = item.properties || item;
return (
`${index + 1}. ID: ${itemData.id || "N/A"}\n` +
` Project ID: ${itemData.projectId || "N/A"}\n` +
` Domain: ${itemData.domain || "N/A"}\n` +
` Tags: ${itemData.tags ? itemData.tags.join(", ") : "None"}\n` +
` Created: ${itemData.createdAt ? new Date(itemData.createdAt).toLocaleString() : "N/A"}`
);
})
.join("\n\n");
}
let errorsSection = "";
if (errors.length > 0) {
errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
errorsSection += errors
.map((errorDetail, index) => {
const itemInput = errorDetail.knowledge;
return (
`${index + 1}. Error for item (Index: ${errorDetail.index})\n` +
` Input Project ID: ${itemInput?.projectId || "N/A"}\n` +
` Input Domain: ${itemInput?.domain || "N/A"}\n` +
` Error Code: ${errorDetail.error.code}\n` +
` Message: ${errorDetail.error.message}` +
(errorDetail.error.details
? `\n Details: ${JSON.stringify(errorDetail.error.details)}`
: "")
);
})
.join("\n\n");
}
return `${summary}${createdSection}${errorsSection}`.trim();
}
}
/**
* Create a formatted, human-readable response for the atlas_knowledge_add tool
*
* @param data The raw knowledge addition response data (can be SingleKnowledgeResponse or BulkKnowledgeResponse)
* @param isError Whether this response represents an error condition (primarily for single responses if not inherent in data)
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatKnowledgeAddResponse(data: any, isError = false): any {
const isBulkResponse =
data.hasOwnProperty("success") &&
data.hasOwnProperty("created") &&
data.hasOwnProperty("errors");
let formattedText: string;
let finalIsError: boolean;
if (isBulkResponse) {
const formatter = new BulkKnowledgeFormatter();
formattedText = formatter.format(data as BulkKnowledgeResponse);
finalIsError = !data.success || data.errors.length > 0;
} else {
const formatter = new SingleKnowledgeFormatter();
formattedText = formatter.format(data as SingleKnowledgeResponse);
finalIsError = isError; // For single responses, rely on the passed isError or enhance if data has success field
}
return createToolResponse(formattedText, finalIsError);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_create/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { TaskResponse } from "../../../types/mcp.js";
import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
/**
* Defines a generic interface for formatting data into a string.
*/
interface ResponseFormatter<T> {
format(data: T): string;
}
/**
* Extends the TaskResponse to include Neo4j properties structure
*/
interface SingleTaskResponse extends TaskResponse {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
}
/**
* Interface for bulk task creation response
*/
interface BulkTaskResponse {
success: boolean;
message: string;
created: (TaskResponse & {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
})[];
errors: {
index: number;
task: any; // Original input for the failed task
error: {
code: string;
message: string;
details?: any;
};
}[];
}
/**
* Formatter for single task creation responses
*/
export class SingleTaskFormatter
implements ResponseFormatter<SingleTaskResponse>
{
format(data: SingleTaskResponse): string {
// Extract task properties from Neo4j structure or direct data
const taskData = data.properties || data;
const {
title,
id,
projectId,
status,
priority,
taskType,
createdAt,
description,
assignedTo,
urls,
tags,
completionRequirements,
dependencies,
outputFormat,
updatedAt,
} = taskData;
// Create a summary section
const summary =
`Task Created Successfully\n\n` +
`Task: ${title || "Unnamed Task"}\n` +
`ID: ${id || "Unknown ID"}\n` +
`Project ID: ${projectId || "Unknown Project"}\n` +
`Status: ${status || "Unknown Status"}\n` +
`Priority: ${priority || "Unknown Priority"}\n` +
`Type: ${taskType || "Unknown Type"}\n` +
`Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
// Create a comprehensive details section
let details = `Task Details:\n`;
const fieldLabels: Record<keyof SingleTaskResponse, string> = {
id: "ID",
projectId: "Project ID",
title: "Title",
description: "Description",
priority: "Priority",
status: "Status",
assignedTo: "Assigned To",
urls: "URLs",
tags: "Tags",
completionRequirements: "Completion Requirements",
dependencies: "Dependencies",
outputFormat: "Output Format",
taskType: "Task Type",
createdAt: "Created At",
updatedAt: "Updated At",
properties: "Raw Properties",
identity: "Neo4j Identity",
labels: "Neo4j Labels",
elementId: "Neo4j Element ID",
};
const relevantKeys: (keyof SingleTaskResponse)[] = [
"id",
"projectId",
"title",
"description",
"priority",
"status",
"assignedTo",
"urls",
"tags",
"completionRequirements",
"dependencies",
"outputFormat",
"taskType",
"createdAt",
"updatedAt",
];
relevantKeys.forEach((key) => {
if (taskData[key] !== undefined && taskData[key] !== null) {
let value = taskData[key];
if (Array.isArray(value)) {
value =
value.length > 0
? value
.map((item) =>
typeof item === "object" ? JSON.stringify(item) : item,
)
.join(", ")
: "None";
} else if (
typeof value === "string" &&
(key === "createdAt" || key === "updatedAt")
) {
try {
value = new Date(value).toLocaleString();
} catch (e) {
/* Keep original */
}
}
details += ` ${fieldLabels[key] || key}: ${value}\n`;
}
});
return `${summary}\n${details}`;
}
}
/**
* Formatter for bulk task creation responses
*/
export class BulkTaskFormatter implements ResponseFormatter<BulkTaskResponse> {
format(data: BulkTaskResponse): string {
const { success, message, created, errors } = data;
const summary =
`${success && errors.length === 0 ? "Tasks Created Successfully" : "Task Creation Completed"}\n\n` +
`Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
`Summary: ${message}\n` +
`Created: ${created.length} task(s)\n` +
`Errors: ${errors.length} error(s)\n`;
let createdSection = "";
if (created.length > 0) {
createdSection = `\n--- Created Tasks (${created.length}) ---\n\n`;
createdSection += created
.map((task, index) => {
const taskData = task.properties || task;
return (
`${index + 1}. ${taskData.title || "Unnamed Task"} (ID: ${taskData.id || "N/A"})\n` +
` Project ID: ${taskData.projectId || "N/A"}\n` +
` Priority: ${taskData.priority || "N/A"}\n` +
` Status: ${taskData.status || "N/A"}\n` +
` Created: ${taskData.createdAt ? new Date(taskData.createdAt).toLocaleString() : "N/A"}`
);
})
.join("\n\n");
}
let errorsSection = "";
if (errors.length > 0) {
errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
errorsSection += errors
.map((errorItem, index) => {
const taskTitle =
errorItem.task?.title || `Input task at index ${errorItem.index}`;
return (
`${index + 1}. Error for task: "${taskTitle}" (Project ID: ${errorItem.task?.projectId || "N/A"})\n` +
` Error Code: ${errorItem.error.code}\n` +
` Message: ${errorItem.error.message}` +
(errorItem.error.details
? `\n Details: ${JSON.stringify(errorItem.error.details)}`
: "")
);
})
.join("\n\n");
}
return `${summary}${createdSection}${errorsSection}`.trim();
}
}
/**
* Create a formatted, human-readable response for the atlas_task_create tool
*
* @param data The raw task creation response data (SingleTaskResponse or BulkTaskResponse)
* @param isError Whether this response represents an error condition (primarily for single responses)
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatTaskCreateResponse(data: any, isError = false): any {
const isBulkResponse =
data.hasOwnProperty("success") &&
data.hasOwnProperty("created") &&
data.hasOwnProperty("errors");
let formattedText: string;
let finalIsError: boolean;
if (isBulkResponse) {
const formatter = new BulkTaskFormatter();
const bulkData = data as BulkTaskResponse;
formattedText = formatter.format(bulkData);
finalIsError = !bulkData.success || bulkData.errors.length > 0;
} else {
const formatter = new SingleTaskFormatter();
// For single response, 'data' is the created task object.
// 'isError' must be determined by the caller if an error occurred before this point.
formattedText = formatter.format(data as SingleTaskResponse);
finalIsError = isError;
}
return createToolResponse(formattedText, finalIsError);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_create/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { ProjectResponse } from "../../../types/mcp.js";
import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
/**
* Defines a generic interface for formatting data into a string.
*/
interface ResponseFormatter<T> {
format(data: T): string;
}
/**
* Extends the ProjectResponse to include Neo4j properties structure
*/
interface SingleProjectResponse extends ProjectResponse {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
}
/**
* Interface for bulk project creation response
*/
interface BulkProjectResponse {
success: boolean;
message: string;
created: (ProjectResponse & {
properties?: any;
identity?: number;
labels?: string[];
elementId?: string;
})[];
errors: {
index: number;
project: any; // Original input for the failed project
error: {
code: string;
message: string;
details?: any;
};
}[];
}
/**
* Formatter for single project creation responses
*/
export class SingleProjectFormatter
implements ResponseFormatter<SingleProjectResponse>
{
format(data: SingleProjectResponse): string {
// Extract project properties from Neo4j structure or direct data
const projectData = data.properties || data;
const {
name,
id,
status,
taskType,
createdAt,
description,
urls,
completionRequirements,
outputFormat,
updatedAt,
} = projectData;
// Create a summary section
const summary =
`Project Created Successfully\n\n` +
`Project: ${name || "Unnamed Project"}\n` +
`ID: ${id || "Unknown ID"}\n` +
`Status: ${status || "Unknown Status"}\n` +
`Type: ${taskType || "Unknown Type"}\n` +
`Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
// Create a comprehensive details section
const fieldLabels: Record<keyof SingleProjectResponse, string> = {
id: "ID",
name: "Name",
description: "Description",
status: "Status",
urls: "URLs",
completionRequirements: "Completion Requirements",
outputFormat: "Output Format",
taskType: "Task Type",
createdAt: "Created At",
updatedAt: "Updated At",
// Neo4j specific fields
properties: "Raw Properties",
identity: "Neo4j Identity",
labels: "Neo4j Labels",
elementId: "Neo4j Element ID",
// Fields from ProjectResponse that might not be in projectData directly if it's just properties
dependencies: "Dependencies", // Assuming ProjectResponse might have this
};
let details = `Project Details:\n`;
// Build details as key-value pairs for relevant fields
const relevantKeys: (keyof SingleProjectResponse)[] = [
"id",
"name",
"description",
"status",
"taskType",
"completionRequirements",
"outputFormat",
"urls",
"createdAt",
"updatedAt",
];
relevantKeys.forEach((key) => {
if (projectData[key] !== undefined && projectData[key] !== null) {
let value = projectData[key];
if (Array.isArray(value)) {
value =
value.length > 0
? value
.map((item) =>
typeof item === "object" ? JSON.stringify(item) : item,
)
.join(", ")
: "None";
} else if (
typeof value === "string" &&
(key === "createdAt" || key === "updatedAt")
) {
try {
value = new Date(value).toLocaleString();
} catch (e) {
/* Keep original if parsing fails */
}
}
details += ` ${fieldLabels[key] || key}: ${value}\n`;
}
});
return `${summary}\n${details}`;
}
}
/**
* Formatter for bulk project creation responses
*/
export class BulkProjectFormatter
implements ResponseFormatter<BulkProjectResponse>
{
format(data: BulkProjectResponse): string {
const { success, message, created, errors } = data;
const summary =
`${success && errors.length === 0 ? "Projects Created Successfully" : "Project Creation Completed"}\n\n` +
`Status: ${success && errors.length === 0 ? "✅ Success" : errors.length > 0 ? "⚠️ Partial Success / Errors" : "✅ Success (No items or no errors)"}\n` +
`Summary: ${message}\n` +
`Created: ${created.length} project(s)\n` +
`Errors: ${errors.length} error(s)\n`;
let createdSection = "";
if (created.length > 0) {
createdSection = `\n--- Created Projects (${created.length}) ---\n\n`;
createdSection += created
.map((project, index) => {
const projectData = project.properties || project;
return (
`${index + 1}. ${projectData.name || "Unnamed Project"} (ID: ${projectData.id || "N/A"})\n` +
` Type: ${projectData.taskType || "N/A"}\n` +
` Status: ${projectData.status || "N/A"}\n` +
` Created: ${projectData.createdAt ? new Date(projectData.createdAt).toLocaleString() : "N/A"}`
);
})
.join("\n\n");
}
let errorsSection = "";
if (errors.length > 0) {
errorsSection = `\n--- Errors Encountered (${errors.length}) ---\n\n`;
errorsSection += errors
.map((errorItem, index) => {
const projectName =
errorItem.project?.name ||
`Input project at index ${errorItem.index}`;
return (
`${index + 1}. Error for project: "${projectName}"\n` +
` Error Code: ${errorItem.error.code}\n` +
` Message: ${errorItem.error.message}` +
(errorItem.error.details
? `\n Details: ${JSON.stringify(errorItem.error.details)}`
: "")
);
})
.join("\n\n");
}
return `${summary}${createdSection}${errorsSection}`.trim();
}
}
/**
* Create a formatted, human-readable response for the atlas_project_create tool
*
* @param data The raw project creation response data (SingleProjectResponse or BulkProjectResponse)
* @param isError Whether this response represents an error condition (primarily for single responses)
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatProjectCreateResponse(data: any, isError = false): any {
const isBulkResponse =
data.hasOwnProperty("success") &&
data.hasOwnProperty("created") &&
data.hasOwnProperty("errors");
let formattedText: string;
let finalIsError: boolean;
if (isBulkResponse) {
const formatter = new BulkProjectFormatter();
const bulkData = data as BulkProjectResponse;
formattedText = formatter.format(bulkData);
finalIsError = !bulkData.success || bulkData.errors.length > 0;
} else {
const formatter = new SingleProjectFormatter();
// For single response, the 'data' is the project object itself.
// 'isError' must be determined by the caller if an error occurred before this point.
// If 'data' represents a successfully created project, isError should be false.
formattedText = formatter.format(data as SingleProjectResponse);
finalIsError = isError;
}
return createToolResponse(formattedText, finalIsError);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_task_create/types.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import {
McpToolResponse,
PriorityLevel,
ResponseFormat,
TaskStatus,
TaskType,
createPriorityLevelEnum,
createResponseFormatEnum,
createTaskStatusEnum,
createTaskTypeEnum,
} from "../../../types/mcp.js";
export const TaskSchema = z.object({
id: z.string().optional().describe("Optional client-generated task ID"),
projectId: z
.string()
.describe("ID of the parent project this task belongs to"),
title: z
.string()
.min(5)
.max(150)
.describe(
"Concise task title clearly describing the objective (5-150 characters)",
),
description: z
.string()
.describe("Detailed explanation of the task requirements and context"),
priority: createPriorityLevelEnum()
.default(PriorityLevel.MEDIUM)
.describe("Importance level"),
status: createTaskStatusEnum()
.default(TaskStatus.TODO)
.describe("Current task state"),
assignedTo: z
.string()
.optional()
.describe("ID of entity responsible for task completion"),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional()
.describe("Relevant URLs with descriptive titles for reference materials"),
tags: z
.array(z.string())
.optional()
.describe("Categorical labels for organization and filtering"),
completionRequirements: z
.string()
.describe("Specific, measurable criteria that indicate task completion"),
dependencies: z
.array(z.string())
.optional()
.describe(
"Array of existing task IDs that must be completed before this task can begin",
),
outputFormat: z
.string()
.describe("Required format specification for task deliverables"),
taskType: createTaskTypeEnum()
.or(z.string())
.describe("Classification of task purpose"),
});
const SingleTaskSchema = z
.object({
mode: z.literal("single"),
id: z.string().optional(),
projectId: z.string(),
title: z.string().min(5).max(150),
description: z.string(),
priority: createPriorityLevelEnum()
.optional()
.default(PriorityLevel.MEDIUM),
status: createTaskStatusEnum().optional().default(TaskStatus.TODO),
assignedTo: z.string().optional(),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional(),
tags: z.array(z.string()).optional(),
completionRequirements: z.string(),
dependencies: z.array(z.string()).optional(),
outputFormat: z.string(),
taskType: createTaskTypeEnum().or(z.string()),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
})
.describe("Creates a single task with comprehensive details and metadata");
const BulkTaskSchema = z
.object({
mode: z.literal("bulk"),
tasks: z
.array(TaskSchema)
.min(1)
.max(100)
.describe(
"Collection of task definitions to create in a single operation. Each object must include all fields required for single task creation (projectId, title, description, completionRequirements, outputFormat, taskType).",
),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
})
.describe("Create multiple related tasks in a single efficient transaction");
// Schema shapes for tool registration
export const AtlasTaskCreateSchemaShape = {
mode: z
.enum(["single", "bulk"])
.describe(
"Operation mode - 'single' for creating one detailed task with full specifications, 'bulk' for efficiently creating multiple related tasks in a single transaction",
),
id: z
.string()
.optional()
.describe(
"Optional client-generated task ID for consistent cross-referencing",
),
projectId: z
.string()
.optional()
.describe(
"ID of the parent project this task belongs to, establishing the project-task relationship hierarchy (required for mode='single')",
),
title: z
.string()
.min(5)
.max(150)
.optional()
.describe(
"Concise task title clearly describing the objective (5-150 characters) for display and identification (required for mode='single')",
),
description: z
.string()
.optional()
.describe(
"Detailed explanation of the task requirements, context, approach, and implementation details (required for mode='single')",
),
priority: createPriorityLevelEnum()
.optional()
.describe(
"Importance level for task prioritization and resource allocation (Default: medium)",
),
status: createTaskStatusEnum()
.optional()
.describe(
"Current task workflow state for tracking task lifecycle and progress (Default: todo)",
),
assignedTo: z
.string()
.optional()
.describe(
"ID of entity responsible for task completion and accountability tracking",
),
urls: z
.array(
z.object({
title: z.string(),
url: z.string(),
}),
)
.optional()
.describe(
"Array of relevant URLs with descriptive titles for reference materials",
),
tags: z
.array(z.string())
.optional()
.describe(
"Array of categorical labels for task organization, filtering, and thematic grouping",
),
completionRequirements: z
.string()
.optional()
.describe(
"Specific, measurable criteria that define when the task is considered complete and ready for verification (required for mode='single')",
),
dependencies: z
.array(z.string())
.optional()
.describe(
"Array of existing task IDs that must be completed before this task can begin, creating sequential workflow paths and prerequisites",
),
outputFormat: z
.string()
.optional()
.describe(
"Required format and structure specification for the task's deliverables, artifacts, and documentation (required for mode='single')",
),
taskType: createTaskTypeEnum()
.or(z.string())
.optional()
.describe(
"Classification of task purpose for workflow organization, filtering, and reporting (required for mode='single')",
),
tasks: z
.array(TaskSchema)
.min(1)
.max(100)
.optional()
.describe(
"Array of complete task definition objects to create in a single transaction (supports 1-100 tasks, required for mode='bulk'). Each object must include all fields required for single task creation (projectId, title, description, completionRequirements, outputFormat, taskType).",
),
responseFormat: createResponseFormatEnum()
.optional()
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
} as const;
// Schema for validation
export const AtlasTaskCreateSchema = z.discriminatedUnion("mode", [
SingleTaskSchema,
BulkTaskSchema,
]);
export type AtlasTaskCreateInput = z.infer<typeof AtlasTaskCreateSchema>;
export type TaskInput = z.infer<typeof TaskSchema>;
export type AtlasTaskCreateResponse = McpToolResponse;
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_list/responseFormat.ts:
--------------------------------------------------------------------------------
```typescript
import { createToolResponse } from "../../../types/mcp.js"; // Import the new response creator
import { Project, ProjectListResponse } from "./types.js";
/**
* Defines a generic interface for formatting data into a string.
*/
interface ResponseFormatter<T> {
format(data: T): string;
}
/**
* Formatter for structured project query responses
*/
export class ProjectListFormatter
implements ResponseFormatter<ProjectListResponse>
{
/**
* Get an emoji indicator for the task status
*/
private getStatusEmoji(status: string): string {
switch (status) {
case "backlog":
return "📋";
case "todo":
return "📝";
case "in_progress":
return "🔄";
case "completed":
return "✅";
default:
return "❓";
}
}
/**
* Get a visual indicator for the priority level
*/
private getPriorityIndicator(priority: string): string {
switch (priority) {
case "critical":
return "[!!!]";
case "high":
return "[!!]";
case "medium":
return "[!]";
case "low":
return "[-]";
default:
return "[?]";
}
}
format(data: ProjectListResponse): string {
const { projects, total, page, limit, totalPages } = data;
// Generate result summary section
const summary =
`Project Portfolio\n\n` +
`Total Entities: ${total}\n` +
`Page: ${page} of ${totalPages}\n` +
`Displaying: ${Math.min(limit, projects.length)} project(s) per page\n`;
if (projects.length === 0) {
return `${summary}\n\nNo project entities matched the specified criteria`;
}
// Format each project
const projectsSections = projects
.map((project, index) => {
// Access properties directly from the project object
const { name, id, status, taskType, createdAt } = project;
let projectSection =
`${index + 1}. ${name || "Unnamed Project"}\n\n` +
`ID: ${id || "Unknown ID"}\n` +
`Status: ${status || "Unknown Status"}\n` +
`Type: ${taskType || "Unknown Type"}\n` +
`Created: ${createdAt ? new Date(createdAt).toLocaleString() : "Unknown Date"}\n`;
// Add project details in plain text format
projectSection += `\nProject Details\n\n`;
// Add each property with proper formatting, accessing directly from 'project'
if (project.id) projectSection += `ID: ${project.id}\n`;
if (project.name) projectSection += `Name: ${project.name}\n`;
if (project.description)
projectSection += `Description: ${project.description}\n`;
if (project.status) projectSection += `Status: ${project.status}\n`;
// Format URLs array
if (project.urls) {
const urlsValue =
Array.isArray(project.urls) && project.urls.length > 0
? project.urls
.map((u) => `${u.title}: ${u.url}`)
.join("\n ") // Improved formatting for URLs
: "None";
projectSection += `URLs: ${urlsValue}\n`;
}
if (project.completionRequirements)
projectSection += `Completion Requirements: ${project.completionRequirements}\n`;
if (project.outputFormat)
projectSection += `Output Format: ${project.outputFormat}\n`;
if (project.taskType)
projectSection += `Task Type: ${project.taskType}\n`;
// Format dates
if (project.createdAt) {
const createdDate =
typeof project.createdAt === "string" &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(project.createdAt)
? new Date(project.createdAt).toLocaleString()
: project.createdAt;
projectSection += `Created At: ${createdDate}\n`;
}
if (project.updatedAt) {
const updatedDate =
typeof project.updatedAt === "string" &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(project.updatedAt)
? new Date(project.updatedAt).toLocaleString()
: project.updatedAt;
projectSection += `Updated At: ${updatedDate}\n`;
}
// Add tasks if included
if (project.tasks && project.tasks.length > 0) {
projectSection += `\nTasks (${project.tasks.length}):\n`;
projectSection += project.tasks
.map((task, taskIndex) => {
const taskTitle = task.title || "Unnamed Task";
const taskId = task.id || "Unknown ID";
const taskStatus = task.status || "Unknown Status";
const taskPriority = task.priority || "Unknown Priority";
const taskCreatedAt = task.createdAt
? new Date(task.createdAt).toLocaleString()
: "Unknown Date";
const statusEmoji = this.getStatusEmoji(taskStatus);
const priorityIndicator = this.getPriorityIndicator(taskPriority);
return (
` ${taskIndex + 1}. ${statusEmoji} ${priorityIndicator} ${taskTitle}\n` +
` ID: ${taskId}\n` +
` Status: ${taskStatus}\n` +
` Priority: ${taskPriority}\n` +
` Created: ${taskCreatedAt}`
);
})
.join("\n\n");
projectSection += "\n";
}
// Add knowledge if included
if (project.knowledge && project.knowledge.length > 0) {
projectSection += `\nKnowledge Items (${project.knowledge.length}):\n`;
projectSection += project.knowledge
.map((item, itemIndex) => {
return (
` ${itemIndex + 1}. ${item.domain || "Uncategorized"} (ID: ${item.id || "N/A"})\n` +
` Tags: ${item.tags && item.tags.length > 0 ? item.tags.join(", ") : "None"}\n` +
` Created: ${item.createdAt ? new Date(item.createdAt).toLocaleString() : "N/A"}\n` +
` Content Preview: ${item.text || "No content available"}`
); // Preview already truncated if needed
})
.join("\n\n");
projectSection += "\n";
}
return projectSection;
})
.join("\n\n----------\n\n");
// Append pagination metadata for multi-page results
let paginationInfo = "";
if (totalPages > 1) {
paginationInfo =
`\n\nPagination Controls:\n` + // Added colon for clarity
`Viewing page ${page} of ${totalPages}.\n` +
`${page < totalPages ? "Use 'page' parameter to navigate to additional results." : "You are on the last page."}`;
}
return `${summary}\n\n${projectsSections}${paginationInfo}`;
}
}
/**
* Create a human-readable formatted response for the atlas_project_list tool
*
* @param data The structured project query response data
* @param isError Whether this response represents an error condition
* @returns Formatted MCP tool response with appropriate structure
*/
export function formatProjectListResponse(data: any, isError = false): any {
const formatter = new ProjectListFormatter();
const formattedText = formatter.format(data as ProjectListResponse); // Assuming data is ProjectListResponse
return createToolResponse(formattedText, isError);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_list/listProjects.ts:
--------------------------------------------------------------------------------
```typescript
import {
KnowledgeService,
ProjectService,
TaskService,
} from "../../../services/neo4j/index.js";
import { BaseErrorCode, McpError } from "../../../types/errors.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import {
Project,
ProjectListRequest,
ProjectListResponse,
Knowledge,
Task,
} from "./types.js"; // Import Knowledge and Task
/**
* Retrieve and filter project entities based on specified criteria
* Provides two query modes: detailed entity retrieval or paginated collection listing
*
* @param request The project query parameters including filters and pagination controls
* @returns Promise resolving to structured project entities with optional related resources
*/
export async function listProjects(
request: ProjectListRequest,
): Promise<ProjectListResponse> {
const reqContext = requestContextService.createRequestContext({
toolName: "listProjects",
requestMode: request.mode,
});
try {
const {
mode = "all",
id,
page = 1,
limit = 20,
includeKnowledge = false,
includeTasks = false,
taskType,
status,
} = request;
// Parameter validation
if (mode === "details" && !id) {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
'Project identifier is required when using mode="details"',
);
}
// Sanitize pagination parameters
const validatedPage = Math.max(1, page);
const validatedLimit = Math.min(Math.max(1, limit), 100);
let projects: Project[] = [];
let total = 0;
let totalPages = 0;
if (mode === "details") {
// Retrieve specific project entity by identifier
const projectResult = await ProjectService.getProjectById(id!);
if (!projectResult) {
throw new McpError(
BaseErrorCode.NOT_FOUND,
`Project with identifier ${id} not found`,
);
}
// Cast to the tool's Project type
projects = [projectResult as Project];
total = 1;
totalPages = 1;
} else {
// Get paginated list of projects with filters
const projectsResult = await ProjectService.getProjects({
status,
taskType,
page: validatedPage,
limit: validatedLimit,
});
// Cast each project to the tool's Project type
projects = projectsResult.data.map((p) => p as Project);
total = projectsResult.total;
totalPages = projectsResult.totalPages;
}
// Process knowledge resource associations if requested
if (includeKnowledge && projects.length > 0) {
for (const project of projects) {
if (mode === "details") {
// For detailed view, retrieve comprehensive knowledge resources
const knowledgeResult = await KnowledgeService.getKnowledge({
projectId: project.id, // Access directly
page: 1,
limit: 100, // Reasonable threshold for associated resources
});
// Add debug logging
logger.info("Knowledge items retrieved", {
...reqContext,
projectId: project.id, // Access directly
count: knowledgeResult.data.length,
firstItem: knowledgeResult.data[0]
? JSON.stringify(knowledgeResult.data[0])
: "none",
});
// Map directly, assuming KnowledgeService returns Neo4jKnowledge objects
project.knowledge = knowledgeResult.data.map((item) => {
// More explicit mapping with debug info
logger.debug("Processing knowledge item", {
...reqContext,
id: item.id,
domain: item.domain,
textLength: item.text ? item.text.length : 0,
});
// Cast to the tool's Knowledge type
return item as Knowledge;
});
} else {
// For list mode, get abbreviated knowledge items
const knowledgeResult = await KnowledgeService.getKnowledge({
projectId: project.id, // Access directly
page: 1,
limit: 5, // Just a few for summary view
});
// Map directly, assuming KnowledgeService returns Neo4jKnowledge objects
project.knowledge = knowledgeResult.data.map((item) => {
// Cast to the tool's Knowledge type, potentially truncating text
const knowledgeItem = item as Knowledge;
return {
...knowledgeItem,
// Show a preview of the text - increased to 200 characters
text:
item.text && item.text.length > 200
? item.text.substring(0, 200) + "... (truncated)"
: item.text,
};
});
}
}
}
// Process task entity associations if requested
if (includeTasks && projects.length > 0) {
for (const project of projects) {
if (mode === "details") {
// For detailed view, retrieve prioritized task entities
const tasksResult = await TaskService.getTasks({
projectId: project.id, // Access directly
page: 1,
limit: 100, // Reasonable threshold for associated entities
sortBy: "priority",
sortDirection: "desc",
});
// Add debug logging
logger.info("Tasks retrieved for project", {
...reqContext,
projectId: project.id, // Access directly
count: tasksResult.data.length,
firstItem: tasksResult.data[0]
? JSON.stringify(tasksResult.data[0])
: "none",
});
// Map directly, assuming TaskService returns Neo4jTask objects
project.tasks = tasksResult.data.map((item) => {
// Debug info
logger.debug("Processing task item", {
...reqContext,
id: item.id,
title: item.title,
status: item.status,
priority: item.priority,
});
// Cast to the tool's Task type
return item as Task;
});
} else {
// For list mode, get abbreviated task items
const tasksResult = await TaskService.getTasks({
projectId: project.id, // Access directly
page: 1,
limit: 5, // Just a few for summary view
sortBy: "priority",
sortDirection: "desc",
});
// Map directly, assuming TaskService returns Neo4jTask objects
project.tasks = tasksResult.data.map((item) => {
// Cast to the tool's Task type
return item as Task;
});
}
}
}
// Construct the response
const response: ProjectListResponse = {
projects,
total,
page: validatedPage,
limit: validatedLimit,
totalPages,
};
logger.info("Project query executed successfully", {
...reqContext,
mode,
count: projects.length,
total,
includeKnowledge,
includeTasks,
});
return response;
} catch (error) {
logger.error("Project query execution failed", error as Error, reqContext);
if (error instanceof McpError) {
throw error;
}
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Failed to retrieve project entities: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_add/index.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
createToolExample,
createToolMetadata,
registerTool,
} from "../../../types/tool.js";
import { atlasAddKnowledge } from "./addKnowledge.js";
import { AtlasKnowledgeAddSchemaShape } from "./types.js";
export const registerAtlasKnowledgeAddTool = (server: McpServer) => {
registerTool(
server,
"atlas_knowledge_add",
"Adds a new knowledge item or multiple items to the system with domain categorization, tagging, and citation support",
AtlasKnowledgeAddSchemaShape,
atlasAddKnowledge,
createToolMetadata({
examples: [
createToolExample(
{
mode: "single",
projectId: "proj_ms_migration",
text: "GraphQL provides significant performance benefits over REST when clients need to request multiple related resources. By allowing clients to specify exactly what data they need in a single request, GraphQL eliminates over-fetching and under-fetching problems common in REST APIs.",
domain: "technical",
tags: ["graphql", "api", "performance", "rest"],
citations: [
"https://graphql.org/learn/best-practices/",
"https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/",
],
},
`{
"id": "know_graphql_benefits",
"projectId": "proj_ms_migration",
"text": "GraphQL provides significant performance benefits over REST when clients need to request multiple related resources. By allowing clients to specify exactly what data they need in a single request, GraphQL eliminates over-fetching and under-fetching problems common in REST APIs.",
"tags": ["graphql", "api", "performance", "rest"],
"domain": "technical",
"citations": ["https://graphql.org/learn/best-practices/", "https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/"],
"createdAt": "2025-03-23T10:11:24.123Z",
"updatedAt": "2025-03-23T10:11:24.123Z"
}`,
"Add technical knowledge about GraphQL benefits with citations and tags",
),
createToolExample(
{
mode: "bulk",
knowledge: [
{
projectId: "proj_ui_redesign",
text: "User interviews revealed that 78% of our customers struggle with the current checkout flow, particularly the address entry form which was described as 'confusing' and 'overly complex'.",
domain: "business",
tags: ["user-research", "checkout", "ux-issues"],
},
{
projectId: "proj_ui_redesign",
text: "Industry research shows that automatically formatting phone numbers and credit card fields as users type reduces error rates by approximately 25%. Implementing real-time validation with clear error messages has been shown to decrease form abandonment rates by up to 40%.",
domain: "technical",
tags: ["form-design", "validation", "ux-patterns"],
citations: [
"https://baymard.com/blog/input-mask-form-fields",
"https://www.smashingmagazine.com/2020/03/form-validation-ux-design/",
],
},
],
},
`{
"success": true,
"message": "Successfully added 2 knowledge items",
"created": [
{
"id": "know_checkout_research",
"projectId": "proj_ui_redesign",
"text": "User interviews revealed that 78% of our customers struggle with the current checkout flow, particularly the address entry form which was described as 'confusing' and 'overly complex'.",
"tags": ["user-research", "checkout", "ux-issues"],
"domain": "business",
"citations": [],
"createdAt": "2025-03-23T10:11:24.123Z",
"updatedAt": "2025-03-23T10:11:24.123Z"
},
{
"id": "know_form_validation",
"projectId": "proj_ui_redesign",
"text": "Industry research shows that automatically formatting phone numbers and credit card fields as users type reduces error rates by approximately 25%. Implementing real-time validation with clear error messages has been shown to decrease form abandonment rates by up to 40%.",
"tags": ["form-design", "validation", "ux-patterns"],
"domain": "technical",
"citations": ["https://baymard.com/blog/input-mask-form-fields", "https://www.smashingmagazine.com/2020/03/form-validation-ux-design/"],
"createdAt": "2025-03-23T10:11:24.456Z",
"updatedAt": "2025-03-23T10:11:24.456Z"
}
],
"errors": []
}`,
"Add multiple knowledge items with mixed domains and research findings",
),
],
requiredPermission: "knowledge:create",
returnSchema: z.union([
// Single knowledge response
z.object({
id: z.string().describe("Knowledge ID"),
projectId: z.string().describe("Project ID"),
text: z.string().describe("Knowledge content"),
tags: z.array(z.string()).describe("Categorical labels"),
domain: z.string().describe("Knowledge domain"),
citations: z.array(z.string()).describe("Reference sources"),
createdAt: z.string().describe("Creation timestamp"),
updatedAt: z.string().describe("Last update timestamp"),
}),
// Bulk creation response
z.object({
success: z.boolean().describe("Operation success status"),
message: z.string().describe("Result message"),
created: z
.array(
z.object({
id: z.string().describe("Knowledge ID"),
projectId: z.string().describe("Project ID"),
text: z.string().describe("Knowledge content"),
tags: z.array(z.string()).describe("Categorical labels"),
domain: z.string().describe("Knowledge domain"),
citations: z.array(z.string()).describe("Reference sources"),
createdAt: z.string().describe("Creation timestamp"),
updatedAt: z.string().describe("Last update timestamp"),
}),
)
.describe("Created knowledge items"),
errors: z
.array(
z.object({
index: z.number().describe("Index in the knowledge array"),
knowledge: z.any().describe("Original knowledge data"),
error: z
.object({
code: z.string().describe("Error code"),
message: z.string().describe("Error message"),
details: z
.any()
.optional()
.describe("Additional error details"),
})
.describe("Error information"),
}),
)
.describe("Creation errors"),
}),
]),
rateLimit: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 20, // 20 requests per minute (higher than project creation as knowledge items are typically smaller)
},
}),
);
};
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_knowledge_list/index.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
ResponseFormat,
createResponseFormatEnum,
createToolResponse,
} from "../../../types/mcp.js";
import {
createToolExample,
createToolMetadata,
registerTool,
} from "../../../types/tool.js";
import { listKnowledge } from "./listKnowledge.js";
import { formatKnowledgeListResponse } from "./responseFormat.js";
import { KnowledgeListRequest } from "./types.js"; // Corrected import name
/**
* Registers the atlas_knowledge_list tool with the MCP server
*
* @param server The MCP server instance
*/
export function registerAtlasKnowledgeListTool(server: McpServer): void {
registerTool(
server,
"atlas_knowledge_list",
"Lists knowledge items according to specified filters with tag-based categorization, domain filtering, and full-text search capabilities",
{
projectId: z
.string()
.describe("ID of the project to list knowledge items for (required)"),
tags: z
.array(z.string())
.optional()
.describe(
"Array of tags to filter by (items matching any tag will be included)",
),
domain: z
.string()
.optional()
.describe("Filter by knowledge domain/category"),
search: z
.string()
.optional()
.describe("Text search query to filter results by content relevance"),
page: z
.number()
.min(1)
.optional()
.default(1)
.describe("Page number for paginated results (Default: 1)"),
limit: z
.number()
.min(1)
.max(100)
.optional()
.default(20)
.describe("Number of results per page, maximum 100 (Default: 20)"),
responseFormat: createResponseFormatEnum()
.optional()
.default(ResponseFormat.FORMATTED)
.describe(
"Desired response format: 'formatted' (default string) or 'json' (raw object)",
),
},
async (input, context) => {
// Process knowledge list request
const validatedInput = input as unknown as KnowledgeListRequest & {
responseFormat?: ResponseFormat;
}; // Corrected type cast
const result = await listKnowledge(validatedInput);
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(result, null, 2));
} else {
// Return the result using the formatter for rich display
return formatKnowledgeListResponse(result, false);
}
},
createToolMetadata({
examples: [
createToolExample(
{
projectId: "proj_ms_migration",
limit: 5,
},
`{
"knowledge": [
{
"id": "know_saga_pattern",
"projectId": "proj_ms_migration",
"projectName": "Microservice Architecture Migration",
"text": "Distributed transactions must use Saga pattern with compensating actions to maintain data integrity across services",
"tags": ["architecture", "data-integrity", "patterns"],
"domain": "technical",
"citations": ["https://microservices.io/patterns/data/saga.html"],
"createdAt": "2025-03-23T11:22:14.789Z",
"updatedAt": "2025-03-23T11:22:14.789Z"
},
{
"id": "know_rate_limiting",
"projectId": "proj_ms_migration",
"projectName": "Microservice Architecture Migration",
"text": "Rate limiting should be implemented at the API Gateway level using Redis-based token bucket algorithm",
"tags": ["api-gateway", "performance", "security"],
"domain": "technical",
"citations": ["https://www.nginx.com/blog/rate-limiting-nginx/"],
"createdAt": "2025-03-23T12:34:27.456Z",
"updatedAt": "2025-03-23T12:34:27.456Z"
}
],
"total": 2,
"page": 1,
"limit": 5,
"totalPages": 1
}`,
"Retrieve all knowledge items for a specific project",
),
createToolExample(
{
projectId: "proj_ms_migration",
domain: "technical",
tags: ["security"],
},
`{
"knowledge": [
{
"id": "know_rate_limiting",
"projectId": "proj_ms_migration",
"projectName": "Microservice Architecture Migration",
"text": "Rate limiting should be implemented at the API Gateway level using Redis-based token bucket algorithm",
"tags": ["api-gateway", "performance", "security"],
"domain": "technical",
"citations": ["https://www.nginx.com/blog/rate-limiting-nginx/"],
"createdAt": "2025-03-23T12:34:27.456Z",
"updatedAt": "2025-03-23T12:34:27.456Z"
}
],
"total": 1,
"page": 1,
"limit": 20,
"totalPages": 1
}`,
"Filter knowledge items by domain and tags",
),
createToolExample(
{
projectId: "proj_ms_migration",
search: "data integrity",
},
`{
"knowledge": [
{
"id": "know_saga_pattern",
"projectId": "proj_ms_migration",
"projectName": "Microservice Architecture Migration",
"text": "Distributed transactions must use Saga pattern with compensating actions to maintain data integrity across services",
"tags": ["architecture", "data-integrity", "patterns"],
"domain": "technical",
"citations": ["https://microservices.io/patterns/data/saga.html"],
"createdAt": "2025-03-23T11:22:14.789Z",
"updatedAt": "2025-03-23T11:22:14.789Z"
}
],
"total": 1,
"page": 1,
"limit": 20,
"totalPages": 1
}`,
"Search knowledge items for specific text content",
),
],
requiredPermission: "knowledge:read",
entityType: "knowledge",
returnSchema: z.object({
knowledge: z.array(
z.object({
id: z.string().describe("Knowledge ID"),
projectId: z.string().describe("Project ID"),
projectName: z.string().optional().describe("Project name"),
text: z.string().describe("Knowledge content"),
tags: z.array(z.string()).optional().describe("Categorical labels"),
domain: z.string().describe("Knowledge domain/category"),
citations: z
.array(z.string())
.optional()
.describe("Reference sources"),
createdAt: z.string().describe("Creation timestamp"),
updatedAt: z.string().describe("Last update timestamp"),
}),
),
total: z
.number()
.describe("Total number of knowledge items matching criteria"),
page: z.number().describe("Current page number"),
limit: z.number().describe("Number of items per page"),
totalPages: z.number().describe("Total number of pages"),
}),
rateLimit: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 30, // 30 requests per minute
},
}),
);
}
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_unified_search/unifiedSearch.ts:
--------------------------------------------------------------------------------
```typescript
import {
SearchResultItem,
SearchService,
} from "../../../services/neo4j/index.js";
import { PaginatedResult } from "../../../services/neo4j/types.js";
import { BaseErrorCode, McpError } from "../../../types/errors.js";
import { ResponseFormat } from "../../../types/mcp.js";
import { ToolContext } from "../../../types/tool.js";
import { logger, requestContextService } from "../../../utils/index.js";
import { formatUnifiedSearchResponse } from "./responseFormat.js";
import {
UnifiedSearchRequestInput,
UnifiedSearchRequestSchema,
UnifiedSearchResponse,
} from "./types.js";
export const atlasUnifiedSearch = async (
input: unknown,
context: ToolContext,
): Promise<any> => {
const reqContext =
context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasUnifiedSearch",
});
try {
const validatedInput = UnifiedSearchRequestSchema.parse(
input,
) as UnifiedSearchRequestInput & { responseFormat?: ResponseFormat };
logger.info("Performing unified search", {
...reqContext,
input: validatedInput,
});
if (!validatedInput.value || validatedInput.value.trim() === "") {
throw new McpError(
BaseErrorCode.VALIDATION_ERROR,
"Search value cannot be empty",
{ param: "value" },
);
}
let searchResults: PaginatedResult<SearchResultItem>;
const propertyForSearch = validatedInput.property?.trim();
const entityTypesForSearch = validatedInput.entityTypes || [
"project",
"task",
"knowledge",
]; // Default if not provided
// Determine if we should use full-text for the given property and entity type
let shouldUseFullText = false;
if (propertyForSearch) {
const lowerProp = propertyForSearch.toLowerCase();
// Check for specific entityType + property combinations that have dedicated full-text indexes
if (entityTypesForSearch.includes("knowledge") && lowerProp === "text") {
shouldUseFullText = true;
} else if (
entityTypesForSearch.includes("project") &&
(lowerProp === "name" || lowerProp === "description")
) {
shouldUseFullText = true;
} else if (
entityTypesForSearch.includes("task") &&
(lowerProp === "title" || lowerProp === "description")
) {
shouldUseFullText = true;
}
// Add other specific full-text indexed fields here if any
} else {
// No specific property, so general full-text search is appropriate across default indexed fields
shouldUseFullText = true;
}
if (shouldUseFullText) {
logger.info(
`Using full-text search. Property: '${propertyForSearch || "default fields"}'`,
{
...reqContext,
property: propertyForSearch,
targetEntityTypes: entityTypesForSearch,
effectiveFuzzy: validatedInput.fuzzy === true,
},
);
const escapeLucene = (str: string) =>
str.replace(/([+\-!(){}\[\]^"~*?:\\\/"])/g, "\\$1");
let luceneQueryValue = escapeLucene(validatedInput.value);
// If fuzzy is requested for the tool, apply it to the Lucene query
if (validatedInput.fuzzy === true) {
luceneQueryValue = `${luceneQueryValue}~1`;
}
// Note: If propertyForSearch is set (e.g., "text" for "knowledge"),
// SearchService.fullTextSearch will use the appropriate index (e.g., "knowledge_fulltext").
// Lucene itself can handle field-specific queries like "fieldName:term",
// but our SearchService.fullTextSearch is already structured to call specific indexes.
// So, just passing the term (and fuzzy if needed) is correct here.
logger.debug("Constructed Lucene query value for full-text search", {
...reqContext,
luceneQueryValue,
});
searchResults = await SearchService.fullTextSearch(luceneQueryValue, {
entityTypes: entityTypesForSearch,
taskType: validatedInput.taskType,
page: validatedInput.page,
limit: validatedInput.limit,
});
} else {
// propertyForSearch is specified, and it's not one we've decided to use full-text for
// This path implies a regex-based search on a specific, non-full-text-optimized property.
// We want "contains" (fuzzy: true for SearchService.search) by default for this path,
// unless the user explicitly passed fuzzy: false in the tool input.
let finalFuzzyForRegexPath: boolean;
if ((input as any)?.fuzzy === false) {
// User explicitly requested an exact match for the regex search
finalFuzzyForRegexPath = false;
} else {
// User either passed fuzzy: true, or didn't pass fuzzy (in which case Zod default is true,
// and we also want "contains" as the intelligent default for this path).
finalFuzzyForRegexPath = true;
}
logger.info(
`Using regex-based search for specific property: '${propertyForSearch}'. Effective fuzzy for SearchService.search (true means contains): ${finalFuzzyForRegexPath}`,
{
...reqContext,
property: propertyForSearch,
targetEntityTypes: entityTypesForSearch,
userInputFuzzy: (input as any)?.fuzzy, // Log what user actually passed, if anything
zodParsedFuzzy: validatedInput.fuzzy, // Log what Zod parsed (with default)
finalFuzzyForRegexPath,
},
);
searchResults = await SearchService.search({
property: propertyForSearch, // Already trimmed
value: validatedInput.value,
entityTypes: entityTypesForSearch,
caseInsensitive: validatedInput.caseInsensitive, // Pass through
fuzzy: finalFuzzyForRegexPath, // This now correctly defaults to 'true' for "contains"
taskType: validatedInput.taskType,
assignedToUserId: validatedInput.assignedToUserId, // Pass through
page: validatedInput.page,
limit: validatedInput.limit,
});
}
if (!searchResults || !Array.isArray(searchResults.data)) {
logger.error(
"Search service returned invalid data structure.",
new Error("Invalid search results structure"),
{ ...reqContext, searchResultsReceived: searchResults },
);
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
"Received invalid data structure from search service.",
);
}
logger.info("Unified search completed successfully", {
...reqContext,
resultCount: searchResults.data.length,
totalResults: searchResults.total,
});
const responseData: UnifiedSearchResponse = {
results: searchResults.data,
total: searchResults.total,
page: searchResults.page,
limit: searchResults.limit,
totalPages: searchResults.totalPages,
};
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return responseData;
} else {
return formatUnifiedSearchResponse(responseData);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
// const errorStack = error instanceof Error ? error.stack : undefined; // Already captured by logger
logger.error("Failed to perform unified search", error as Error, {
...reqContext,
// errorMessage and errorStack are part of the Error object passed to logger
inputReceived: input,
});
if (error instanceof McpError) {
throw error;
} else {
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Error performing unified search: ${errorMessage}`,
);
}
}
};
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
// Imports MUST be at the top level
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import http from "http";
import { config, environment } from "./config/index.js"; // This loads .env via dotenv.config()
import { initializeAndStartServer } from "./mcp/server.js";
import { closeNeo4jConnection } from "./services/neo4j/index.js";
import { logger, McpLogLevel, requestContextService } from "./utils/index.js";
/**
* The main MCP server instance, stored if transport is stdio for shutdown.
* Or the HTTP server instance if transport is http.
* @type {McpServer | http.Server | undefined}
*/
let serverInstance: McpServer | http.Server | undefined;
/**
* Gracefully shuts down the main MCP server and related services.
* Handles process termination signals (SIGTERM, SIGINT) and critical errors.
*
* @param signal - The signal or event name that triggered the shutdown (e.g., "SIGTERM", "uncaughtException").
*/
const shutdown = async (signal: string) => {
// Create a proper RequestContext for shutdown operations
const shutdownContext = requestContextService.createRequestContext({
operation: "Shutdown",
signal,
appName: config.mcpServerName,
});
logger.info(
`Received ${signal}. Starting graceful shutdown for ${config.mcpServerName}...`,
shutdownContext,
);
try {
if (serverInstance) {
if (serverInstance instanceof McpServer) {
logger.info("Closing main MCP server (stdio) instance...", shutdownContext);
await serverInstance.close();
logger.info(
"Main MCP server (stdio) instance closed successfully.",
shutdownContext,
);
} else if (serverInstance instanceof http.Server) {
logger.info("Closing HTTP server instance...", shutdownContext);
const currentHttpServer = serverInstance;
await new Promise<void>((resolve, reject) => {
currentHttpServer.close((err?: Error) => {
if (err) {
logger.error("Error closing HTTP server", err, shutdownContext);
return reject(err);
}
logger.info("HTTP server instance closed successfully.", shutdownContext);
resolve();
});
});
}
} else {
logger.info(
"No global MCP server instance to close (expected for HTTP transport or if not yet initialized).",
shutdownContext,
);
}
logger.info("Closing Neo4j driver connection...", shutdownContext);
await closeNeo4jConnection();
logger.info(
"Neo4j driver connection closed successfully.",
shutdownContext,
);
logger.info(
`Graceful shutdown for ${config.mcpServerName} completed successfully. Exiting.`,
shutdownContext,
);
process.exit(0);
} catch (error) {
// Pass the error object directly as the second argument to logger.error
logger.error("Critical error during shutdown", error as Error, {
...shutdownContext,
// error message and stack are now part of the Error object passed to logger
});
process.exit(1);
}
};
/**
* Initializes and starts the main MCP server.
* Sets up logging, request context, initializes the server instance, starts the transport,
* and registers signal handlers for graceful shutdown and error handling.
*/
const start = async () => {
// --- Logger Initialization ---
const validMcpLogLevels: McpLogLevel[] = [
"debug",
"info",
"notice",
"warning",
"error",
"crit",
"alert",
"emerg",
];
const initialLogLevelConfig = config.logLevel;
let validatedMcpLogLevel: McpLogLevel = "info"; // Default
if (validMcpLogLevels.includes(initialLogLevelConfig as McpLogLevel)) {
validatedMcpLogLevel = initialLogLevelConfig as McpLogLevel;
} else {
// Use console.warn here as logger isn't fully initialized yet, only if TTY
if (process.stdout.isTTY) {
console.warn(
`Invalid MCP_LOG_LEVEL "${initialLogLevelConfig}" provided via config/env. Defaulting to "info".`,
);
}
}
// Initialize the logger with the validated MCP level and wait for it to complete.
await logger.initialize(validatedMcpLogLevel);
// The logger.initialize() method itself logs its status, so no redundant log here.
// --- End Logger Initialization ---
const configLoadContext = requestContextService.createRequestContext({
operation: "ConfigLoad",
});
logger.debug("Configuration loaded successfully", {
...configLoadContext,
configValues: config,
}); // Spread context and add specific data
const transportType = config.mcpTransportType;
const startupContext = requestContextService.createRequestContext({
operation: `AtlasServerStartup_${transportType}`,
appName: config.mcpServerName,
appVersion: config.mcpServerVersion,
environment: environment,
});
logger.info(
`Starting ${config.mcpServerName} v${config.mcpServerVersion} (Transport: ${transportType})...`,
startupContext,
);
try {
logger.debug(
"Initializing and starting MCP server transport...",
startupContext,
);
const potentialServer = await initializeAndStartServer();
if (transportType === "stdio" && potentialServer instanceof McpServer) {
serverInstance = potentialServer;
logger.debug(
"Stored McpServer instance for stdio transport.",
startupContext,
);
} else if (transportType === "http" && potentialServer instanceof http.Server) {
serverInstance = potentialServer;
logger.debug(
"Stored HTTP server instance. MCP sessions are per-request.",
startupContext,
);
} else if (transportType === "http" && !potentialServer) {
logger.debug(
"HTTP transport started. Server instance not returned by initializeAndStartServer. MCP sessions are per-request.",
startupContext,
);
}
logger.info(
`${config.mcpServerName} is running with ${transportType} transport.`,
{
...startupContext,
startTime: new Date().toISOString(),
},
);
// --- Signal and Error Handling Setup ---
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("uncaughtException", async (error) => {
const errorContext = {
...startupContext,
event: "uncaughtException",
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
};
logger.error(
"Uncaught exception detected. Initiating shutdown...",
errorContext,
);
await shutdown("uncaughtException");
});
process.on("unhandledRejection", async (reason: unknown) => {
const rejectionContext = {
...startupContext,
event: "unhandledRejection",
reason: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
};
logger.error(
"Unhandled promise rejection detected. Initiating shutdown...",
rejectionContext,
);
await shutdown("unhandledRejection");
});
} catch (error) {
logger.error("Critical error during ATLAS MCP Server startup, exiting.", {
...startupContext,
finalErrorContext: "Startup Failure",
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
process.exit(1);
}
};
// --- Async IIFE to allow top-level await ---
(async () => {
await start();
})();
```
--------------------------------------------------------------------------------
/src/mcp/tools/atlas_project_create/createProject.ts:
--------------------------------------------------------------------------------
```typescript
import { ProjectService } from "../../../services/neo4j/projectService.js";
import { ProjectDependencyType } from "../../../services/neo4j/types.js"; // Import the enum
import {
BaseErrorCode,
McpError,
ProjectErrorCode,
} from "../../../types/errors.js";
import { ResponseFormat, createToolResponse } from "../../../types/mcp.js";
import { logger, requestContextService } from "../../../utils/index.js"; // Import requestContextService
import { ToolContext } from "../../../types/tool.js";
import { AtlasProjectCreateInput, AtlasProjectCreateSchema } from "./types.js";
import { formatProjectCreateResponse } from "./responseFormat.js";
export const atlasCreateProject = async (
input: unknown,
context: ToolContext,
) => {
let validatedInput: AtlasProjectCreateInput | undefined;
const reqContext =
context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasCreateProject",
});
try {
// Parse and validate input against schema
validatedInput = AtlasProjectCreateSchema.parse(input);
// Handle single vs bulk project creation based on mode
if (validatedInput.mode === "bulk") {
// Execute bulk creation operation
logger.info("Initializing multiple projects", {
...reqContext,
count: validatedInput.projects.length,
});
const results = {
success: true,
message: `Successfully created ${validatedInput.projects.length} projects`,
created: [] as any[],
errors: [] as any[],
};
// Process each project sequentially to maintain consistency
for (let i = 0; i < validatedInput.projects.length; i++) {
const projectData = validatedInput.projects[i];
try {
const createdProject = await ProjectService.createProject({
name: projectData.name,
description: projectData.description,
status: projectData.status || "active",
urls: projectData.urls || [],
completionRequirements: projectData.completionRequirements,
outputFormat: projectData.outputFormat,
taskType: projectData.taskType,
id: projectData.id, // Use client-provided ID if available
});
results.created.push(createdProject);
// Create dependency relationships if specified
if (projectData.dependencies && projectData.dependencies.length > 0) {
for (const dependencyId of projectData.dependencies) {
try {
await ProjectService.addProjectDependency(
createdProject.id,
dependencyId,
ProjectDependencyType.REQUIRES, // Use enum member
"Dependency created during project creation",
);
} catch (error) {
const depErrorContext =
requestContextService.createRequestContext({
...reqContext,
originalErrorMessage:
error instanceof Error ? error.message : String(error),
originalErrorStack:
error instanceof Error ? error.stack : undefined,
projectId: createdProject.id,
dependencyIdAttempted: dependencyId,
});
logger.warning(
`Failed to create dependency for project ${createdProject.id} to ${dependencyId}`,
depErrorContext,
);
}
}
}
} catch (error) {
results.success = false;
results.errors.push({
index: i,
project: projectData,
error: {
code:
error instanceof McpError
? error.code
: BaseErrorCode.INTERNAL_ERROR,
message: error instanceof Error ? error.message : "Unknown error",
details: error instanceof McpError ? error.details : undefined,
},
});
}
}
if (results.errors.length > 0) {
results.message = `Created ${results.created.length} of ${validatedInput.projects.length} projects with ${results.errors.length} errors`;
}
logger.info("Bulk project initialization completed", {
...reqContext,
successCount: results.created.length,
errorCount: results.errors.length,
projectIds: results.created.map((p) => p.id),
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(results, null, 2));
} else {
return formatProjectCreateResponse(results);
}
} else {
// Process single project creation
const {
mode,
id,
name,
description,
status,
urls,
completionRequirements,
dependencies,
outputFormat,
taskType,
} = validatedInput;
logger.info("Initializing new project", {
...reqContext,
name,
status,
});
const project = await ProjectService.createProject({
id, // Use client-provided ID if available
name,
description,
status: status || "active",
urls: urls || [],
completionRequirements,
outputFormat,
taskType,
});
// Create dependency relationships if specified
if (dependencies && dependencies.length > 0) {
for (const dependencyId of dependencies) {
try {
await ProjectService.addProjectDependency(
project.id,
dependencyId,
ProjectDependencyType.REQUIRES, // Use enum member
"Dependency created during project creation",
);
} catch (error) {
const depErrorContext = requestContextService.createRequestContext({
...reqContext,
originalErrorMessage:
error instanceof Error ? error.message : String(error),
originalErrorStack:
error instanceof Error ? error.stack : undefined,
projectId: project.id,
dependencyIdAttempted: dependencyId,
});
logger.warning(
`Failed to create dependency for project ${project.id} to ${dependencyId}`,
depErrorContext,
);
}
}
}
logger.info("Project initialized successfully", {
...reqContext,
projectId: project.id,
});
// Conditionally format response
if (validatedInput.responseFormat === ResponseFormat.JSON) {
return createToolResponse(JSON.stringify(project, null, 2));
} else {
return formatProjectCreateResponse(project);
}
}
} catch (error) {
// Handle specific error cases
if (error instanceof McpError) {
throw error;
}
logger.error("Failed to initialize project(s)", error as Error, {
...reqContext,
inputReceived: validatedInput ?? input, // Log validated or raw input
});
// Handle duplicate name error specifically
if (error instanceof Error && error.message.includes("duplicate")) {
throw new McpError(
ProjectErrorCode.DUPLICATE_NAME,
`A project with this name already exists`,
{
name:
validatedInput?.mode === "single"
? validatedInput?.name
: validatedInput?.projects?.[0]?.name,
},
);
}
// Convert other errors to McpError
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Error creating project(s): ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
};
```