UNPKG

knowledgegraph-mcp

Version:

MCP server for enabling persistent knowledge storage for Claude through a knowledge graph with multiple storage backends

552 lines 31.4 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { KnowledgeGraphManager } from './core.js'; // Re-export types and classes for external use export { KnowledgeGraphManager }; // Initialize the knowledge graph manager let knowledgeGraphManager; // The server instance and tools exposed to Claude const server = new Server({ name: "knowledgegraph-server", version: "0.0.1", }, { capabilities: { tools: {}, }, }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "search_knowledge", description: "SEARCH ENTITIES by TEXT or TAGS across names, types, observations, and tags. Supports exact/fuzzy search modes, multiple query batching, tag filtering, and pagination. Returns entities and their relationships. MUST USE BEFORE create_entities, add_observations, and create_relations to verify entity existence. PAGINATION: Use page parameter to navigate large result sets (page=0 for first page, page=1 for second, etc.).", inputSchema: { type: "object", properties: { query: { oneOf: [ { type: "string" }, { type: "array", items: { type: "string" } } ], description: "Search text (string or array of strings for batch search). Searches across entity names, types, observations, and tags. Optional when exactTags is provided." }, exactTags: { type: "array", items: { type: "string" }, description: "Array of tags for exact-match filtering (case-sensitive). Enables tag-only search when query is omitted." }, tagMatchMode: { type: "string", enum: ["any", "all"], description: "Tag matching mode: 'any' finds entities with ANY specified tag, 'all' requires ALL tags (default: 'any')" }, searchMode: { type: "string", enum: ["exact", "fuzzy"], description: "Search algorithm: 'exact' for substring matching (fast), 'fuzzy' for similarity matching (slower, broader results). Default: 'exact'" }, fuzzyThreshold: { type: "number", minimum: 0.0, maximum: 1.0, description: "Similarity threshold for fuzzy search (0.0-1.0). Lower values = more results. Default: 0.3" }, page: { type: "number", minimum: 0, description: "Page number for pagination (0-based). Use 0 or omit for first page." }, pageSize: { type: "number", minimum: 1, maximum: 1000, description: "Number of results per page (default: 100, max: 1000)" }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: [], }, }, { name: "create_entities", description: "CREATE new entities with OBSERVATIONS and optional tags. MANDATORY: If creating multiple entities, use a SINGLE call (batch). Each entity requires a unique name, type, and at least one observation. Ignores existing names. Use search_knowledge first.", inputSchema: { type: "object", properties: { entities: { type: "array", description: "Array of entity objects to create. Each entity must have at least one observation.", items: { type: "object", properties: { name: { type: "string", description: "Unique entity identifier. Use descriptive names (e.g., 'John_Smith_Engineer', 'React_v18')" }, entityType: { type: "string", description: "Entity category. Valid types: 'person', 'technology', 'project', 'company', 'concept', 'event', 'preference'" }, observations: { type: "array", items: { type: "string" }, description: "Array of factual statements about the entity. Must contain at least one non-empty string." }, tags: { type: "array", items: { type: "string" }, description: "Optional tags for categorization and filtering (e.g., ['urgent', 'technical', 'completed'])" }, }, required: ["name", "entityType", "observations"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["entities"], }, }, { name: "add_observations", description: "ADD new factual observations to existing entities. Requires exact entity names that exist in the knowledge graph. Use search_knowledge first to verify entity existence.", inputSchema: { type: "object", properties: { observations: { type: "array", description: "Array of observation updates. Each update specifies an entity and new observations to add.", items: { type: "object", properties: { entityName: { type: "string", description: "Exact name of existing entity to update" }, observations: { type: "array", items: { type: "string" }, description: "Array of new factual statements to add. Must contain at least one non-empty string." }, }, required: ["entityName", "observations"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["observations"], }, }, { name: "create_relations", description: "CREATE directional RELATIONSHIPS between existing entities. Both source and target entities must exist. Use active voice relationship types (e.g., 'works_at', 'manages', 'depends_on'). Verify entity existence with search_knowledge first.", inputSchema: { type: "object", properties: { relations: { type: "array", description: "Array of relationship objects to create between existing entities", items: { type: "object", properties: { from: { type: "string", description: "Source entity name (must exist in knowledge graph)" }, to: { type: "string", description: "Target entity name (must exist in knowledge graph)" }, relationType: { type: "string", description: "Relationship type in active voice (e.g., 'works_at', 'manages', 'created_by', 'depends_on', 'uses')" }, }, required: ["from", "to", "relationType"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["relations"], }, }, { name: "delete_entities", description: "Permanently DELETE entities and all their RELATIONSHIPS. This action is irreversible and cascades to remove all connections. Verify entity existence with search_knowledge first. Consider delete_observations for partial updates.", inputSchema: { type: "object", properties: { entityNames: { type: "array", items: { type: "string" }, description: "Array of exact entity names to permanently delete" }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["entityNames"], }, }, { name: "delete_observations", description: "DELETE specific OBSERVATIONS from entities while preserving the entity, its relationships, and tags. Use for correcting errors or removing outdated facts without deleting the entire entity.", inputSchema: { type: "object", properties: { deletions: { type: "array", description: "Array of deletion requests specifying which observations to remove from which entities", items: { type: "object", properties: { entityName: { type: "string", description: "Exact name of entity to update" }, observations: { type: "array", items: { type: "string" }, description: "Array of exact observation strings to remove from the entity" }, }, required: ["entityName", "observations"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["deletions"], }, }, { name: "delete_relations", description: "DELETE specific RELATIONSHIPS between entities while preserving both entities. Use for updating connection status when relationships change (job changes, project completion, etc.).", inputSchema: { type: "object", properties: { relations: { type: "array", description: "Array of specific relationships to delete", items: { type: "object", properties: { from: { type: "string", description: "Source entity name" }, to: { type: "string", description: "Target entity name" }, relationType: { type: "string", description: "Exact relationship type to remove (must match existing relation)" }, }, required: ["from", "to", "relationType"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["relations"], }, }, { name: "read_graph", description: "RETRIEVE the complete KNOWLEDGE GRAPH with all entities and relationships for a project. Returns comprehensive view of the entire network structure. Can be large for projects with many entities.", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, }, }, { name: "open_nodes", description: "RETRIEVE specific ENTITIES by exact names along with their interconnections. Returns detailed information about the specified entities and relationships between them. Requires knowing exact entity names.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" }, description: "Array of exact entity names to retrieve with their details and interconnections", }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["names"], }, }, { name: "add_tags", description: "ADD categorical TAGS to existing entities for filtering and organization. Tags are case-sensitive exact-match labels used for quick retrieval with search_knowledge. Common categories: status, priority, type, domain.", inputSchema: { type: "object", properties: { updates: { type: "array", description: "Array of tag updates specifying which tags to add to which entities", items: { type: "object", properties: { entityName: { type: "string", description: "Exact name of existing entity to tag" }, tags: { type: "array", items: { type: "string" }, description: "Array of tags to add (case-sensitive, exact-match labels)" } }, required: ["entityName", "tags"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["updates"], }, }, { name: "remove_tags", description: "REMOVE specific TAGS from existing entities to maintain accurate categorization. Use for status updates, priority changes, or cleaning up outdated classifications. Tags must match exactly (case-sensitive).", inputSchema: { type: "object", properties: { updates: { type: "array", description: "Array of tag removal requests specifying which tags to remove from which entities", items: { type: "object", properties: { entityName: { type: "string", description: "Exact name of entity to update" }, tags: { type: "array", items: { type: "string" }, description: "Array of exact tags to remove (case-sensitive, must match existing tags)" } }, required: ["entityName", "tags"], }, }, project_id: { type: "string", description: "Project identifier for data isolation. Must be consistent across all calls in a conversation", pattern: "^[a-zA-Z0-9_-]+$" } }, required: ["updates"], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { throw new Error(`No arguments provided for tool: ${name}`); } // Extract project parameter from args if present const project = args.project_id; switch (name) { case "search_knowledge": { // Check if exactTags is provided for tag-only search const hasExactTags = args.exactTags && Array.isArray(args.exactTags) && args.exactTags.length > 0; // Handle multiple queries - allow empty/undefined query if exactTags is provided let queries; if (args.query === undefined || args.query === null) { if (hasExactTags) { queries = [""]; // Use empty string for tag-only search } else { return { content: [{ type: "text", text: "❌ ERROR: Query parameter is required when exactTags is not provided" }] }; } } else { queries = Array.isArray(args.query) ? args.query : [args.query]; } // Validate queries - allow empty strings only when exactTags is provided if (queries.length === 0 || queries.some((q) => typeof q !== 'string' || (!hasExactTags && q.trim() === ''))) { return { content: [{ type: "text", text: "❌ ERROR: Query must be a non-empty string or array of non-empty strings when exactTags is not provided" }] }; } // Validate pagination parameters const page = args.page !== undefined ? args.page : 0; const pageSize = args.pageSize !== undefined ? args.pageSize : 100; if (typeof page !== 'number' || page < 0 || !Number.isInteger(page)) { return { content: [{ type: "text", text: "❌ ERROR: Page must be a non-negative integer (0, 1, 2, ...)" }] }; } if (typeof pageSize !== 'number' || pageSize < 1 || pageSize > 1000 || !Number.isInteger(pageSize)) { return { content: [{ type: "text", text: "❌ ERROR: PageSize must be an integer between 1 and 1000" }] }; } // Set up pagination options const paginationOptions = { page, pageSize }; let searchType = ""; const isMultipleQueries = queries.length > 1; // Determine search options and type let searchOptions = {}; if (args.exactTags && Array.isArray(args.exactTags) && args.exactTags.length > 0) { // Exact tag search mode searchOptions = { exactTags: args.exactTags, tagMatchMode: args.tagMatchMode || 'any' }; searchType = isMultipleQueries ? `tag search (${args.exactTags.join(', ')}) for ${queries.length} queries` : `tag search (${args.exactTags.join(', ')})`; } else if (args.searchMode === 'fuzzy') { // Fuzzy search mode searchOptions = { searchMode: 'fuzzy', fuzzyThreshold: args.fuzzyThreshold || 0.3 }; searchType = isMultipleQueries ? `fuzzy search for ${queries.length} queries` : `fuzzy search "${queries[0]}"`; } else { // General text search mode (exact) searchType = isMultipleQueries ? `exact search for ${queries.length} queries` : `exact search "${queries[0]}"`; } // Use paginated search const paginatedResult = await knowledgeGraphManager.searchNodesPaginated(queries.length === 1 ? queries[0] : queries, paginationOptions, searchOptions, project); const entityCount = paginatedResult.entities.length; const relationCount = paginatedResult.relations.length; const { pagination } = paginatedResult; let successMsg = `🔍 SEARCH RESULTS: Found ${entityCount} entities, ${relationCount} relations (${searchType})`; // Add pagination information successMsg += `\n📄 PAGE ${pagination.currentPage + 1} of ${pagination.totalPages} (${pagination.totalCount} total entities, ${pageSize} per page)`; if (pagination.hasNextPage) { successMsg += `\n➡️ NEXT PAGE: Use page=${pagination.currentPage + 1} to see more results`; } if (pagination.hasPreviousPage) { successMsg += `\n⬅️ PREVIOUS PAGE: Use page=${pagination.currentPage - 1} to see previous results`; } if (isMultipleQueries) { successMsg += `\n📋 QUERIES: [${queries.map((q) => `"${q}"`).join(', ')}]`; } if (pagination.totalCount === 0) { successMsg += "\n💡 TIP: Try fuzzy search or check spelling. Use search_knowledge(searchMode='fuzzy')"; } else if (pagination.totalCount > 100) { successMsg += "\n⚠️ MANY RESULTS: Consider adding tags for filtering. Use exactTags=['category']"; } // Prepare the result object const result = { entities: paginatedResult.entities, relations: paginatedResult.relations, pagination: pagination }; return { content: [{ type: "text", text: `${successMsg}\n\n${JSON.stringify(result, null, 2)}` }] }; } case "create_entities": { const entities = args.entities; const result = await knowledgeGraphManager.createEntities(entities, project); const successMsg = `✅ SUCCESS: Created ${result.length} entities`; let nextSteps = result.length > 0 ? "\n🔍 NEXT STEPS: 1) Add relations with create_relations 2) Add status tags with add_tags" : ""; // Add warning message when only a single entity is created if (entities.length === 1) { nextSteps += "\n⚠️ NOTE: For multiple entities, use a single batch call to create_entities."; } return { content: [{ type: "text", text: `${successMsg}${nextSteps}` }] }; } case "add_observations": { const result = await knowledgeGraphManager.addObservations(args.observations, project); const totalAdded = result.reduce((sum, r) => sum + r.addedObservations.length, 0); const successMsg = `✅ SUCCESS: Added ${totalAdded} observations to ${result.length} entities`; return { content: [{ type: "text", text: `${successMsg}` }] }; } case "create_relations": { const result = await knowledgeGraphManager.createRelations(args.relations, project); const successMsg = `✅ SUCCESS: Created ${result.newRelations.length} relations`; let detailsMsg = ""; if (result.skippedRelations.length > 0) { detailsMsg += `\n⚠️ SKIPPED: ${result.skippedRelations.length} duplicate relations:`; result.skippedRelations.forEach(skip => { detailsMsg += `\n • ${skip.reason}`; }); } if (result.totalRequested > 1) { detailsMsg += `\n📊 SUMMARY: ${result.newRelations.length} created, ${result.skippedRelations.length} skipped, ${result.totalRequested} total requested`; } const nextSteps = result.newRelations.length > 0 ? "\n🔍 NEXT STEPS: Use search_knowledge to explore connected entities" : ""; return { content: [{ type: "text", text: `${successMsg}${detailsMsg}${nextSteps}` }] }; } case "delete_entities": { const entityNames = args.entityNames; await knowledgeGraphManager.deleteEntities(entityNames, project); return { content: [{ type: "text", text: `✅ SUCCESS: Deleted ${entityNames.length} entities and all their relations\n⚠️ WARNING: This action cannot be undone` }] }; } case "delete_observations": { const deletions = args.deletions; await knowledgeGraphManager.deleteObservations(deletions, project); const totalDeleted = deletions.reduce((sum, d) => sum + d.observations.length, 0); return { content: [{ type: "text", text: `✅ SUCCESS: Deleted ${totalDeleted} observations from ${deletions.length} entities` }] }; } case "delete_relations": { const relations = args.relations; await knowledgeGraphManager.deleteRelations(relations, project); return { content: [{ type: "text", text: `✅ SUCCESS: Deleted ${relations.length} relations\n🔗 NOTE: Entities remain unchanged` }] }; } case "read_graph": { const result = await knowledgeGraphManager.readGraph(project); const entityCount = result.entities.length; const relationCount = result.relations.length; const successMsg = `📊 GRAPH OVERVIEW: ${entityCount} entities, ${relationCount} relations`; return { content: [{ type: "text", text: `${successMsg}\n\n${JSON.stringify(result, null, 2)}` }] }; } case "open_nodes": { const result = await knowledgeGraphManager.openNodes(args.names, project); const entityCount = result.entities.length; const relationCount = result.relations.length; const successMsg = `📋 RETRIEVED: ${entityCount} entities with ${relationCount} interconnections`; return { content: [{ type: "text", text: `${successMsg}\n\n${JSON.stringify(result, null, 2)}` }] }; } case "add_tags": { const result = await knowledgeGraphManager.addTags(args.updates, project); const totalAdded = result.reduce((sum, r) => sum + r.addedTags.length, 0); const successMsg = `🏷️ SUCCESS: Added ${totalAdded} tags to ${result.length} entities`; const nextSteps = totalAdded > 0 ? "\n🔍 NEXT STEPS: Use search_knowledge(exactTags=['tag']) to find tagged entities" : ""; return { content: [{ type: "text", text: `${successMsg}${nextSteps}` }] }; } case "remove_tags": { const result = await knowledgeGraphManager.removeTags(args.updates, project); const totalRemoved = result.reduce((sum, r) => sum + r.removedTags.length, 0); const successMsg = `🏷️ SUCCESS: Removed ${totalRemoved} tags from ${result.length} entities`; return { content: [{ type: "text", text: `${successMsg}` }] }; } default: throw new Error(`Unknown tool: ${name}`); } }); async function main() { try { // Initialize the knowledge graph manager knowledgeGraphManager = new KnowledgeGraphManager(); // Set up graceful shutdown process.on('SIGINT', async () => { console.error('Received SIGINT, shutting down gracefully...'); await knowledgeGraphManager.close(); process.exit(0); }); process.on('SIGTERM', async () => { console.error('Received SIGTERM, shutting down gracefully...'); await knowledgeGraphManager.close(); process.exit(0); }); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Knowledge Graph MCP Server running on stdio"); } catch (error) { console.error("Failed to initialize server:", error); process.exit(1); } } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); //# sourceMappingURL=index.js.map