@danielsimonjr/memory-mcp 0.41.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * MCP Tool Handlers
3
+ *
4
+ * Extracted from MCPServer.ts to reduce file size and improve maintainability.
5
+ * Contains handler functions for all 45 Knowledge Graph tools.
6
+ *
7
+ * @module server/toolHandlers
8
+ */
9
+ import { formatToolResponse, formatTextResponse, formatRawResponse } from '../utils/responseFormatter.js';
10
+ /**
11
+ * Registry of all tool handlers keyed by tool name.
12
+ */
13
+ export const toolHandlers = {
14
+ // ==================== ENTITY HANDLERS ====================
15
+ create_entities: async (manager, args) => formatToolResponse(await manager.createEntities(args.entities)),
16
+ delete_entities: async (manager, args) => {
17
+ await manager.deleteEntities(args.entityNames);
18
+ return formatTextResponse(`Deleted ${args.entityNames.length} entities`);
19
+ },
20
+ read_graph: async (manager) => formatToolResponse(await manager.readGraph()),
21
+ open_nodes: async (manager, args) => formatToolResponse(await manager.openNodes(args.names)),
22
+ // ==================== RELATION HANDLERS ====================
23
+ create_relations: async (manager, args) => formatToolResponse(await manager.createRelations(args.relations)),
24
+ delete_relations: async (manager, args) => {
25
+ await manager.deleteRelations(args.relations);
26
+ return formatTextResponse(`Deleted ${args.relations.length} relations`);
27
+ },
28
+ // ==================== OBSERVATION HANDLERS ====================
29
+ add_observations: async (manager, args) => formatToolResponse(await manager.addObservations(args.observations)),
30
+ delete_observations: async (manager, args) => {
31
+ await manager.deleteObservations(args.deletions);
32
+ return formatTextResponse('Observations deleted successfully');
33
+ },
34
+ // ==================== SEARCH HANDLERS ====================
35
+ search_nodes: async (manager, args) => formatToolResponse(await manager.searchNodes(args.query, args.tags, args.minImportance, args.maxImportance)),
36
+ search_by_date_range: async (manager, args) => formatToolResponse(await manager.searchByDateRange(args.startDate, args.endDate, args.entityType, args.tags)),
37
+ search_nodes_ranked: async (manager, args) => formatToolResponse(await manager.searchNodesRanked(args.query, args.tags, args.minImportance, args.maxImportance, args.limit)),
38
+ boolean_search: async (manager, args) => formatToolResponse(await manager.booleanSearch(args.query, args.tags, args.minImportance, args.maxImportance)),
39
+ fuzzy_search: async (manager, args) => formatToolResponse(await manager.fuzzySearch(args.query, args.threshold, args.tags, args.minImportance, args.maxImportance)),
40
+ get_search_suggestions: async (manager, args) => formatToolResponse(await manager.getSearchSuggestions(args.query, args.maxSuggestions)),
41
+ // ==================== SAVED SEARCH HANDLERS ====================
42
+ save_search: async (manager, args) => formatToolResponse(await manager.saveSearch(args)),
43
+ execute_saved_search: async (manager, args) => formatToolResponse(await manager.executeSavedSearch(args.name)),
44
+ list_saved_searches: async (manager) => formatToolResponse(await manager.listSavedSearches()),
45
+ delete_saved_search: async (manager, args) => {
46
+ const deleted = await manager.deleteSavedSearch(args.name);
47
+ return formatTextResponse(deleted
48
+ ? `Saved search "${args.name}" deleted successfully`
49
+ : `Saved search "${args.name}" not found`);
50
+ },
51
+ update_saved_search: async (manager, args) => formatToolResponse(await manager.updateSavedSearch(args.name, args.updates)),
52
+ // ==================== TAG HANDLERS ====================
53
+ add_tags: async (manager, args) => formatToolResponse(await manager.addTags(args.entityName, args.tags)),
54
+ remove_tags: async (manager, args) => formatToolResponse(await manager.removeTags(args.entityName, args.tags)),
55
+ set_importance: async (manager, args) => formatToolResponse(await manager.setImportance(args.entityName, args.importance)),
56
+ add_tags_to_multiple_entities: async (manager, args) => formatToolResponse(await manager.addTagsToMultipleEntities(args.entityNames, args.tags)),
57
+ replace_tag: async (manager, args) => formatToolResponse(await manager.replaceTag(args.oldTag, args.newTag)),
58
+ merge_tags: async (manager, args) => formatToolResponse(await manager.mergeTags(args.tag1, args.tag2, args.targetTag)),
59
+ // ==================== TAG ALIAS HANDLERS ====================
60
+ add_tag_alias: async (manager, args) => formatToolResponse(await manager.addTagAlias(args.alias, args.canonical, args.description)),
61
+ list_tag_aliases: async (manager) => formatToolResponse(await manager.listTagAliases()),
62
+ remove_tag_alias: async (manager, args) => {
63
+ const removed = await manager.removeTagAlias(args.alias);
64
+ return formatTextResponse(removed
65
+ ? `Tag alias "${args.alias}" removed successfully`
66
+ : `Tag alias "${args.alias}" not found`);
67
+ },
68
+ get_aliases_for_tag: async (manager, args) => formatToolResponse(await manager.getAliasesForTag(args.canonicalTag)),
69
+ resolve_tag: async (manager, args) => formatToolResponse({
70
+ tag: args.tag,
71
+ resolved: await manager.resolveTag(args.tag),
72
+ }),
73
+ // ==================== HIERARCHY HANDLERS ====================
74
+ set_entity_parent: async (manager, args) => formatToolResponse(await manager.setEntityParent(args.entityName, args.parentName)),
75
+ get_children: async (manager, args) => formatToolResponse(await manager.getChildren(args.entityName)),
76
+ get_parent: async (manager, args) => formatToolResponse(await manager.getParent(args.entityName)),
77
+ get_ancestors: async (manager, args) => formatToolResponse(await manager.getAncestors(args.entityName)),
78
+ get_descendants: async (manager, args) => formatToolResponse(await manager.getDescendants(args.entityName)),
79
+ get_subtree: async (manager, args) => formatToolResponse(await manager.getSubtree(args.entityName)),
80
+ get_root_entities: async (manager) => formatToolResponse(await manager.getRootEntities()),
81
+ get_entity_depth: async (manager, args) => formatToolResponse({
82
+ entityName: args.entityName,
83
+ depth: await manager.getEntityDepth(args.entityName),
84
+ }),
85
+ move_entity: async (manager, args) => formatToolResponse(await manager.moveEntity(args.entityName, args.newParentName)),
86
+ // ==================== ANALYTICS HANDLERS ====================
87
+ get_graph_stats: async (manager) => formatToolResponse(await manager.getGraphStats()),
88
+ validate_graph: async (manager) => formatToolResponse(await manager.validateGraph()),
89
+ // ==================== COMPRESSION HANDLERS ====================
90
+ find_duplicates: async (manager, args) => formatToolResponse(await manager.findDuplicates(args.threshold)),
91
+ merge_entities: async (manager, args) => formatToolResponse(await manager.mergeEntities(args.entityNames, args.targetName)),
92
+ compress_graph: async (manager, args) => formatToolResponse(await manager.compressGraph(args.threshold, args.dryRun)),
93
+ archive_entities: async (manager, args) => formatToolResponse(await manager.archiveEntities({
94
+ olderThan: args.olderThan,
95
+ importanceLessThan: args.importanceLessThan,
96
+ tags: args.tags,
97
+ }, args.dryRun)),
98
+ // ==================== IMPORT/EXPORT HANDLERS ====================
99
+ import_graph: async (manager, args) => formatToolResponse(await manager.importGraph(args.format, args.data, args.mergeStrategy, args.dryRun)),
100
+ export_graph: async (manager, args) => formatRawResponse(await manager.exportGraph(args.format, args.filter)),
101
+ };
102
+ /**
103
+ * Handle a tool call by dispatching to the appropriate handler.
104
+ *
105
+ * @param name - Tool name to call
106
+ * @param args - Tool arguments
107
+ * @param manager - Knowledge graph manager instance
108
+ * @returns Tool response
109
+ * @throws Error if tool name is unknown
110
+ */
111
+ export async function handleToolCall(name, args, manager) {
112
+ const handler = toolHandlers[name];
113
+ if (!handler) {
114
+ throw new Error(`Unknown tool: ${name}`);
115
+ }
116
+ return handler(manager, args);
117
+ }
@@ -59,6 +59,7 @@ export const LOG_PREFIXES = {
59
59
  * Similarity scoring weights for duplicate detection.
60
60
  * These weights determine the relative importance of each factor
61
61
  * when calculating entity similarity for duplicate detection.
62
+ * Total weights must sum to 1.0 (100%).
62
63
  */
63
64
  export const SIMILARITY_WEIGHTS = {
64
65
  /** Name similarity weight (40%) - Uses Levenshtein distance */
@@ -66,9 +67,9 @@ export const SIMILARITY_WEIGHTS = {
66
67
  /** Entity type match weight (20%) - Exact match required */
67
68
  TYPE: 0.2,
68
69
  /** Observation overlap weight (30%) - Uses Jaccard similarity */
69
- OBSERVATION: 0.3,
70
+ OBSERVATIONS: 0.3,
70
71
  /** Tag overlap weight (10%) - Uses Jaccard similarity */
71
- TAG: 0.1,
72
+ TAGS: 0.1,
72
73
  };
73
74
  /**
74
75
  * Default threshold for duplicate detection (80% similarity required).
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Entity Lookup and Utility Functions
3
+ *
4
+ * Centralizes entity lookup patterns to eliminate redundant code across managers.
5
+ * Provides type-safe entity finding with optional error throwing.
6
+ */
7
+ import { EntityNotFoundError } from './errors.js';
8
+ export function findEntityByName(graph, name, throwIfNotFound = true) {
9
+ const entity = graph.entities.find(e => e.name === name);
10
+ if (!entity && throwIfNotFound) {
11
+ throw new EntityNotFoundError(name);
12
+ }
13
+ return entity ?? null;
14
+ }
15
+ /**
16
+ * Finds multiple entities by name.
17
+ *
18
+ * @param graph - The knowledge graph to search
19
+ * @param names - Array of entity names to find
20
+ * @param throwIfAnyNotFound - Whether to throw if any entity doesn't exist (default: true)
21
+ * @returns Array of found entities (may be shorter than names if throwIfAnyNotFound is false)
22
+ * @throws EntityNotFoundError if any entity not found and throwIfAnyNotFound is true
23
+ */
24
+ export function findEntitiesByNames(graph, names, throwIfAnyNotFound = true) {
25
+ const entities = [];
26
+ for (const name of names) {
27
+ const entity = findEntityByName(graph, name, false);
28
+ if (entity) {
29
+ entities.push(entity);
30
+ }
31
+ else if (throwIfAnyNotFound) {
32
+ throw new EntityNotFoundError(name);
33
+ }
34
+ }
35
+ return entities;
36
+ }
37
+ /**
38
+ * Checks if an entity exists in the graph.
39
+ *
40
+ * @param graph - The knowledge graph to search
41
+ * @param name - The entity name to check
42
+ * @returns true if entity exists, false otherwise
43
+ */
44
+ export function entityExists(graph, name) {
45
+ return graph.entities.some(e => e.name === name);
46
+ }
47
+ /**
48
+ * Gets the index of an entity in the graph's entities array.
49
+ *
50
+ * @param graph - The knowledge graph to search
51
+ * @param name - The entity name to find
52
+ * @returns The index if found, -1 otherwise
53
+ */
54
+ export function getEntityIndex(graph, name) {
55
+ return graph.entities.findIndex(e => e.name === name);
56
+ }
57
+ /**
58
+ * Removes an entity from the graph by name.
59
+ * Mutates the graph's entities array in place.
60
+ *
61
+ * @param graph - The knowledge graph to modify
62
+ * @param name - The entity name to remove
63
+ * @returns true if entity was removed, false if not found
64
+ */
65
+ export function removeEntityByName(graph, name) {
66
+ const index = getEntityIndex(graph, name);
67
+ if (index === -1)
68
+ return false;
69
+ graph.entities.splice(index, 1);
70
+ return true;
71
+ }
72
+ /**
73
+ * Gets all entity names as a Set for fast lookup.
74
+ *
75
+ * @param graph - The knowledge graph
76
+ * @returns Set of all entity names
77
+ */
78
+ export function getEntityNameSet(graph) {
79
+ return new Set(graph.entities.map(e => e.name));
80
+ }
81
+ /**
82
+ * Groups entities by their type.
83
+ *
84
+ * @param entities - Array of entities to group
85
+ * @returns Map of entity type to array of entities
86
+ */
87
+ export function groupEntitiesByType(entities) {
88
+ const groups = new Map();
89
+ for (const entity of entities) {
90
+ const type = entity.entityType;
91
+ if (!groups.has(type)) {
92
+ groups.set(type, []);
93
+ }
94
+ groups.get(type).push(entity);
95
+ }
96
+ return groups;
97
+ }
98
+ /**
99
+ * Updates the lastModified timestamp on an entity.
100
+ * Mutates the entity in place.
101
+ *
102
+ * @param entity - The entity to update
103
+ * @returns The updated entity (same reference)
104
+ */
105
+ export function touchEntity(entity) {
106
+ entity.lastModified = new Date().toISOString();
107
+ return entity;
108
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Entity Filtering Utilities
3
+ *
4
+ * Centralizes common filtering logic for importance, date ranges, and other
5
+ * entity properties to eliminate duplicate patterns across search implementations.
6
+ */
7
+ /**
8
+ * Checks if an entity's importance is within the specified range.
9
+ * Entities without importance are treated as not matching if any filter is set.
10
+ *
11
+ * @param importance - The entity's importance value (may be undefined)
12
+ * @param minImportance - Minimum importance filter (inclusive)
13
+ * @param maxImportance - Maximum importance filter (inclusive)
14
+ * @returns true if importance is within range or no filters are set
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Check if entity passes importance filter
19
+ * if (isWithinImportanceRange(entity.importance, 5, 10)) {
20
+ * // Entity has importance between 5 and 10
21
+ * }
22
+ * ```
23
+ */
24
+ export function isWithinImportanceRange(importance, minImportance, maxImportance) {
25
+ // If no filters set, always pass
26
+ if (minImportance === undefined && maxImportance === undefined) {
27
+ return true;
28
+ }
29
+ // Check minimum importance
30
+ if (minImportance !== undefined) {
31
+ if (importance === undefined || importance < minImportance) {
32
+ return false;
33
+ }
34
+ }
35
+ // Check maximum importance
36
+ if (maxImportance !== undefined) {
37
+ if (importance === undefined || importance > maxImportance) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ /**
44
+ * Filters entities by importance range.
45
+ * Returns all entities if no importance filters are specified.
46
+ *
47
+ * @param entities - Array of entities to filter
48
+ * @param minImportance - Minimum importance filter (inclusive)
49
+ * @param maxImportance - Maximum importance filter (inclusive)
50
+ * @returns Filtered entities within the importance range
51
+ */
52
+ export function filterByImportance(entities, minImportance, maxImportance) {
53
+ if (minImportance === undefined && maxImportance === undefined) {
54
+ return entities;
55
+ }
56
+ return entities.filter(e => isWithinImportanceRange(e.importance, minImportance, maxImportance));
57
+ }
58
+ /**
59
+ * Checks if a date value is within the specified range.
60
+ * Handles undefined values appropriately.
61
+ *
62
+ * @param dateValue - ISO 8601 date string to check (may be undefined)
63
+ * @param startDate - Start of date range (inclusive)
64
+ * @param endDate - End of date range (inclusive)
65
+ * @returns true if date is within range or no filters are set
66
+ */
67
+ export function isWithinDateRange(dateValue, startDate, endDate) {
68
+ // If no filters set, always pass
69
+ if (!startDate && !endDate) {
70
+ return true;
71
+ }
72
+ // If date value is undefined but we have filters, fail
73
+ if (!dateValue) {
74
+ return false;
75
+ }
76
+ const date = new Date(dateValue);
77
+ if (startDate && date < new Date(startDate)) {
78
+ return false;
79
+ }
80
+ if (endDate && date > new Date(endDate)) {
81
+ return false;
82
+ }
83
+ return true;
84
+ }
85
+ /**
86
+ * Filters entities by creation date range.
87
+ *
88
+ * @param entities - Array of entities to filter
89
+ * @param startDate - Start of date range (inclusive)
90
+ * @param endDate - End of date range (inclusive)
91
+ * @returns Filtered entities created within the date range
92
+ */
93
+ export function filterByCreatedDate(entities, startDate, endDate) {
94
+ if (!startDate && !endDate) {
95
+ return entities;
96
+ }
97
+ return entities.filter(e => isWithinDateRange(e.createdAt, startDate, endDate));
98
+ }
99
+ /**
100
+ * Filters entities by last modified date range.
101
+ *
102
+ * @param entities - Array of entities to filter
103
+ * @param startDate - Start of date range (inclusive)
104
+ * @param endDate - End of date range (inclusive)
105
+ * @returns Filtered entities modified within the date range
106
+ */
107
+ export function filterByModifiedDate(entities, startDate, endDate) {
108
+ if (!startDate && !endDate) {
109
+ return entities;
110
+ }
111
+ return entities.filter(e => isWithinDateRange(e.lastModified, startDate, endDate));
112
+ }
113
+ /**
114
+ * Filters entities by entity type.
115
+ *
116
+ * @param entities - Array of entities to filter
117
+ * @param entityType - Entity type to filter by (case-sensitive)
118
+ * @returns Filtered entities of the specified type
119
+ */
120
+ export function filterByEntityType(entities, entityType) {
121
+ if (!entityType) {
122
+ return entities;
123
+ }
124
+ return entities.filter(e => e.entityType === entityType);
125
+ }
126
+ /**
127
+ * Checks if an entity passes all the specified filters.
128
+ * Short-circuits on first failing filter for performance.
129
+ *
130
+ * Note: Tag filtering should be handled separately using tagUtils.hasMatchingTag
131
+ * as it requires special normalization logic.
132
+ *
133
+ * @param entity - Entity to check
134
+ * @param filters - Filters to apply
135
+ * @returns true if entity passes all filters
136
+ */
137
+ export function entityPassesFilters(entity, filters) {
138
+ // Importance filter
139
+ if (!isWithinImportanceRange(entity.importance, filters.minImportance, filters.maxImportance)) {
140
+ return false;
141
+ }
142
+ // Entity type filter
143
+ if (filters.entityType && entity.entityType !== filters.entityType) {
144
+ return false;
145
+ }
146
+ // Created date filter
147
+ if (!isWithinDateRange(entity.createdAt, filters.createdAfter, filters.createdBefore)) {
148
+ return false;
149
+ }
150
+ // Modified date filter
151
+ if (!isWithinDateRange(entity.lastModified, filters.modifiedAfter, filters.modifiedBefore)) {
152
+ return false;
153
+ }
154
+ return true;
155
+ }
@@ -1,13 +1,39 @@
1
1
  /**
2
2
  * Utilities Module Barrel Export
3
+ *
4
+ * Centralizes all utility exports for convenient importing.
5
+ * Sprint 1 additions: responseFormatter, tagUtils, entityUtils,
6
+ * validationHelper, paginationUtils, filterUtils
3
7
  */
8
+ // Error types
4
9
  export { KnowledgeGraphError, EntityNotFoundError, RelationNotFoundError, DuplicateEntityError, ValidationError, CycleDetectedError, InvalidImportanceError, FileOperationError, ImportError, ExportError, InsufficientEntitiesError, } from './errors.js';
10
+ // String utilities
5
11
  export { levenshteinDistance } from './levenshtein.js';
6
12
  export { calculateTF, calculateIDF, calculateTFIDF, tokenize } from './tfidf.js';
13
+ // Logging
7
14
  export { logger } from './logger.js';
15
+ // Date utilities
8
16
  export { isWithinDateRange, parseDateRange, isValidISODate, getCurrentTimestamp } from './dateUtils.js';
17
+ // Validation utilities
9
18
  export { validateEntity, validateRelation, validateImportance, validateTags } from './validationUtils.js';
19
+ // Path utilities
10
20
  export { defaultMemoryPath, ensureMemoryFilePath, validateFilePath } from './pathUtils.js';
21
+ // Constants
11
22
  export { FILE_EXTENSIONS, FILE_SUFFIXES, DEFAULT_FILE_NAMES, ENV_VARS, DEFAULT_BASE_DIR, LOG_PREFIXES, SIMILARITY_WEIGHTS, DEFAULT_DUPLICATE_THRESHOLD, SEARCH_LIMITS, IMPORTANCE_RANGE, } from './constants.js';
23
+ // Zod schemas
12
24
  export { EntitySchema, CreateEntitySchema, UpdateEntitySchema, RelationSchema, CreateRelationSchema, SearchQuerySchema, DateRangeSchema, TagAliasSchema, ExportFormatSchema, BatchCreateEntitiesSchema, BatchCreateRelationsSchema, EntityNamesSchema, DeleteRelationsSchema, } from './schemas.js';
25
+ // Search cache
13
26
  export { SearchCache, searchCaches, clearAllSearchCaches, getAllCacheStats, cleanupAllCaches, } from './searchCache.js';
27
+ // === Sprint 1: New Utility Exports ===
28
+ // MCP Response formatting (Task 1.1)
29
+ export { formatToolResponse, formatTextResponse, formatRawResponse, formatErrorResponse, } from './responseFormatter.js';
30
+ // Tag utilities (Task 1.2)
31
+ export { normalizeTag, normalizeTags, hasMatchingTag, hasAllTags, filterByTags, addUniqueTags, removeTags, } from './tagUtils.js';
32
+ // Entity utilities (Task 1.3)
33
+ export { findEntityByName, findEntitiesByNames, entityExists, getEntityIndex, removeEntityByName, getEntityNameSet, groupEntitiesByType, touchEntity, } from './entityUtils.js';
34
+ // Zod validation helpers (Task 1.4)
35
+ export { formatZodErrors, validateWithSchema, validateSafe, validateArrayWithSchema, } from './validationHelper.js';
36
+ // Pagination utilities (Task 1.5)
37
+ export { validatePagination, applyPagination, paginateArray, getPaginationMeta, } from './paginationUtils.js';
38
+ // Filter utilities (Task 1.6)
39
+ export { isWithinImportanceRange, filterByImportance, filterByCreatedDate, filterByModifiedDate, filterByEntityType, entityPassesFilters, } from './filterUtils.js';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Pagination Utilities
3
+ *
4
+ * Centralizes pagination validation and application logic
5
+ * to eliminate duplicate patterns across search implementations.
6
+ */
7
+ import { SEARCH_LIMITS } from './constants.js';
8
+ /**
9
+ * Validates and normalizes pagination parameters.
10
+ * Ensures offset is non-negative and limit is within configured bounds.
11
+ *
12
+ * @param offset - Starting position (default: 0)
13
+ * @param limit - Maximum results to return (default: SEARCH_LIMITS.DEFAULT)
14
+ * @returns Validated pagination parameters with helper methods
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const pagination = validatePagination(10, 50);
19
+ * const results = items.slice(pagination.offset, pagination.offset + pagination.limit);
20
+ * if (pagination.hasMore(items.length)) {
21
+ * console.log('More results available');
22
+ * }
23
+ * ```
24
+ */
25
+ export function validatePagination(offset = 0, limit = SEARCH_LIMITS.DEFAULT) {
26
+ const validatedOffset = Math.max(0, offset);
27
+ const validatedLimit = Math.min(Math.max(SEARCH_LIMITS.MIN, limit), SEARCH_LIMITS.MAX);
28
+ return {
29
+ offset: validatedOffset,
30
+ limit: validatedLimit,
31
+ hasMore: (totalCount) => validatedOffset + validatedLimit < totalCount,
32
+ };
33
+ }
34
+ /**
35
+ * Applies pagination to an array of items.
36
+ *
37
+ * @param items - Array to paginate
38
+ * @param pagination - Validated pagination parameters
39
+ * @returns Paginated slice of the array
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const pagination = validatePagination(offset, limit);
44
+ * const pageResults = applyPagination(allResults, pagination);
45
+ * ```
46
+ */
47
+ export function applyPagination(items, pagination) {
48
+ return items.slice(pagination.offset, pagination.offset + pagination.limit);
49
+ }
50
+ /**
51
+ * Applies pagination using raw offset and limit values.
52
+ * Combines validation and application in one call.
53
+ *
54
+ * @param items - Array to paginate
55
+ * @param offset - Starting position
56
+ * @param limit - Maximum results
57
+ * @returns Paginated slice of the array
58
+ */
59
+ export function paginateArray(items, offset = 0, limit = SEARCH_LIMITS.DEFAULT) {
60
+ const pagination = validatePagination(offset, limit);
61
+ return applyPagination(items, pagination);
62
+ }
63
+ /**
64
+ * Calculates pagination metadata for a result set.
65
+ *
66
+ * @param totalCount - Total number of items
67
+ * @param offset - Current offset
68
+ * @param limit - Current limit
69
+ * @returns Pagination metadata
70
+ */
71
+ export function getPaginationMeta(totalCount, offset = 0, limit = SEARCH_LIMITS.DEFAULT) {
72
+ const pagination = validatePagination(offset, limit);
73
+ return {
74
+ totalCount,
75
+ offset: pagination.offset,
76
+ limit: pagination.limit,
77
+ hasMore: pagination.hasMore(totalCount),
78
+ pageNumber: Math.floor(pagination.offset / pagination.limit) + 1,
79
+ totalPages: Math.ceil(totalCount / pagination.limit),
80
+ };
81
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * MCP Tool Response Formatter Utilities
3
+ *
4
+ * Centralizes response formatting for MCP tool calls to eliminate
5
+ * redundant JSON.stringify patterns across the codebase.
6
+ */
7
+ /**
8
+ * Formats data as an MCP tool response with JSON content.
9
+ * Centralizes the response format to ensure consistency and reduce duplication.
10
+ *
11
+ * @param data - Any data to be JSON stringified
12
+ * @returns Formatted MCP tool response
13
+ */
14
+ export function formatToolResponse(data) {
15
+ return {
16
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
17
+ };
18
+ }
19
+ /**
20
+ * Formats a simple text message as an MCP tool response.
21
+ * Use for success messages that don't need JSON formatting.
22
+ *
23
+ * @param message - Plain text message
24
+ * @returns Formatted MCP tool response
25
+ */
26
+ export function formatTextResponse(message) {
27
+ return {
28
+ content: [{ type: 'text', text: message }],
29
+ };
30
+ }
31
+ /**
32
+ * Formats raw string content as an MCP tool response.
33
+ * Use for export formats that return pre-formatted strings (markdown, CSV, etc.)
34
+ *
35
+ * @param content - Raw string content
36
+ * @returns Formatted MCP tool response
37
+ */
38
+ export function formatRawResponse(content) {
39
+ return {
40
+ content: [{ type: 'text', text: content }],
41
+ };
42
+ }
43
+ /**
44
+ * Formats an error as an MCP tool response with isError flag.
45
+ *
46
+ * @param error - Error object or message string
47
+ * @returns Formatted MCP tool error response
48
+ */
49
+ export function formatErrorResponse(error) {
50
+ const message = error instanceof Error ? error.message : error;
51
+ return {
52
+ content: [{ type: 'text', text: message }],
53
+ isError: true,
54
+ };
55
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Tag Normalization and Matching Utilities
3
+ *
4
+ * Centralizes tag operations to eliminate duplicate normalization logic
5
+ * across the codebase. All tags are normalized to lowercase for consistent matching.
6
+ */
7
+ /**
8
+ * Normalizes a single tag to lowercase and trimmed.
9
+ *
10
+ * @param tag - Tag to normalize
11
+ * @returns Normalized tag
12
+ */
13
+ export function normalizeTag(tag) {
14
+ return tag.toLowerCase().trim();
15
+ }
16
+ /**
17
+ * Normalizes an array of tags to lowercase.
18
+ * Handles undefined/null input gracefully.
19
+ *
20
+ * @param tags - Array of tags to normalize, or undefined
21
+ * @returns Normalized tags array, or empty array if input is undefined/null
22
+ */
23
+ export function normalizeTags(tags) {
24
+ if (!tags || tags.length === 0)
25
+ return [];
26
+ return tags.map(tag => tag.toLowerCase());
27
+ }
28
+ /**
29
+ * Checks if an entity's tags include any of the specified search tags.
30
+ * Both inputs are normalized before comparison.
31
+ *
32
+ * @param entityTags - Tags on the entity (may be undefined)
33
+ * @param searchTags - Tags to search for (may be undefined)
34
+ * @returns true if any search tag matches any entity tag, false if no match or either is empty
35
+ */
36
+ export function hasMatchingTag(entityTags, searchTags) {
37
+ if (!entityTags || entityTags.length === 0)
38
+ return false;
39
+ if (!searchTags || searchTags.length === 0)
40
+ return false;
41
+ const normalizedEntity = normalizeTags(entityTags);
42
+ const normalizedSearch = normalizeTags(searchTags);
43
+ return normalizedSearch.some(tag => normalizedEntity.includes(tag));
44
+ }
45
+ /**
46
+ * Checks if entity tags include ALL of the specified required tags.
47
+ *
48
+ * @param entityTags - Tags on the entity (may be undefined)
49
+ * @param requiredTags - All tags that must be present
50
+ * @returns true if all required tags are present
51
+ */
52
+ export function hasAllTags(entityTags, requiredTags) {
53
+ if (!entityTags || entityTags.length === 0)
54
+ return false;
55
+ if (requiredTags.length === 0)
56
+ return true;
57
+ const normalizedEntity = normalizeTags(entityTags);
58
+ return normalizeTags(requiredTags).every(tag => normalizedEntity.includes(tag));
59
+ }
60
+ /**
61
+ * Filters entities by tag match.
62
+ * Returns all entities if searchTags is empty or undefined.
63
+ *
64
+ * @param entities - Array of entities with optional tags property
65
+ * @param searchTags - Tags to filter by
66
+ * @returns Filtered entities that have at least one matching tag
67
+ */
68
+ export function filterByTags(entities, searchTags) {
69
+ if (!searchTags || searchTags.length === 0) {
70
+ return entities;
71
+ }
72
+ const normalizedSearch = normalizeTags(searchTags);
73
+ return entities.filter(entity => {
74
+ if (!entity.tags || entity.tags.length === 0)
75
+ return false;
76
+ const normalizedEntity = normalizeTags(entity.tags);
77
+ return normalizedSearch.some(tag => normalizedEntity.includes(tag));
78
+ });
79
+ }
80
+ /**
81
+ * Adds new tags to an existing tag array, avoiding duplicates.
82
+ * All tags are normalized to lowercase.
83
+ *
84
+ * @param existingTags - Current tags (may be undefined)
85
+ * @param newTags - Tags to add
86
+ * @returns Combined tags array with no duplicates
87
+ */
88
+ export function addUniqueTags(existingTags, newTags) {
89
+ const existing = normalizeTags(existingTags);
90
+ const toAdd = normalizeTags(newTags);
91
+ const uniqueNew = toAdd.filter(tag => !existing.includes(tag));
92
+ return [...existing, ...uniqueNew];
93
+ }
94
+ /**
95
+ * Removes specified tags from an existing tag array.
96
+ * Comparison is case-insensitive.
97
+ *
98
+ * @param existingTags - Current tags (may be undefined)
99
+ * @param tagsToRemove - Tags to remove
100
+ * @returns Tags array with specified tags removed
101
+ */
102
+ export function removeTags(existingTags, tagsToRemove) {
103
+ if (!existingTags || existingTags.length === 0)
104
+ return [];
105
+ const toRemoveNormalized = normalizeTags(tagsToRemove);
106
+ return existingTags.filter(tag => !toRemoveNormalized.includes(tag.toLowerCase()));
107
+ }