mcp-adr-analysis-server
Version:
MCP server for analyzing Architectural Decision Records and project architecture
125 lines • 4.63 kB
JavaScript
/**
* Research by Topic Resource - Topic-specific research documents
* URI Pattern: adr://research/{topic}
*/
import * as path from 'path';
import * as fs from 'fs/promises';
import { McpAdrError } from '../types/index.js';
import { resourceCache, generateETag } from './resource-cache.js';
import { resourceRouter } from './resource-router.js';
/**
* Extract title from markdown content
*/
function extractTitle(content) {
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch && titleMatch[1]) {
return titleMatch[1].trim();
}
return 'Untitled';
}
/**
* Generate topic summary from documents
*/
function generateTopicSummary(documents) {
const firstDoc = documents[0];
return {
totalWords: documents.reduce((sum, doc) => sum + doc.wordCount, 0),
totalSize: documents.reduce((sum, doc) => sum + doc.size, 0),
lastUpdated: documents.length > 0 && firstDoc
? documents.reduce((latest, doc) => (doc.lastModified > latest ? doc.lastModified : latest), firstDoc.lastModified)
: new Date().toISOString(),
documentCount: documents.length,
};
}
/**
* Check if a file matches the topic
*/
function matchesTopic(filename, content, topic) {
const normalizedTopic = topic.toLowerCase().replace(/-/g, '_');
const normalizedFilename = filename.toLowerCase();
const normalizedContent = content.toLowerCase();
// Check filename
if (normalizedFilename.includes(topic.toLowerCase()) ||
normalizedFilename.includes(normalizedTopic)) {
return true;
}
// Check content (first 1000 characters for keywords)
const contentPreview = normalizedContent.substring(0, 1000);
if (contentPreview.includes(topic.toLowerCase()) || contentPreview.includes(normalizedTopic)) {
return true;
}
return false;
}
/**
* Generate research by topic resource
*/
export async function generateResearchByTopicResource(params, _searchParams) {
const topic = params['topic'];
if (!topic) {
throw new McpAdrError('Missing required parameter: topic', 'INVALID_PARAMS');
}
const cacheKey = `research-topic:${topic}`;
// Check cache
const cached = await resourceCache.get(cacheKey);
if (cached) {
return cached;
}
const researchDirs = ['docs/research', 'custom/research'];
const relatedDocs = [];
// Scan research directories
for (const dir of researchDirs) {
const fullPath = path.resolve(process.cwd(), dir);
try {
const files = await fs.readdir(fullPath);
for (const file of files) {
if (file.endsWith('.md')) {
const filePath = path.join(fullPath, file);
const content = await fs.readFile(filePath, 'utf-8');
// Check if file matches topic
if (matchesTopic(file, content, topic)) {
const stats = await fs.stat(filePath);
relatedDocs.push({
id: file.replace('.md', ''),
title: extractTitle(content),
topic,
path: path.join(dir, file),
content,
lastModified: stats.mtime.toISOString(),
wordCount: content.split(/\s+/).length,
size: stats.size,
});
}
}
}
}
catch {
// Directory doesn't exist or can't be read, skip silently
continue;
}
}
// Sort by last modified (newest first)
relatedDocs.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
if (relatedDocs.length === 0) {
throw new McpAdrError(`No research documents found for topic: ${topic}`, 'RESOURCE_NOT_FOUND');
}
const topicData = {
topic,
documentCount: relatedDocs.length,
documents: relatedDocs,
summary: generateTopicSummary(relatedDocs),
};
const result = {
data: topicData,
contentType: 'application/json',
lastModified: new Date().toISOString(),
cacheKey,
ttl: 600, // 10 minutes cache
etag: generateETag(topicData),
};
// Cache result
resourceCache.set(cacheKey, result, result.ttl);
return result;
}
// Register route
resourceRouter.register('/research/{topic}', generateResearchByTopicResource, 'Research documents filtered by topic');
//# sourceMappingURL=research-by-topic-resource.js.map