Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
graph.handlers.ts32.1 kB
/** * @module graph.handlers * @description Consolidated graph tool handlers for MCP memory operations. * Routes operations to GraphManager methods and provides unified interface * for node, edge, batch, lock, and clear operations. */ import type { IGraphManager } from "../managers/index.js"; import type { NodeType, EdgeType, ClearType } from "../types/index.js"; import { flattenForMCP } from "./mcp/flattenForMCP.js"; import { generateConfirmationToken, validateConfirmationToken, consumeConfirmationToken } from "./confirmation.utils.js"; /** * Check if an error is a Neo4j nested map property error * * @description Neo4j doesn't support nested objects in properties. * This function detects such errors so we can auto-flatten and retry. * * @param error - Error object or message to check * @returns True if error is related to nested map properties * * @internal */ function isNestedMapError(error: any): boolean { const message = error?.message || String(error); return message.includes('Property values can only be of primitive types or arrays thereof') || message.includes('Encountered: Map{'); } /** * Handle memory_node operations - CRUD for graph nodes * * @description Provides unified interface for creating, reading, updating, * and deleting nodes in the Neo4j graph database. Supports todos, memories, * files, concepts, and custom node types. Automatically handles nested * properties by flattening them if needed. * * @param args - Operation arguments * @param args.operation - Operation type: 'add' | 'get' | 'update' | 'delete' | 'query' | 'search' * @param args.type - Node type (required for add, optional for query) * @param args.id - Node ID (required for get/update/delete) * @param args.properties - Node properties (for add/update) * @param args.filters - Query filters (for query operation) * @param args.query - Search query text (for search operation) * @param args.options - Search options like limit, offset, types * @param args.confirm - Confirmation flag for delete operations * @param args.confirmationId - Confirmation token for delete operations * @param graphManager - Graph manager instance * * @returns Promise with operation result * * @example * ```typescript * // Create a memory node * const result = await handleMemoryNode({ * operation: 'add', * type: 'memory', * properties: { * title: 'Important Decision', * content: 'We decided to use PostgreSQL for better ACID compliance' * } * }, graphManager); * // Returns: { success: true, operation: 'add', node: {...} } * ``` * * @example * ```typescript * // Query pending todos * const result = await handleMemoryNode({ * operation: 'query', * type: 'todo', * filters: { status: 'pending' } * }, graphManager); * // Returns: { success: true, operation: 'query', count: 5, nodes: [...] } * ``` * * @example * ```typescript * // Semantic search * const result = await handleMemoryNode({ * operation: 'search', * query: 'authentication implementation', * options: { limit: 10, types: ['memory', 'file'] } * }, graphManager); * // Returns: { success: true, operation: 'search', count: 3, nodes: [...] } * ``` * * @throws {Error} If operation fails or invalid arguments provided */ export async function handleMemoryNode(args: any, graphManager: IGraphManager) { const { operation } = args; switch (operation) { case 'add': { const { type, properties } = args as { type?: NodeType; properties: Record<string, any> }; try { const node = await graphManager.addNode(type, properties); return { success: true, operation: 'add', node }; } catch (error: any) { // Auto-retry with flattened properties if nested map error detected if (isNestedMapError(error) && properties) { console.warn('⚠️ Nested map detected in add operation, auto-flattening and retrying...'); const flattenedProperties = flattenForMCP(properties); const node = await graphManager.addNode(type, flattenedProperties); return { success: true, operation: 'add', node, warning: 'Properties were automatically flattened due to nested structure. Consider flattening client-side for better performance.' }; } throw error; } } case 'get': { const { id } = args as { id: string }; if (!id) { return { success: false, error: 'id is required for get operation' }; } const node = await graphManager.getNode(id); return { success: true, operation: 'get', node }; } case 'update': { const { id, properties } = args as { id: string; properties: Record<string, any> }; if (!id || !properties) { return { success: false, error: 'id and properties are required for update operation' }; } try { const node = await graphManager.updateNode(id, properties); return { success: true, operation: 'update', node }; } catch (error: any) { // Auto-retry with flattened properties if nested map error detected if (isNestedMapError(error)) { console.warn('⚠️ Nested map detected in update operation, auto-flattening and retrying...'); const flattenedProperties = flattenForMCP(properties); const node = await graphManager.updateNode(id, flattenedProperties); return { success: true, operation: 'update', node, warning: 'Properties were automatically flattened due to nested structure. Consider flattening client-side for better performance.' }; } throw error; } } case 'delete': { const { id, confirm, confirmationId } = args as { id: string; confirm?: boolean; confirmationId?: string; }; if (!id) { return { success: false, error: 'id is required for delete operation' }; } // CONFIRMATION FLOW for delete if (!confirm || !confirmationId) { // Get node details for preview const node = await graphManager.getNode(id); if (!node) { return { success: false, error: `Node not found: ${id}` }; } // Count edges that will be deleted const edges = await graphManager.getEdges(id, 'both'); const newConfirmationId = generateConfirmationToken('memory_node_delete', { id }); return { success: true, needsConfirmation: true, confirmationId: newConfirmationId, preview: { node: { id: node.id, type: node.type }, cascadeDeletedEdges: edges.length }, message: `⚠️ This will delete node '${id}' (type: ${node.type}) and ${edges.length} connected edge(s). Call memory_node with operation='delete', id='${id}', confirm=true, and confirmationId="${newConfirmationId}" to proceed.`, expiresIn: '5 minutes' }; } // Validate and execute const isValid = validateConfirmationToken(confirmationId, 'memory_node_delete', { id }); if (!isValid) { return { success: false, error: 'Invalid or expired confirmation token. Please request a new preview.' }; } consumeConfirmationToken(confirmationId); const deleted = await graphManager.deleteNode(id); return { success: true, operation: 'delete', confirmed: true, deleted }; } case 'query': { const { type, filters } = args as { type?: NodeType; filters?: Record<string, any> }; const nodes = await graphManager.queryNodes(type, filters); return { success: true, operation: 'query', count: nodes.length, nodes }; } case 'search': { const { query, options } = args as { query: string; options?: any }; if (!query) { return { success: false, error: 'query is required for search operation' }; } const nodes = await graphManager.searchNodes(query, options); return { success: true, operation: 'search', count: nodes.length, nodes }; } default: return { success: false, error: `Unknown operation: ${operation}. Valid operations: add, get, update, delete, query, search` }; } } /** * Handle memory_edge operations - Manage relationships between nodes * * @description Build knowledge graphs by creating, deleting, and querying * relationships between nodes. Supports operations like finding neighbors, * extracting subgraphs, and traversing the graph structure. * * @param args - Operation arguments * @param args.operation - Operation type: 'add' | 'delete' | 'get' | 'neighbors' | 'subgraph' * @param args.source - Source node ID (for add operation) * @param args.target - Target node ID (for add operation) * @param args.type - Edge type like 'depends_on', 'part_of', 'related_to' * @param args.properties - Optional edge properties * @param args.edge_id - Edge ID (for delete operation) * @param args.node_id - Node ID (for get/neighbors/subgraph operations) * @param args.direction - Edge direction: 'in' | 'out' | 'both' (default: 'both') * @param args.edge_type - Filter by edge type (for neighbors operation) * @param args.depth - Traversal depth (default: 1) * @param graphManager - Graph manager instance * * @returns Promise with operation result * * @example * ```typescript * // Create a relationship * const result = await handleMemoryEdge({ * operation: 'add', * source: 'todo-1', * target: 'project-2', * type: 'part_of' * }, graphManager); * ``` * * @example * ```typescript * // Find all neighbors * const result = await handleMemoryEdge({ * operation: 'neighbors', * node_id: 'todo-1', * edge_type: 'depends_on', * depth: 2 * }, graphManager); * ``` * * @example * ```typescript * // Extract subgraph * const result = await handleMemoryEdge({ * operation: 'subgraph', * node_id: 'project-1', * depth: 3 * }, graphManager); * // Returns: { success: true, subgraph: { nodes: [...], edges: [...] } } * ``` */ export async function handleMemoryEdge(args: any, graphManager: IGraphManager) { const { operation } = args; switch (operation) { case 'add': { const { source, target, type, properties } = args as { source: string; target: string; type: EdgeType; properties?: Record<string, any>; }; if (!source || !target || !type) { return { success: false, error: 'source, target, and type are required for add operation' }; } try { const edge = await graphManager.addEdge(source, target, type, properties); return { success: true, operation: 'add', edge }; } catch (error: any) { // Auto-retry with flattened properties if nested map error detected if (isNestedMapError(error) && properties) { console.warn('⚠️ Nested map detected in edge properties, auto-flattening and retrying...'); const flattenedProperties = flattenForMCP(properties); const edge = await graphManager.addEdge(source, target, type, flattenedProperties); return { success: true, operation: 'add', edge, warning: 'Edge properties were automatically flattened due to nested structure. Consider flattening client-side for better performance.' }; } throw error; } } case 'delete': { const { edge_id } = args as { edge_id: string }; if (!edge_id) { return { success: false, error: 'edge_id is required for delete operation' }; } const deleted = await graphManager.deleteEdge(edge_id); return { success: true, operation: 'delete', deleted }; } case 'get': { const { node_id, direction } = args as { node_id: string; direction?: 'in' | 'out' | 'both' }; if (!node_id) { return { success: false, error: 'node_id is required for get operation' }; } const edges = await graphManager.getEdges(node_id, direction); return { success: true, operation: 'get', count: edges.length, edges }; } case 'neighbors': { const { node_id, edge_type, depth } = args as { node_id: string; edge_type?: EdgeType; depth?: number }; if (!node_id) { return { success: false, error: 'node_id is required for neighbors operation' }; } const neighbors = await graphManager.getNeighbors(node_id, edge_type, depth); return { success: true, operation: 'neighbors', count: neighbors.length, neighbors }; } case 'subgraph': { const { node_id, depth } = args as { node_id: string; depth?: number }; if (!node_id) { return { success: false, error: 'node_id is required for subgraph operation' }; } const subgraph = await graphManager.getSubgraph(node_id, depth); return { success: true, operation: 'subgraph', subgraph }; } default: return { success: false, error: `Unknown operation: ${operation}. Valid operations: add, delete, get, neighbors, subgraph` }; } } /** * Handle memory_batch operations - Bulk operations for nodes and edges * * @description Perform bulk operations on multiple nodes or edges at once * for better performance. Supports batch add, update, and delete operations * with automatic property flattening and confirmation flows for destructive operations. * * @param args - Operation arguments * @param args.operation - Operation type: 'add_nodes' | 'update_nodes' | 'delete_nodes' | 'add_edges' | 'delete_edges' * @param args.nodes - Array of nodes to create (for add_nodes) * @param args.updates - Array of node updates with id and properties (for update_nodes) * @param args.ids - Array of node/edge IDs to delete (for delete operations) * @param args.edges - Array of edges to create (for add_edges) * @param args.confirm - Confirmation flag for delete operations * @param args.confirmationId - Confirmation token for delete operations * @param graphManager - Graph manager instance * * @returns Promise with operation result * * @example * ```typescript * // Batch create nodes * const result = await handleMemoryBatch({ * operation: 'add_nodes', * nodes: [ * { type: 'todo', properties: { title: 'Task 1' } }, * { type: 'todo', properties: { title: 'Task 2' } } * ] * }, graphManager); * // Returns: { success: true, count: 2, nodes: [...] } * ``` * * @example * ```typescript * // Batch update nodes * const result = await handleMemoryBatch({ * operation: 'update_nodes', * updates: [ * { id: 'todo-1', properties: { status: 'completed' } }, * { id: 'todo-2', properties: { status: 'completed' } } * ] * }, graphManager); * ``` * * @example * ```typescript * // Batch create edges * const result = await handleMemoryBatch({ * operation: 'add_edges', * edges: [ * { source: 'todo-1', target: 'project-1', type: 'part_of' }, * { source: 'todo-2', target: 'project-1', type: 'part_of' } * ] * }, graphManager); * ``` */ export async function handleMemoryBatch(args: any, graphManager: IGraphManager) { const { operation } = args; switch (operation) { case 'add_nodes': { const { nodes } = args as { nodes: Array<{ type: NodeType; properties: Record<string, any> }> }; if (!nodes || !Array.isArray(nodes)) { return { success: false, error: 'nodes array is required for add_nodes operation' }; } try { const created = await graphManager.addNodes(nodes); return { success: true, operation: 'add_nodes', count: created.length, nodes: created }; } catch (error: any) { // Auto-retry with flattened properties if nested map error detected if (isNestedMapError(error)) { console.warn('⚠️ Nested map detected in batch add_nodes, auto-flattening and retrying...'); const flattenedNodes = nodes.map(node => ({ ...node, properties: flattenForMCP(node.properties) })); const created = await graphManager.addNodes(flattenedNodes); return { success: true, operation: 'add_nodes', count: created.length, nodes: created, warning: 'Node properties were automatically flattened due to nested structure. Consider flattening client-side for better performance.' }; } throw error; } } case 'update_nodes': { const { updates } = args as { updates: Array<{ id: string; properties: Record<string, any> }> }; if (!updates || !Array.isArray(updates)) { return { success: false, error: 'updates array is required for update_nodes operation' }; } try { const updated = await graphManager.updateNodes(updates); return { success: true, operation: 'update_nodes', count: updated.length, nodes: updated }; } catch (error: any) { // Auto-retry with flattened properties if nested map error detected if (isNestedMapError(error)) { console.warn('⚠️ Nested map detected in batch update_nodes, auto-flattening and retrying...'); const flattenedUpdates = updates.map(update => ({ ...update, properties: flattenForMCP(update.properties) })); const updated = await graphManager.updateNodes(flattenedUpdates); return { success: true, operation: 'update_nodes', count: updated.length, nodes: updated, warning: 'Node properties were automatically flattened due to nested structure. Consider flattening client-side for better performance.' }; } throw error; } } case 'delete_nodes': { const { ids, confirm, confirmationId } = args as { ids: string[]; confirm?: boolean; confirmationId?: string; }; if (!ids || !Array.isArray(ids)) { return { success: false, error: 'ids array is required for delete_nodes operation' }; } // CONFIRMATION FLOW for batch delete if (!confirm || !confirmationId) { const newConfirmationId = generateConfirmationToken('memory_batch_delete_nodes', { ids }); return { success: true, needsConfirmation: true, confirmationId: newConfirmationId, preview: { nodeCount: ids.length, nodeIds: ids.slice(0, 10), // Show first 10 more: ids.length > 10 ? ids.length - 10 : 0 }, message: `⚠️ This will delete ${ids.length} node(s) and their connected edges. Call memory_batch with operation='delete_nodes', ids=[...], confirm=true, and confirmationId="${newConfirmationId}" to proceed.`, expiresIn: '5 minutes' }; } // Validate and execute const isValid = validateConfirmationToken(confirmationId, 'memory_batch_delete_nodes', { ids }); if (!isValid) { return { success: false, error: 'Invalid or expired confirmation token. Please request a new preview.' }; } consumeConfirmationToken(confirmationId); const result = await graphManager.deleteNodes(ids); return { success: true, operation: 'delete_nodes', confirmed: true, result }; } case 'add_edges': { const { edges } = args as { edges: Array<{ source: string; target: string; type: EdgeType; properties?: Record<string, any> }> }; if (!edges || !Array.isArray(edges)) { return { success: false, error: 'edges array is required for add_edges operation' }; } try { const created = await graphManager.addEdges(edges); return { success: true, operation: 'add_edges', count: created.length, edges: created }; } catch (error: any) { // Auto-retry with flattened properties if nested map error detected if (isNestedMapError(error)) { console.warn('⚠️ Nested map detected in batch add_edges, auto-flattening and retrying...'); const flattenedEdges = edges.map(edge => ({ ...edge, properties: edge.properties ? flattenForMCP(edge.properties) : undefined })); const created = await graphManager.addEdges(flattenedEdges); return { success: true, operation: 'add_edges', count: created.length, edges: created, warning: 'Edge properties were automatically flattened due to nested structure. Consider flattening client-side for better performance.' }; } throw error; } } case 'delete_edges': { const { ids, confirm, confirmationId } = args as { ids: string[]; confirm?: boolean; confirmationId?: string; }; if (!ids || !Array.isArray(ids)) { return { success: false, error: 'ids array is required for delete_edges operation' }; } // CONFIRMATION FLOW for batch delete edges if (!confirm || !confirmationId) { const newConfirmationId = generateConfirmationToken('memory_batch_delete_edges', { ids }); return { success: true, needsConfirmation: true, confirmationId: newConfirmationId, preview: { edgeCount: ids.length, edgeIds: ids.slice(0, 10), more: ids.length > 10 ? ids.length - 10 : 0 }, message: `⚠️ This will delete ${ids.length} edge(s). Call memory_batch with operation='delete_edges', ids=[...], confirm=true, and confirmationId="${newConfirmationId}" to proceed.`, expiresIn: '5 minutes' }; } // Validate and execute const isValid = validateConfirmationToken(confirmationId, 'memory_batch_delete_edges', { ids }); if (!isValid) { return { success: false, error: 'Invalid or expired confirmation token. Please request a new preview.' }; } consumeConfirmationToken(confirmationId); const result = await graphManager.deleteEdges(ids); return { success: true, operation: 'delete_edges', confirmed: true, result }; } default: return { success: false, error: `Unknown operation: ${operation}. Valid operations: add_nodes, update_nodes, delete_nodes, add_edges, delete_edges` }; } } /** * Handle memory_lock operations - Multi-agent locking for concurrent access * * @description Provides optimistic locking mechanism for multi-agent scenarios. * Prevents race conditions when multiple agents try to modify the same node. * Locks automatically expire after timeout to prevent deadlocks. * * @param args - Operation arguments * @param args.operation - Operation type: 'acquire' | 'release' | 'query_available' | 'cleanup' * @param args.node_id - Node ID to lock/unlock * @param args.agent_id - Agent identifier (e.g., 'pm-agent', 'worker-1') * @param args.timeout_ms - Lock timeout in milliseconds (default: 300000 = 5 minutes) * @param args.type - Node type filter (for query_available) * @param args.filters - Property filters (for query_available) * @param graphManager - Graph manager instance * * @returns Promise with operation result * * @example * ```typescript * // Acquire lock * const result = await handleMemoryLock({ * operation: 'acquire', * node_id: 'todo-1', * agent_id: 'worker-agent-1', * timeout_ms: 300000 * }, graphManager); * // Returns: { success: true, locked: true, message: '...' } * ``` * * @example * ```typescript * // Query available (unlocked) nodes * const result = await handleMemoryLock({ * operation: 'query_available', * type: 'todo', * filters: { status: 'pending' } * }, graphManager); * // Returns: { success: true, count: 5, nodes: [...] } * ``` * * @example * ```typescript * // Release lock * const result = await handleMemoryLock({ * operation: 'release', * node_id: 'todo-1', * agent_id: 'worker-agent-1' * }, graphManager); * ``` */ export async function handleMemoryLock(args: any, graphManager: IGraphManager) { const { operation } = args; switch (operation) { case 'acquire': { const { node_id, agent_id, timeout_ms } = args as { node_id: string; agent_id: string; timeout_ms?: number }; if (!node_id || !agent_id) { return { success: false, error: 'node_id and agent_id are required for acquire operation' }; } const locked = await graphManager.lockNode(node_id, agent_id, timeout_ms); return { success: true, operation: 'acquire', locked, message: locked ? `Lock acquired by ${agent_id} on ${node_id}` : `Node ${node_id} is already locked by another agent` }; } case 'release': { const { node_id, agent_id } = args as { node_id: string; agent_id: string }; if (!node_id || !agent_id) { return { success: false, error: 'node_id and agent_id are required for release operation' }; } const unlocked = await graphManager.unlockNode(node_id, agent_id); return { success: true, operation: 'release', unlocked, message: unlocked ? `Lock released by ${agent_id} on ${node_id}` : `Node ${node_id} was not locked by ${agent_id}` }; } case 'query_available': { const { type, filters } = args as { type?: NodeType; filters?: Record<string, any>; }; const nodes = await graphManager.queryNodesWithLockStatus(type, filters, true); return { success: true, operation: 'query_available', count: nodes.length, nodes }; } case 'cleanup': { const cleaned = await graphManager.cleanupExpiredLocks(); return { success: true, operation: 'cleanup', cleaned, message: `Cleaned up ${cleaned} expired lock(s)` }; } default: return { success: false, error: `Unknown operation: ${operation}. Valid operations: acquire, release, query_available, cleanup` }; } } // ============================================================================ // memory_clear handler - Dangerous operation with confirmation flow // ============================================================================ /** * Handle memory_clear operations - Clear data from graph database * * @description Provides safe deletion of data by type or complete database clear. * Includes confirmation flow for destructive operations. Use with caution! * * @param args - Operation arguments * @param args.type - Clear type: 'ALL' | 'todo' | 'memory' | 'file' | etc. * @param args.confirm - Confirmation flag (required for execution) * @param args.confirmationId - Confirmation token from preview request * @param graphManager - Graph manager instance * * @returns Promise with operation result * * @example * ```typescript * // Request preview for clearing all todos * const preview = await handleMemoryClear({ * type: 'todo' * }, graphManager); * // Returns: { needsConfirmation: true, confirmationId: '...', preview: {...} } * * // Execute with confirmation * const result = await handleMemoryClear({ * type: 'todo', * confirm: true, * confirmationId: preview.confirmationId * }, graphManager); * // Returns: { success: true, confirmed: true, deleted: 42 } * ``` * * @example * ```typescript * // Clear entire database (use with extreme caution!) * const preview = await handleMemoryClear({ * type: 'ALL' * }, graphManager); * * const result = await handleMemoryClear({ * type: 'ALL', * confirm: true, * confirmationId: preview.confirmationId * }, graphManager); * ``` * * @throws {Error} If operation fails or invalid confirmation */ export async function handleMemoryClear(args: any, graphManager: IGraphManager) { const { type, confirm, confirmationId } = args as { type?: ClearType; confirm?: boolean; confirmationId?: string; }; if (!type) { return { success: false, error: "type is required. Use type='ALL' to clear entire graph or specify a node type." }; } // Safety guard: prevent accidental clearing of the real database during tests. const isTestEnv = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true' || !!(globalThis as any).vitest; // If the graphManager appears to be a mock (mock returns null driver), allow; otherwise block 'ALL' clears in test env. let isMockManager = false; try { // Some managers return null for getDriver in mocks const driver = (graphManager as any).getDriver && (graphManager as any).getDriver(); if (driver === null) isMockManager = true; } catch (e) { // If getDriver throws or is absent, treat as non-mock conservatively isMockManager = false; } if (isTestEnv && type === 'ALL' && !isMockManager) { return { success: false, error: "Refusing to clear the entire database during tests when using a real GraphManager. Use the mock GraphManager in tests or set type to a specific node type." }; } // CONFIRMATION FLOW: Step 1 - Generate preview if not confirmed if (!confirm || !confirmationId) { // Get current stats to show what would be deleted const stats = await graphManager.getStats(); let preview: { deletedNodes: number; deletedEdges: number; types?: Record<string, number> }; if (type === 'ALL') { preview = { deletedNodes: stats.nodeCount, deletedEdges: stats.edgeCount, types: stats.types }; } else { // Count nodes of specific type const nodes = await graphManager.queryNodes(type); preview = { deletedNodes: nodes.length, deletedEdges: 0, // Edges will be cascade deleted types: { [type]: nodes.length } }; } // Generate confirmation token const newConfirmationId = generateConfirmationToken('memory_clear', { type }); return { success: true, needsConfirmation: true, confirmationId: newConfirmationId, preview, message: type === 'ALL' ? `⚠️ This will delete ALL ${preview.deletedNodes} nodes and ${preview.deletedEdges} edges. Call memory_clear again with confirm=true and confirmationId="${newConfirmationId}" to proceed.` : `⚠️ This will delete ${preview.deletedNodes} nodes of type '${type}'. Call memory_clear again with confirm=true and confirmationId="${newConfirmationId}" to proceed.`, expiresIn: '5 minutes' }; } // CONFIRMATION FLOW: Step 2 - Validate and execute if confirmed if (confirm && confirmationId) { // Validate confirmation token const isValid = validateConfirmationToken(confirmationId, 'memory_clear', { type }); if (!isValid) { return { success: false, error: 'Invalid or expired confirmation token. Please request a new preview by calling memory_clear without confirm=true.' }; } // Consume the token (one-time use) consumeConfirmationToken(confirmationId); // Execute the clear operation const result = await graphManager.clear(type); return { success: true, confirmed: true, ...result, message: type === 'ALL' ? `✅ Cleared ALL data: ${result.deletedNodes} nodes, ${result.deletedEdges} edges` : `✅ Cleared ${result.deletedNodes} nodes of type '${type}' and ${result.deletedEdges} edges` }; } // Should never reach here return { success: false, error: 'Invalid confirmation flow state.' }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server