@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
597 lines • 24.2 kB
JavaScript
import { randomUUID } from 'crypto';
import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js';
/**
* Store a new memory
*/
const storeMemoryTool = createTool({
name: 'store_memory',
description: 'Store information in the project memory for future reference and context',
category: 'memory-management',
inputSchema: {
type: 'object',
properties: {
content: {
type: 'string',
description: 'The content to store in memory',
minLength: 1,
maxLength: 10000
},
type: {
type: 'string',
enum: ['code', 'documentation', 'decision', 'learning', 'context', 'insight'],
description: 'Type of memory being stored'
},
title: {
type: 'string',
description: 'Optional title for the memory',
maxLength: 200
},
tags: {
type: 'array',
items: { type: 'string', maxLength: 50 },
description: 'Tags for categorizing and searching',
maxItems: 20
},
metadata: {
type: 'object',
description: 'Additional structured metadata',
additionalProperties: true
},
importance: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
default: 'medium',
description: 'Importance level of this memory'
},
category: {
type: 'string',
description: 'Category for grouping related memories',
maxLength: 100
},
source: {
type: 'string',
description: 'Source of the information (file, meeting, etc.)',
maxLength: 500
},
relatedIds: {
type: 'array',
items: { type: 'string', pattern: '^[a-zA-Z0-9-]+$' },
description: 'IDs of related memories',
maxItems: 10
}
},
required: ['content', 'type'],
additionalProperties: false
},
async execute(input, context) {
try {
const memoryId = randomUUID();
const now = Date.now();
// Validate related memory IDs if provided
if (input.relatedIds && input.relatedIds.length > 0) {
for (const relatedId of input.relatedIds) {
const relatedCheck = await context.db.get('SELECT id FROM memories WHERE id = ? AND project_id = ?', [relatedId, context.projectId || 'default']);
if (!relatedCheck.success || !relatedCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: `Related memory not found: ${relatedId}`,
category: 'validation'
});
}
}
}
// Generate a title if not provided
const title = input.title || generateTitleFromContent(input.content, input.type);
// Insert memory into database
const result = await context.db.run(`INSERT INTO memories
(id, title, content, type, tags, metadata, importance, category,
source, project_id, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
memoryId,
title,
input.content,
input.type,
JSON.stringify(input.tags || []),
JSON.stringify(input.metadata || {}),
input.importance || 'medium',
input.category || null,
input.source || null,
context.projectId || 'default',
context.userId || 'system',
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to store memory',
details: { error: result.error },
category: 'system'
});
}
// Create relationships if related IDs provided
if (input.relatedIds && input.relatedIds.length > 0) {
for (const relatedId of input.relatedIds) {
await context.db.run('INSERT INTO memory_relationships (id, memory_id, related_memory_id, link_type, created_at) VALUES (?, ?, ?, ?, ?)', [randomUUID(), memoryId, relatedId, 'related', now]);
}
}
return createSuccessResult({
memory: {
id: memoryId,
title,
type: input.type,
importance: input.importance || 'medium',
category: input.category || null,
tags: input.tags || [],
relatedCount: input.relatedIds?.length || 0,
createdAt: new Date(now).toISOString()
},
message: `Memory "${title}" stored successfully`,
searchable: true,
relatedMemories: input.relatedIds || []
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to store memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Search memories
*/
const searchMemoryTool = createTool({
name: 'search_memory',
description: 'Search stored memories using keywords, filters, and metadata',
category: 'memory-management',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for finding relevant memories',
minLength: 1,
maxLength: 500
},
type: {
type: 'string',
enum: ['code', 'documentation', 'decision', 'learning', 'context', 'insight'],
description: 'Filter by memory type'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by specific tags',
maxItems: 10
},
importance: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: 'Filter by importance level'
},
category: {
type: 'string',
description: 'Filter by category',
maxLength: 100
},
dateRange: {
type: 'object',
properties: {
from: { type: 'string', format: 'date' },
to: { type: 'string', format: 'date' }
},
description: 'Filter by date range'
},
limit: {
type: 'integer',
description: 'Maximum number of results',
minimum: 1,
maximum: 100,
default: 20
},
includeContent: {
type: 'boolean',
description: 'Include full content in results',
default: false
}
},
required: ['query'],
additionalProperties: false
},
async execute(input, context) {
try {
// Build the search query
let sql = `
SELECT m.*,
(CASE
WHEN m.title LIKE ? THEN 10
WHEN m.content LIKE ? THEN 5
WHEN m.tags LIKE ? THEN 3
ELSE 1
END) as relevance_score
FROM memories m
WHERE m.project_id = ?
AND (m.title LIKE ? OR m.content LIKE ? OR m.tags LIKE ? OR m.category LIKE ?)
`;
const searchPattern = `%${input.query}%`;
const params = [
searchPattern, // title relevance
searchPattern, // content relevance
searchPattern, // tags relevance
context.projectId || 'default',
searchPattern, // title search
searchPattern, // content search
searchPattern, // tags search
searchPattern // category search
];
// Add filters
if (input.type) {
sql += ' AND m.type = ?';
params.push(input.type);
}
if (input.importance) {
sql += ' AND m.importance = ?';
params.push(input.importance);
}
if (input.category) {
sql += ' AND m.category = ?';
params.push(input.category);
}
if (input.tags && input.tags.length > 0) {
const tagConditions = input.tags.map(() => 'm.tags LIKE ?').join(' AND ');
sql += ` AND (${tagConditions})`;
input.tags.forEach(tag => params.push(`%"${tag}"%`));
}
if (input.dateRange) {
if (input.dateRange.from) {
sql += ' AND m.created_at >= ?';
params.push(new Date(input.dateRange.from).getTime().toString());
}
if (input.dateRange.to) {
sql += ' AND m.created_at <= ?';
params.push(new Date(input.dateRange.to).getTime().toString());
}
}
sql += ' ORDER BY relevance_score DESC, m.importance DESC, m.created_at DESC LIMIT ?';
params.push((input.limit || 20).toString());
const result = await context.db.query(sql, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to search memories',
details: { error: result.error },
category: 'system'
});
}
const memories = (result.data || []).map((memory) => {
const tags = JSON.parse(memory.tags || '[]');
const metadata = JSON.parse(memory.metadata || '{}');
const memoryData = {
id: memory.id,
title: memory.title,
type: memory.type,
importance: memory.importance,
category: memory.category,
tags,
source: memory.source,
relevanceScore: memory.relevance_score,
createdBy: memory.created_by,
createdAt: new Date(memory.created_at).toISOString(),
updatedAt: new Date(memory.updated_at).toISOString()
};
if (input.includeContent) {
memoryData.content = memory.content;
memoryData.metadata = metadata;
}
else {
// Provide a content preview
memoryData.contentPreview = memory.content.length > 200
? memory.content.substring(0, 200) + '...'
: memory.content;
}
return memoryData;
});
return createSuccessResult({
memories,
count: memories.length,
query: input.query,
filters: {
type: input.type || null,
importance: input.importance || null,
category: input.category || null,
tags: input.tags || [],
dateRange: input.dateRange || null
},
hasMore: memories.length === (input.limit || 20),
searchTips: memories.length === 0 ? [
'Try using broader search terms',
'Check your filters - they might be too restrictive',
'Search in different memory types',
'Consider searching by tags or categories'
] : undefined
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to search memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get specific memory by ID
*/
const getMemoryTool = createTool({
name: 'get_memory',
description: 'Retrieve a specific memory by its ID with full content and relationships',
category: 'memory-management',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
memoryId: {
type: 'string',
description: 'Memory ID to retrieve',
pattern: '^[a-zA-Z0-9-]+$'
}
},
required: ['memoryId'],
additionalProperties: false
},
async execute(input, context) {
try {
// Get the memory
const memoryResult = await context.db.get('SELECT * FROM memories WHERE id = ? AND project_id = ?', [input.memoryId, context.projectId || 'default']);
if (!memoryResult.success || !memoryResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Memory not found',
details: { memoryId: input.memoryId },
category: 'validation'
});
}
const memory = memoryResult.data;
// Get related memories
const relatedResult = await context.db.query(`SELECT r.link_type, r.description, r.created_at as linked_at,
rm.id, rm.title, rm.type, rm.importance
FROM memory_relationships r
JOIN memories rm ON (r.related_memory_id = rm.id OR r.memory_id = rm.id)
WHERE (r.memory_id = ? OR r.related_memory_id = ?) AND rm.id != ?`, [input.memoryId, input.memoryId, input.memoryId]);
const relatedMemories = (relatedResult.data || []).map((rel) => ({
id: rel.id,
title: rel.title,
type: rel.type,
importance: rel.importance,
linkType: rel.link_type,
description: rel.description,
linkedAt: new Date(rel.linked_at).toISOString()
}));
return createSuccessResult({
memory: {
id: memory.id,
title: memory.title,
content: memory.content,
type: memory.type,
importance: memory.importance,
category: memory.category,
tags: JSON.parse(memory.tags || '[]'),
metadata: JSON.parse(memory.metadata || '{}'),
source: memory.source,
createdBy: memory.created_by,
createdAt: new Date(memory.created_at).toISOString(),
updatedAt: new Date(memory.updated_at).toISOString()
},
relatedMemories,
statistics: {
relatedCount: relatedMemories.length,
contentLength: memory.content.length,
tagCount: JSON.parse(memory.tags || '[]').length
}
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get memory statistics and insights
*/
const getMemoryStatsTool = createTool({
name: 'get_memory_stats',
description: 'Get statistics and insights about stored memories',
category: 'memory-management',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
timeframe: {
type: 'string',
enum: ['24h', '7d', '30d', '90d', 'all'],
default: '30d',
description: 'Time period for statistics'
},
type: {
type: 'string',
enum: ['code', 'documentation', 'decision', 'learning', 'context', 'insight'],
description: 'Filter statistics by memory type'
}
},
additionalProperties: false
},
async execute(input, context) {
try {
// Calculate time threshold
const now = Date.now();
let timeThreshold = 0;
if (input.timeframe !== 'all') {
switch (input.timeframe) {
case '24h':
timeThreshold = now - (24 * 60 * 60 * 1000);
break;
case '7d':
timeThreshold = now - (7 * 24 * 60 * 60 * 1000);
break;
case '30d':
timeThreshold = now - (30 * 24 * 60 * 60 * 1000);
break;
case '90d':
timeThreshold = now - (90 * 24 * 60 * 60 * 1000);
break;
}
}
let whereClause = 'WHERE project_id = ?';
const params = [context.projectId || 'default'];
if (timeThreshold > 0) {
whereClause += ' AND created_at >= ?';
params.push(timeThreshold.toString());
}
if (input.type) {
whereClause += ' AND type = ?';
params.push(input.type);
}
// Get basic statistics
const totalResult = await context.db.get(`SELECT COUNT(*) as total FROM memories ${whereClause}`, params);
// Get type distribution
const typeResult = await context.db.query(`SELECT type, COUNT(*) as count FROM memories ${whereClause} GROUP BY type`, params);
// Get importance distribution
const importanceResult = await context.db.query(`SELECT importance, COUNT(*) as count FROM memories ${whereClause} GROUP BY importance`, params);
// Get category distribution
const categoryResult = await context.db.query(`SELECT category, COUNT(*) as count FROM memories ${whereClause}
AND category IS NOT NULL GROUP BY category ORDER BY count DESC LIMIT 10`, params);
// Get recent activity
const recentResult = await context.db.query(`SELECT DATE(created_at/1000, 'unixepoch') as date, COUNT(*) as count
FROM memories ${whereClause}
GROUP BY DATE(created_at/1000, 'unixepoch')
ORDER BY date DESC LIMIT 30`, params);
// Get top creators
const creatorsResult = await context.db.query(`SELECT created_by, COUNT(*) as count FROM memories ${whereClause}
AND created_by IS NOT NULL GROUP BY created_by ORDER BY count DESC LIMIT 10`, params);
const total = totalResult.data?.total || 0;
const typeDistribution = (typeResult.data || []).reduce((acc, row) => {
acc[row.type] = row.count;
return acc;
}, {});
const importanceDistribution = (importanceResult.data || []).reduce((acc, row) => {
acc[row.importance] = row.count;
return acc;
}, {});
const topCategories = (categoryResult.data || []).map((row) => ({
category: row.category,
count: row.count
}));
const dailyActivity = (recentResult.data || []).map((row) => ({
date: row.date,
count: row.count
}));
const topCreators = (creatorsResult.data || []).map((row) => ({
user: row.created_by,
count: row.count
}));
return createSuccessResult({
timeframe: input.timeframe || '30d',
type: input.type || null,
overview: {
totalMemories: total,
averagePerDay: dailyActivity.length > 0
? Math.round((total / dailyActivity.length) * 100) / 100
: 0
},
distribution: {
byType: typeDistribution,
byImportance: importanceDistribution,
topCategories,
topCreators
},
activity: {
dailyActivity: dailyActivity.slice(0, 7), // Last 7 days
trend: calculateTrend(dailyActivity)
},
insights: generateInsights(total, typeDistribution, importanceDistribution, dailyActivity)
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get memory statistics: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Helper functions
*/
function generateTitleFromContent(content, type) {
const words = content.split(/\s+/).slice(0, 8).join(' ');
const truncated = words.length > 50 ? words.substring(0, 47) + '...' : words;
const typePrefix = type.charAt(0).toUpperCase() + type.slice(1);
return `${typePrefix}: ${truncated}`;
}
function calculateTrend(dailyActivity) {
if (dailyActivity.length < 2)
return 'stable';
const recent = dailyActivity.slice(0, 3).reduce((sum, day) => sum + day.count, 0);
const older = dailyActivity.slice(3, 6).reduce((sum, day) => sum + day.count, 0);
if (recent > older * 1.2)
return 'increasing';
if (recent < older * 0.8)
return 'decreasing';
return 'stable';
}
function generateInsights(total, typeDistribution, importanceDistribution, dailyActivity) {
const insights = [];
if (total === 0) {
insights.push('No memories stored yet - start capturing important information');
return insights;
}
if (total < 10) {
insights.push('Getting started with memory collection - consider storing more context');
}
else if (total > 100) {
insights.push('Rich memory collection - good knowledge base building');
}
const topType = Object.entries(typeDistribution).reduce((a, b) => typeDistribution[a[0]] > typeDistribution[b[0]] ? a : b)?.[0];
if (topType) {
insights.push(`Most common memory type: ${topType}`);
}
const criticalCount = importanceDistribution.critical || 0;
if (criticalCount > 0) {
insights.push(`${criticalCount} critical memories stored`);
}
if (dailyActivity.length > 0) {
const avgDaily = dailyActivity.reduce((sum, day) => sum + day.count, 0) / dailyActivity.length;
if (avgDaily > 2) {
insights.push('High memory activity - good knowledge capture');
}
else if (avgDaily < 0.5) {
insights.push('Low recent activity - consider storing more insights');
}
}
return insights;
}
/**
* Setup memory management tools
*/
export async function setupMemoryManagementTools() {
return {
module: 'memory-management',
tools: [
storeMemoryTool,
searchMemoryTool,
getMemoryTool,
getMemoryStatsTool
]
};
}
//# sourceMappingURL=tools.js.map