UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

217 lines 8.39 kB
/** * Knowledge Graph Resource - Read-only knowledge graph data * URI Pattern: knowledge://graph * * Provides direct access to the knowledge graph structure with nodes, edges, and metadata. * This replaces querying KnowledgeGraphManager as a Tool, following ADR-018 atomic tools pattern. */ import { McpAdrError } from '../types/index.js'; import { resourceCache, generateETag } from './resource-cache.js'; import { resourceRouter } from './resource-router.js'; /** * Generate knowledge graph resource providing read-only access to graph structure. * * Returns comprehensive graph data including nodes (intents, ADRs, tools, code files) * and edges (relationships between nodes). This is a read-only view - use the * update_knowledge tool to modify the graph. * * **URI Pattern:** `knowledge://graph` * * **Query Parameters:** (none) * * @param params - URL path parameters (none for this resource) * @param searchParams - URL query parameters (none used) * @param kgManager - KnowledgeGraphManager instance (injected by MCP server) * * @returns Promise resolving to resource generation result containing: * - data: Knowledge graph with nodes, edges, and metadata * - contentType: "application/json" * - lastModified: ISO timestamp of generation * - cacheKey: Unique identifier "knowledge-graph" * - ttl: Cache duration (60 seconds) * - etag: Entity tag for cache validation * * @throws {McpAdrError} When: * - RESOURCE_GENERATION_ERROR: KnowledgeGraphManager not provided or graph loading fails * * @example * ```typescript * // Get knowledge graph * const graph = await generateKnowledgeGraphResource( * {}, * new URLSearchParams(), * kgManager * ); * * console.log(`Nodes: ${graph.data.nodes.length}`); * console.log(`Edges: ${graph.data.edges.length}`); * console.log(`Active Intents: ${graph.data.analytics.activeIntents}`); * * // Expected output structure: * { * data: { * nodes: [ * { id: "adr-001", type: "adr", title: "Use React", status: "accepted" }, * { id: "intent-123", type: "intent", name: "Add auth", status: "executing" }, * { id: "tool-analyze", type: "tool", name: "analyze_project_ecosystem" } * ], * edges: [ * { source: "intent-123", target: "adr-001", relationship: "created" }, * { source: "intent-123", target: "tool-analyze", relationship: "uses" } * ], * metadata: { * lastUpdated: "2025-12-16T13:00:00.000Z", * nodeCount: 42, * edgeCount: 18, * intentCount: 15, * adrCount: 8, * toolCount: 5, * version: "1.0.0" * }, * analytics: { * totalIntents: 15, * completedIntents: 10, * activeIntents: 5, * averageGoalCompletion: 0.67, * mostUsedTools: [...] * } * }, * contentType: "application/json", * cacheKey: "knowledge-graph", * ttl: 60 * } * ``` * * @since v2.2.0 * @see {@link KnowledgeGraphManager.loadKnowledgeGraph} for graph data source */ export async function generateKnowledgeGraphResource(_params, _searchParams, kgManager) { const cacheKey = 'knowledge-graph'; // Check cache const cached = await resourceCache.get(cacheKey); if (cached) { return cached; } try { if (!kgManager) { throw new McpAdrError('Knowledge graph requires initialized knowledge graph manager. Access this resource through the MCP server.', 'RESOURCE_GENERATION_ERROR'); } // Load knowledge graph snapshot const snapshot = await kgManager.loadKnowledgeGraph(); // Build nodes from intents const nodes = []; const edges = []; const toolSet = new Set(); // Process intents into nodes and extract tool relationships for (const intent of snapshot.intents) { nodes.push({ id: intent.intentId, type: 'intent', name: intent.humanRequest, status: intent.currentStatus, timestamp: intent.timestamp, relevanceScore: calculateIntentRelevance(intent), }); // Add tool nodes and edges for (const tool of intent.toolChain) { const toolId = `tool-${tool.toolName}`; if (!toolSet.has(toolId)) { toolSet.add(toolId); nodes.push({ id: toolId, type: 'tool', name: tool.toolName, }); } edges.push({ source: intent.intentId, target: toolId, relationship: 'uses', success: tool.success, }); } // Add ADR nodes and edges if present const adrsCreated = intent.adrsCreated || []; for (const adrId of adrsCreated) { const adrNodeId = `adr-${adrId}`; nodes.push({ id: adrNodeId, type: 'adr', title: `ADR ${adrId}`, status: 'accepted', }); edges.push({ source: intent.intentId, target: adrNodeId, relationship: 'created', }); } } // Count node types const intentCount = nodes.filter(n => n.type === 'intent').length; const adrCount = nodes.filter(n => n.type === 'adr').length; const toolCount = nodes.filter(n => n.type === 'tool').length; const graphData = { nodes, edges, metadata: { lastUpdated: new Date().toISOString(), nodeCount: nodes.length, edgeCount: edges.length, intentCount, adrCount, toolCount, version: snapshot.version, }, analytics: { totalIntents: snapshot.analytics.totalIntents, completedIntents: snapshot.analytics.completedIntents, activeIntents: snapshot.analytics.activeIntents, averageGoalCompletion: snapshot.analytics.averageGoalCompletion, mostUsedTools: snapshot.analytics.mostUsedTools, }, }; const result = { data: graphData, contentType: 'application/json', lastModified: new Date().toISOString(), cacheKey, ttl: 60, // 60 seconds cache (graph changes moderately frequently) etag: generateETag(graphData), }; // Cache result resourceCache.set(cacheKey, result, result.ttl); return result; } catch (error) { throw new McpAdrError(`Failed to generate knowledge graph: ${error instanceof Error ? error.message : String(error)}`, 'RESOURCE_GENERATION_ERROR'); } } /** * Calculate relevance score for an intent based on status and age */ function calculateIntentRelevance(intent) { const now = new Date(); const age = now.getTime() - new Date(intent.timestamp).getTime(); const ageInDays = age / (24 * 60 * 60 * 1000); // Base relevance on status let relevance = 0.5; if (intent.currentStatus === 'executing') relevance = 0.9; else if (intent.currentStatus === 'planning') relevance = 0.8; else if (intent.currentStatus === 'completed') relevance = 0.6; // Decay relevance over time relevance *= Math.max(0.3, 1 - ageInDays / 30); // Boost relevance if it has successful tool executions const successRate = intent.toolChain.filter((t) => t.success).length / (intent.toolChain.length || 1); relevance = (relevance + successRate) / 2; return Math.round(relevance * 100) / 100; } // Note: This resource requires KnowledgeGraphManager to be injected. // The route is registered but the handler needs to be wrapped with manager injection // when called from the MCP server. See index.ts readResource method for injection pattern. resourceRouter.register('/graph', generateKnowledgeGraphResource, // TypeScript workaround for manager injection pattern 'Knowledge graph structure with nodes, edges, and metadata'); //# sourceMappingURL=knowledge-graph-resource.js.map