Skip to main content
Glama

DevFlow MCP

by Takin-Profit
call-tool-handler.ts24.4 kB
/** * Call Tool Handler - MCP Protocol Request Router * * Routes CallTool requests to appropriate tool handlers. * All handlers use Zod validation and standardized responses. */ import type { KnowledgeGraphManager } from "#knowledge-graph-manager" import { handleAddObservations, handleCreateEntities, handleCreateRelations, handleDeleteEntities, handleReadGraph, } from "#server/tool-handlers" import type { Entity, Logger, MCPToolResponse, TemporalEntityType, } from "#types" import { DEFAULT_MIN_SIMILARITY, DEFAULT_SEARCH_LIMIT, DeleteObservationsInputSchema, DeleteRelationsInputSchema, GetEntityEmbeddingInputSchema, GetEntityHistoryInputSchema, GetGraphAtTimeInputSchema, GetRelationHistoryInputSchema, GetRelationInputSchema, OpenNodesInputSchema, SearchNodesInputSchema, SemanticSearchInputSchema, UpdateRelationInputSchema, } from "#types" import { buildErrorResponse, buildSuccessResponse, buildValidationErrorResponse, handleError, } from "#utils" // ============================================================================ // Constants // ============================================================================ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i // ============================================================================ // Main Handler // ============================================================================ /** * Handles the CallTool request. * Delegates to the appropriate tool handler based on the tool name. * * @param request The CallTool request object * @param knowledgeGraphManager The KnowledgeGraphManager instance * @param logger Logger instance for structured logging * @returns A response object with the result content * @throws Error if the tool is unknown or arguments are missing */ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: This is a dispatcher function that routes to different tool handlers export async function handleCallToolRequest( request: { params?: { name?: string; arguments?: Record<string, unknown> } }, knowledgeGraphManager: KnowledgeGraphManager, logger: Logger ): Promise<MCPToolResponse> { if (!request) { return buildErrorResponse("Invalid request: request is null or undefined") } if (!request.params) { return buildErrorResponse("Invalid request: missing params") } const { name, arguments: args } = request.params if (!name) { return buildErrorResponse("Invalid request: missing tool name") } if (!args) { return buildErrorResponse(`No arguments provided for tool: ${name}`) } switch (name) { // Delegate to updated tool handlers (tool-handlers.ts) case "create_entities": return await handleCreateEntities(args, knowledgeGraphManager, logger) case "read_graph": return await handleReadGraph(args, knowledgeGraphManager, logger) case "create_relations": return await handleCreateRelations(args, knowledgeGraphManager, logger) case "add_observations": return await handleAddObservations(args, knowledgeGraphManager, logger) case "delete_entities": return await handleDeleteEntities(args, knowledgeGraphManager, logger) case "delete_observations": { try { const result = DeleteObservationsInputSchema.safeParse(args) if (!result.success) { logger.warn("delete_observations validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { deletions } = result.data logger.debug("delete_observations called", { deletionCount: deletions.length, }) await knowledgeGraphManager.deleteObservations(deletions) logger.info("delete_observations completed", { deleted: deletions.length, }) const totalDeleted = deletions.reduce( (sum, d) => sum + d.observations.length, 0 ) return buildSuccessResponse({ deleted: totalDeleted, entities: deletions.map((d) => ({ entityName: d.entityName, deletedCount: d.observations.length, })), }) } catch (error) { return handleError(error, logger) } } case "delete_relations": { try { const result = DeleteRelationsInputSchema.safeParse(args) if (!result.success) { logger.warn("delete_relations validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { relations } = result.data logger.debug("delete_relations called", { relationCount: relations.length, }) await knowledgeGraphManager.deleteRelations(relations) logger.info("delete_relations completed", { deleted: relations.length, }) return buildSuccessResponse({ deleted: relations.length, }) } catch (error) { return handleError(error, logger) } } case "get_relation": { try { const result = GetRelationInputSchema.safeParse(args) if (!result.success) { logger.warn("get_relation validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { from, to, relationType } = result.data logger.debug("get_relation called", { from, to, relationType }) const relation = await knowledgeGraphManager.getRelation( from as string, to as string, relationType ) logger.info("get_relation completed", { found: !!relation }) return buildSuccessResponse(relation) } catch (error) { return handleError(error, logger) } } case "update_relation": { try { const result = UpdateRelationInputSchema.safeParse(args) if (!result.success) { logger.warn("update_relation validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const relation = result.data logger.debug("update_relation called", { from: relation.from, to: relation.to, type: relation.relationType, }) const updated = await knowledgeGraphManager.updateRelation(relation) logger.info("update_relation completed") return buildSuccessResponse(updated) } catch (error) { return handleError(error, logger) } } case "search_nodes": { try { const result = SearchNodesInputSchema.safeParse(args) if (!result.success) { logger.warn("search_nodes validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { query } = result.data logger.debug("search_nodes called", { query }) const results = await knowledgeGraphManager.searchNodes(query) logger.info("search_nodes completed", { count: results.entities.length, }) return buildSuccessResponse({ results, count: results.entities.length, }) } catch (error) { return handleError(error, logger) } } case "open_nodes": { try { const result = OpenNodesInputSchema.safeParse(args) if (!result.success) { logger.warn("open_nodes validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { names } = result.data logger.debug("open_nodes called", { count: names.length }) const openResult = await knowledgeGraphManager.openNodes( names.map((n) => n as string) ) logger.info("open_nodes completed", { found: openResult.entities?.length || 0, }) return buildSuccessResponse({ entities: openResult.entities || [], relations: openResult.relations || [], found: openResult.entities?.length || 0, }) } catch (error) { return handleError(error, logger) } } case "get_entity_history": { try { const result = GetEntityHistoryInputSchema.safeParse(args) if (!result.success) { logger.warn("get_entity_history validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { entityName } = result.data logger.debug("get_entity_history called", { entityName }) const history = await knowledgeGraphManager.getEntityHistory( entityName as string ) logger.info("get_entity_history completed", { versionCount: history.length, }) return buildSuccessResponse({ entityName, history, totalVersions: history.length, }) } catch (error) { return handleError(error, logger) } } case "get_relation_history": { try { const result = GetRelationHistoryInputSchema.safeParse(args) if (!result.success) { logger.warn("get_relation_history validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { from, to, relationType } = result.data logger.debug("get_relation_history called", { from, to, relationType }) const history = await knowledgeGraphManager.getRelationHistory( from as string, to as string, relationType ) logger.info("get_relation_history completed", { versionCount: history.length, }) return buildSuccessResponse({ from, to, relationType, history, totalVersions: history.length, }) } catch (error) { return handleError(error, logger) } } case "get_graph_at_time": { try { const result = GetGraphAtTimeInputSchema.safeParse(args) if (!result.success) { logger.warn("get_graph_at_time validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { timestamp } = result.data logger.debug("get_graph_at_time called", { timestamp }) const graph = await knowledgeGraphManager.getGraphAtTime( timestamp as number ) logger.info("get_graph_at_time completed", { entityCount: graph.entities.length, relationCount: graph.relations.length, }) return buildSuccessResponse({ timestamp, graph, }) } catch (error) { return handleError(error, logger) } } case "get_decayed_graph": { try { logger.debug("get_decayed_graph called") // No validation needed - no arguments const graph = await knowledgeGraphManager.getDecayedGraph() logger.info("get_decayed_graph completed", { entityCount: graph.entities.length, relationCount: graph.relations.length, }) return buildSuccessResponse(graph) } catch (error) { return handleError(error, logger) } } case "force_generate_embedding": { try { const result = GetEntityEmbeddingInputSchema.safeParse(args) if (!result.success) { logger.warn("force_generate_embedding validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { entityName } = result.data const entityNameStr = entityName as string logger.debug("Force generating embedding for entity", { entityName: entityNameStr, }) // Determine if the input looks like a UUID const isUUID = UUID_PATTERN.test(entityNameStr) if (isUUID) { logger.debug("Input appears to be a UUID", { entityName: entityNameStr, }) } // Try to get all entities first to locate the correct one logger.debug("Trying to find entity by opening all nodes") const allEntities = await knowledgeGraphManager.openNodes([]) let entity: Entity | TemporalEntityType | null = null if (allEntities?.entities && allEntities.entities.length > 0) { logger.debug("Found entities in total", { count: allEntities.entities.length, }) // Try different methods to find the entity // 1. Direct match by name entity = allEntities.entities.find( (e: Entity) => e.name === entityNameStr ) ?? null // 2. If not found and input is UUID, try matching by ID if (!entity && isUUID) { entity = allEntities.entities.find( (e: Entity & { id?: string }) => // The id property might not be in the Entity interface, but could exist at runtime "id" in e && e.id === entityNameStr ) ?? null logger.debug("Searching by ID match for UUID", { uuid: entityNameStr, }) } // Log found entities to help debugging if (!entity) { logger.debug("Entity not found in list", { availableEntities: allEntities.entities.map( (e: { name: string; id?: string }) => ({ name: e.name, id: e.id, }) ), }) } } else { logger.debug("No entities found in graph") } // If still not found, try explicit lookup by name if (!entity) { logger.debug( "Entity not found in list, trying explicit lookup by name" ) const openedEntities = await knowledgeGraphManager.openNodes([ entityNameStr, ]) if (openedEntities?.entities && openedEntities.entities.length > 0) { entity = openedEntities.entities[0] ?? null logger.debug("Found entity by name", { name: entity?.name, id: (entity as Entity & { id?: string }).id || "none", }) } } // If still not found, check if we can query by ID through the database const database = knowledgeGraphManager.getDatabase() if (!entity && isUUID && database && database.getEntity) { try { logger.debug("Trying direct database lookup by ID", { entityId: entityNameStr, }) entity = await database.getEntity(entityNameStr) if (entity) { logger.debug("Found entity by direct ID lookup", { name: entity.name, id: (entity as Record<string, unknown>).id || "none", }) } } catch (err) { logger.debug("Direct ID lookup failed", { error: err, }) } } // Final check if (!entity) { logger.error("Entity not found after all lookup attempts", { entityName: entityNameStr, }) return buildErrorResponse(`Entity not found: ${entityNameStr}`) } logger.debug("Successfully found entity", { name: entity.name, id: (entity as Entity & { id?: string }).id || "none", }) // Check if embedding service and job manager are available const embeddingJobManager = knowledgeGraphManager.getEmbeddingJobManager() if (!embeddingJobManager) { logger.error("EmbeddingJobManager not initialized") return buildErrorResponse("EmbeddingJobManager not initialized") } logger.debug("EmbeddingJobManager found, proceeding") // Directly get the text for the entity // Cast to Entity since TemporalEntityType extends Entity and we've already null-checked const embeddingText = embeddingJobManager.prepareEntityText( entity as Entity ) logger.debug("Prepared entity text for embedding", { textLength: embeddingText.length, }) // Generate embedding directly const embeddingService = embeddingJobManager.getEmbeddingService() if (!embeddingService) { logger.error("Embedding service not available") return buildErrorResponse("Embedding service not available") } const vector = await embeddingService.generateEmbedding(embeddingText) logger.debug("Generated embedding vector", { vectorLength: vector.length, }) // Store the embedding with both name and ID for redundancy // Validate entity.name is a string if (typeof entity.name !== "string") { return buildErrorResponse( `Entity name must be a string, got ${typeof entity.name}` ) } logger.debug("Storing embedding for entity", { entityName: entity.name, }) // Store embedding (this functionality may need to be implemented in the database) logger.warn("Vector storage not yet implemented in this version") return buildSuccessResponse({ entityName: entity.name, embedding: vector, model: embeddingService.getModelInfo().name, }) } catch (error) { return handleError(error, logger) } } case "semantic_search": { try { const result = SemanticSearchInputSchema.safeParse(args) if (!result.success) { logger.warn("semantic_search validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { query, limit = DEFAULT_SEARCH_LIMIT, minSimilarity = DEFAULT_MIN_SIMILARITY, entityTypes = [], hybridSearch = true, } = result.data logger.debug("semantic_search called", { query, limit, minSimilarity }) // Call the search method with semantic search options const searchResults = await knowledgeGraphManager.search(query, { limit, minSimilarity, entityTypes, hybridSearch, semanticSearch: true, }) logger.info("semantic_search completed", { count: searchResults.entities.length, }) // Format results to match the expected response schema const results = searchResults.entities.map((entity: Entity) => ({ entity, similarity: 1.0, // Default similarity score })) return buildSuccessResponse({ results, count: results.length, }) } catch (error) { return handleError(error, logger) } } case "get_entity_embedding": { try { const result = GetEntityEmbeddingInputSchema.safeParse(args) if (!result.success) { logger.warn("get_entity_embedding validation failed", { issues: result.error.issues, }) return buildValidationErrorResponse(result.error) } const { entityName } = result.data const entityNameStr = entityName as string logger.debug("get_entity_embedding called", { entityName: entityNameStr, }) // Check if entity exists const entity = await knowledgeGraphManager.openNodes([entityNameStr]) if (!entity.entities || entity.entities.length === 0) { return buildErrorResponse(`Entity not found: ${entityNameStr}`) } // Access the embedding using appropriate interface const database = knowledgeGraphManager.getDatabase() if (database?.getEntityEmbedding) { const embedding = await database.getEntityEmbedding(entityNameStr) if (!embedding) { return buildErrorResponse( `No embedding found for entity: ${entityNameStr}` ) } logger.info("get_entity_embedding completed", { dimensions: embedding.vector?.length || 0, }) return buildSuccessResponse({ entityName, embedding: embedding.vector, model: embedding.model || "unknown", }) } return buildErrorResponse( "Embedding retrieval not supported by this database" ) } catch (error) { return handleError(error, logger) } } case "debug_embedding_config": { try { // Diagnostic tool to check embedding configuration // Check for OpenAI API key const hasOpenAIKey = !!process.env.DFM_OPENAI_API_KEY const embeddingModel = process.env.DFM_OPENAI_EMBEDDING_MODEL || "text-embedding-3-small" // Check if embedding job manager is initialized const embeddingJobManager = knowledgeGraphManager.getEmbeddingJobManager() const hasEmbeddingJobManager = !!embeddingJobManager // Get database info const storageType = "sqlite" // SQLite-only configuration const sqliteInfo = { location: process.env.DFM_SQLITE_LOCATION || ":memory:", connectionStatus: "available", vectorStoreStatus: "sqlite-vec enabled", } // Count entities with embeddings const entitiesWithEmbeddings = 0 // This functionality would need to be implemented logger.debug("Embedding count not yet implemented") // Get embedding service information let embeddingServiceInfo: Record<string, unknown> | null = null if (hasEmbeddingJobManager && embeddingJobManager) { try { const embeddingService = embeddingJobManager.getEmbeddingService() if (embeddingService) { embeddingServiceInfo = embeddingService.getModelInfo() as unknown as Record< string, unknown > } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) logger.error("Error getting embedding service info", { error: errorMessage, }) } } const embeddingProviderInfo = embeddingServiceInfo const pendingJobs = 0 // Not available through public API // Return diagnostic information with proper formatting const diagnosticInfo = { storage_type: storageType, openai_api_key_present: hasOpenAIKey, embedding_model: embeddingModel, embedding_job_manager_initialized: hasEmbeddingJobManager, embedding_service_initialized: !!embeddingProviderInfo, embedding_service_info: embeddingServiceInfo, embedding_provider_info: embeddingProviderInfo, sqlite_config: sqliteInfo, entities_with_embeddings: entitiesWithEmbeddings, pending_embedding_jobs: pendingJobs, environment_variables: { DEBUG: process.env.DEBUG === "true", DFM_ENV: process.env.DFM_ENV, MEMORY_STORAGE_TYPE: "sqlite", }, } return buildSuccessResponse(diagnosticInfo) } catch (error) { return handleError(error, logger) } } case "diagnose_vector_search": { try { const database = knowledgeGraphManager.getDatabase() if (database?.diagnoseVectorSearch) { const diagnostics = await database.diagnoseVectorSearch() return buildSuccessResponse(diagnostics) } return buildErrorResponse("Diagnostic method not available") } catch (error) { return handleError(error, logger) } } default: return buildErrorResponse(`Unknown tool: ${name}`) } }

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/Takin-Profit/devflow-mcp'

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