intent-graph-mcp-server
Version:
Universal LLM-powered MCP server for automated Intent Graph generation (Writer Palmyra, Claude, OpenAI, custom)
360 lines • 13.3 kB
JavaScript
/**
* IntentGraph Utility Functions
* Helper functions for graph operations, validation, and analysis
*/
/**
* Generate unique node ID
*/
export function generateNodeId(agentName) {
const sanitized = agentName.toLowerCase().replace(/[^a-z0-9]/g, '_');
const timestamp = Date.now().toString(36);
return `node_${sanitized}_${timestamp}`;
}
/**
* Generate unique edge ID
*/
export function generateEdgeId(fromNode, toNode) {
const timestamp = Date.now().toString(36);
return `edge_${fromNode}_to_${toNode}_${timestamp}`;
}
/**
* Calculate complexity metrics for a graph
*/
export function calculateComplexityMetrics(nodes, edges) {
const nodeCount = nodes.length;
const edgeCount = edges.length;
// Calculate depth (longest path from entry to exit)
const depth = calculateDepth(nodes, edges);
// Calculate width (max parallel nodes at any level)
const width = calculateWidth(nodes, edges);
// Calculate cyclomatic complexity (edges - nodes + 2 * connected components)
const connectedComponents = 1; // Assume single connected graph
const cyclomaticComplexity = edgeCount - nodeCount + 2 * connectedComponents;
// Calculate overall complexity score (1-100)
const complexityScore = Math.min(100, Math.max(1, Math.floor(nodeCount * 2 +
edgeCount * 1.5 +
depth * 3 +
cyclomaticComplexity * 5)));
return {
node_count: nodeCount,
edge_count: edgeCount,
depth,
width,
complexity_score: complexityScore,
cyclomatic_complexity: cyclomaticComplexity
};
}
/**
* Calculate graph depth (longest path)
*/
function calculateDepth(nodes, edges) {
if (nodes.length === 0)
return 0;
const adjacency = new Map();
nodes.forEach(n => adjacency.set(n.node_id, []));
edges.forEach(e => {
const neighbors = adjacency.get(e.from_node) || [];
neighbors.push(e.to_node);
adjacency.set(e.from_node, neighbors);
});
let maxDepth = 0;
function dfs(nodeId, depth, visited) {
if (visited.has(nodeId))
return;
visited.add(nodeId);
maxDepth = Math.max(maxDepth, depth);
const neighbors = adjacency.get(nodeId) || [];
neighbors.forEach(next => dfs(next, depth + 1, visited));
}
// Start DFS from entry nodes
const entryNodes = nodes.filter(n => n.node_type === 'entry');
if (entryNodes.length === 0 && nodes.length > 0) {
// If no entry nodes, use first node
dfs(nodes[0].node_id, 1, new Set());
}
else {
entryNodes.forEach(n => dfs(n.node_id, 1, new Set()));
}
return maxDepth;
}
/**
* Calculate graph width (max parallel nodes)
*/
function calculateWidth(nodes, edges) {
if (nodes.length === 0)
return 0;
// Count parallel edges from each node
const parallelCounts = new Map();
edges.forEach(e => {
if (e.edge_type === 'parallel') {
const count = parallelCounts.get(e.from_node) || 0;
parallelCounts.set(e.from_node, count + 1);
}
});
const maxParallel = Math.max(1, ...Array.from(parallelCounts.values()));
return maxParallel;
}
/**
* Validate graph structure
*/
export function validateGraph(graph) {
const checks = [];
let isValid = true;
// Check 1: Unique node IDs
const nodeIds = new Set();
const duplicateNodes = [];
graph.intent_graph.nodes.forEach(n => {
if (nodeIds.has(n.node_id)) {
duplicateNodes.push(n.node_id);
}
nodeIds.add(n.node_id);
});
const uniqueNodeCheck = duplicateNodes.length === 0;
checks.push({
check_name: 'unique_node_ids',
passed: uniqueNodeCheck,
message: uniqueNodeCheck ? 'All node IDs are unique' : `Duplicate node IDs: ${duplicateNodes.join(', ')}`
});
if (!uniqueNodeCheck)
isValid = false;
// Check 2: Unique edge IDs
const edgeIds = new Set();
const duplicateEdges = [];
graph.intent_graph.edges.forEach(e => {
if (edgeIds.has(e.edge_id)) {
duplicateEdges.push(e.edge_id);
}
edgeIds.add(e.edge_id);
});
const uniqueEdgeCheck = duplicateEdges.length === 0;
checks.push({
check_name: 'unique_edge_ids',
passed: uniqueEdgeCheck,
message: uniqueEdgeCheck ? 'All edge IDs are unique' : `Duplicate edge IDs: ${duplicateEdges.join(', ')}`
});
if (!uniqueEdgeCheck)
isValid = false;
// Check 3: Valid edge references
const invalidEdges = [];
graph.intent_graph.edges.forEach(e => {
if (!nodeIds.has(e.from_node) || !nodeIds.has(e.to_node)) {
invalidEdges.push(e.edge_id);
}
});
const validEdgeCheck = invalidEdges.length === 0;
checks.push({
check_name: 'valid_edge_references',
passed: validEdgeCheck,
message: validEdgeCheck ? 'All edges reference existing nodes' : `Invalid edges: ${invalidEdges.join(', ')}`
});
if (!validEdgeCheck)
isValid = false;
// Check 4: Entry and exit points exist
const hasEntry = graph.intent_graph.execution_plan.entry_points.length > 0;
const hasExit = graph.intent_graph.execution_plan.exit_points.length > 0;
const entryExitCheck = hasEntry && hasExit;
checks.push({
check_name: 'entry_exit_points',
passed: entryExitCheck,
message: entryExitCheck
? 'Entry and exit points defined'
: `Missing: ${!hasEntry ? 'entry points' : ''} ${!hasExit ? 'exit points' : ''}`
});
if (!entryExitCheck)
isValid = false;
// Check 5: No cycles (DAG structure) unless iteration edges exist
const hasCycles = detectCycles(graph.intent_graph.nodes, graph.intent_graph.edges);
const hasIterationEdges = graph.intent_graph.edges.some(e => e.edge_type === 'iteration');
const dagCheck = !hasCycles || hasIterationEdges;
checks.push({
check_name: 'dag_structure',
passed: dagCheck,
message: dagCheck
? (hasIterationEdges ? 'Valid graph with iteration cycles' : 'Graph is a valid DAG')
: 'Graph contains cycles without iteration edges'
});
if (!dagCheck)
isValid = false;
// Check 6: All nodes reachable from entry points
const reachableNodes = findReachableNodes(graph.intent_graph.execution_plan.entry_points, graph.intent_graph.edges);
const allNodesReachable = graph.intent_graph.nodes.every(n => reachableNodes.has(n.node_id) || graph.intent_graph.execution_plan.entry_points.includes(n.node_id));
checks.push({
check_name: 'all_nodes_reachable',
passed: allNodesReachable,
message: allNodesReachable
? 'All nodes are reachable from entry points'
: `${graph.intent_graph.nodes.length - reachableNodes.size} orphaned nodes detected`
});
if (!allNodesReachable)
isValid = false;
return {
is_valid: isValid,
checks_performed: checks,
validation_timestamp: new Date().toISOString()
};
}
/**
* Detect cycles in graph
*/
function detectCycles(nodes, edges) {
const adjacency = new Map();
nodes.forEach(n => adjacency.set(n.node_id, []));
// Skip iteration edges for cycle detection
edges.filter(e => e.edge_type !== 'iteration').forEach(e => {
const neighbors = adjacency.get(e.from_node) || [];
neighbors.push(e.to_node);
adjacency.set(e.from_node, neighbors);
});
const visiting = new Set();
const visited = new Set();
function hasCycle(nodeId) {
if (visiting.has(nodeId))
return true;
if (visited.has(nodeId))
return false;
visiting.add(nodeId);
const neighbors = adjacency.get(nodeId) || [];
for (const next of neighbors) {
if (hasCycle(next))
return true;
}
visiting.delete(nodeId);
visited.add(nodeId);
return false;
}
for (const node of nodes) {
if (!visited.has(node.node_id)) {
if (hasCycle(node.node_id))
return true;
}
}
return false;
}
/**
* Find all nodes reachable from entry points
*/
function findReachableNodes(entryPoints, edges) {
const reachable = new Set(entryPoints);
const adjacency = new Map();
edges.forEach(e => {
const neighbors = adjacency.get(e.from_node) || [];
neighbors.push(e.to_node);
adjacency.set(e.from_node, neighbors);
});
function dfs(nodeId) {
const neighbors = adjacency.get(nodeId) || [];
neighbors.forEach(next => {
if (!reachable.has(next)) {
reachable.add(next);
dfs(next);
}
});
}
entryPoints.forEach(entry => dfs(entry));
return reachable;
}
/**
* Calculate critical path (longest path through graph)
*/
export function calculateCriticalPath(nodes, edges) {
if (nodes.length === 0)
return [];
const adjacency = new Map();
nodes.forEach(n => adjacency.set(n.node_id, []));
edges.forEach(e => {
const neighbors = adjacency.get(e.from_node) || [];
neighbors.push(e.to_node);
adjacency.set(e.from_node, neighbors);
});
let longestPath = [];
function dfs(nodeId, path, visited) {
if (visited.has(nodeId))
return;
visited.add(nodeId);
path.push(nodeId);
if (path.length > longestPath.length) {
longestPath = [...path];
}
const neighbors = adjacency.get(nodeId) || [];
neighbors.forEach(next => dfs(next, path, visited));
path.pop();
visited.delete(nodeId);
}
// Start from entry nodes
const entryNodes = nodes.filter(n => n.node_type === 'entry');
if (entryNodes.length === 0 && nodes.length > 0) {
dfs(nodes[0].node_id, [], new Set());
}
else {
entryNodes.forEach(n => dfs(n.node_id, [], new Set()));
}
return longestPath;
}
/**
* Find parallel execution opportunities
*/
export function findParallelOpportunities(_nodes, edges) {
const opportunities = [];
// Group edges by source node
const edgesBySource = new Map();
edges.forEach(e => {
const edgesFromNode = edgesBySource.get(e.from_node) || [];
edgesFromNode.push(e);
edgesBySource.set(e.from_node, edgesFromNode);
});
// Find nodes with multiple sequential outgoing edges that could be parallel
edgesBySource.forEach((outgoingEdges, sourceNode) => {
const sequentialEdges = outgoingEdges.filter(e => e.edge_type === 'sequential');
if (sequentialEdges.length > 1) {
// Check if target nodes are independent (no edges between them)
const targetNodes = sequentialEdges.map(e => e.to_node);
const hasInterdependency = edges.some(e => targetNodes.includes(e.from_node) && targetNodes.includes(e.to_node));
if (!hasInterdependency) {
opportunities.push({
from_node: sourceNode,
parallel_nodes: targetNodes
});
}
}
});
return opportunities;
}
/**
* Update graph metadata after modifications
*/
export function updateGraphMetadata(stored) {
const { document } = stored;
const { nodes, edges } = document.intent_graph;
// Update complexity metrics
document.metadata.complexity_metrics = calculateComplexityMetrics(nodes, edges);
// Update resource estimates
const totalDuration = nodes.reduce((sum, n) => sum + (n.metadata?.estimated_duration_ms || 0), 0);
const totalCost = nodes.reduce((sum, n) => sum + (n.metadata?.cost_estimate || 0), 0);
document.metadata.resource_estimates = {
estimated_duration_ms: totalDuration,
estimated_cost: totalCost,
estimated_tokens: Math.floor(totalCost * 1000), // Rough estimate
estimated_api_calls: nodes.filter(n => n.agent_type === 'api' || n.agent_type === 'llm').length
};
// Update execution plan
const entryNodes = nodes.filter(n => n.node_type === 'entry').map(n => n.node_id);
const exitNodes = nodes.filter(n => n.node_type === 'exit').map(n => n.node_id);
document.intent_graph.execution_plan.entry_points = entryNodes.length > 0 ? entryNodes : [];
document.intent_graph.execution_plan.exit_points = exitNodes.length > 0 ? exitNodes : [];
document.intent_graph.execution_plan.total_estimated_steps = nodes.length;
document.intent_graph.execution_plan.critical_path = calculateCriticalPath(nodes, edges);
// Find and update parallel groups
const parallelGroups = findParallelOpportunities(nodes, edges);
document.intent_graph.execution_plan.parallel_groups = parallelGroups.map((opp, idx) => ({
group_id: `parallel_group_${idx + 1}`,
nodes: opp.parallel_nodes,
execution_mode: 'all'
}));
const maxParallel = Math.max(1, ...parallelGroups.map(g => g.parallel_nodes.length));
document.intent_graph.execution_plan.max_parallel_nodes = maxParallel;
// Update validation
document.validation = validateGraph(document);
// Update timestamp
stored.updated_at = new Date();
}
//# sourceMappingURL=utils.js.map