atlas-mcp-server
Version:
ATLAS (Adaptive Task & Logic Automation System): An MCP server enabling LLM agents to manage projects, tasks, and knowledge via a Neo4j-backed, three-tier architecture. Facilitates complex workflow automation and project management through LLM Agents.
153 lines (152 loc) • 7.85 kB
JavaScript
import { SearchService, } from "../../../services/neo4j/index.js";
import { BaseErrorCode, McpError } from "../../../types/errors.js";
import { ResponseFormat } from "../../../types/mcp.js";
import { logger, requestContextService } from "../../../utils/index.js";
import { formatUnifiedSearchResponse } from "./responseFormat.js";
import { UnifiedSearchRequestSchema, } from "./types.js";
export const atlasUnifiedSearch = async (input, context) => {
const reqContext = context.requestContext ??
requestContextService.createRequestContext({
toolName: "atlasUnifiedSearch",
});
try {
const validatedInput = UnifiedSearchRequestSchema.parse(input);
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;
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) => 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;
if (input?.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?.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 = {
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, {
...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}`);
}
}
};