UNPKG

lorehub

Version:

Capture and surface the collective wisdom of your codebase

1,447 lines (1,314 loc) 49.1 kB
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 type { Tool } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { Database } from '../db/database.js'; import { getDbPath } from '../cli/utils/db-config.js'; import type { Lore, LoreType, RelationType } from '../core/types.js'; // Tool schemas const SearchLoresSchema = z.object({ query: z.string().describe('Search query (supports wildcards * and ?)'), realm_path: z.string().optional().describe('Realm path to search in (searches all realms if not specified)'), type: z.enum(['decree', 'wisdom', 'belief', 'constraint', 'requirement', 'risk', 'quest', 'saga', 'story', 'anomaly', 'other']).optional(), province: z.string().optional().describe('Filter by province (service in monorepos)'), limit: z.number().optional().default(50).describe('Maximum number of results'), }); const ListLoresSchema = z.object({ realm_path: z.string().optional().describe('Realm path to list lores from (lists from all realms if not specified)'), type: z.enum(['decree', 'wisdom', 'belief', 'constraint', 'requirement', 'risk', 'quest', 'saga', 'story', 'anomaly', 'other']).optional(), province: z.string().optional().describe('Filter by province (service in monorepos)'), limit: z.number().optional().default(20).describe('Maximum number of results'), }); const GetLoreSchema = z.object({ lore_id: z.string().describe('The ID of the lore to retrieve'), }); const ListRealmsSchema = z.object({}); const CreateLoreSchema = z.object({ realm_path: z.string().describe('Realm path where the lore should be created'), content: z.string().describe('The main content of the lore'), why: z.string().optional().describe('Additional context or reasoning for this lore'), type: z.enum(['decree', 'wisdom', 'belief', 'constraint', 'requirement', 'risk', 'quest', 'saga', 'story', 'anomaly', 'other']).describe('The type of lore'), provinces: z.array(z.string()).optional().describe('Provinces this lore applies to (services in monorepos)'), sigils: z.array(z.string()).optional().describe('Sigils for categorizing the lore (tags)'), confidence: z.number().min(0).max(100).optional().default(80).describe('Confidence level (0-100)'), origin: z.object({ type: z.enum(['manual', 'inferred', 'imported']).default('manual'), reference: z.string().default('mcp'), context: z.string().optional(), }).optional().default({ type: 'manual', reference: 'mcp' }), }); const UpdateLoreSchema = z.object({ lore_id: z.string().describe('The ID of the lore to update'), content: z.string().optional().describe('Updated content of the lore'), why: z.string().optional().describe('Updated context or reasoning'), type: z.enum(['decree', 'wisdom', 'belief', 'constraint', 'requirement', 'risk', 'quest', 'saga', 'story', 'anomaly', 'other']).optional(), provinces: z.array(z.string()).optional().describe('Updated provinces list'), sigils: z.array(z.string()).optional().describe('Updated sigils list'), confidence: z.number().min(0).max(100).optional().describe('Updated confidence level'), status: z.enum(['living', 'ancient', 'whispered', 'proclaimed', 'archived']).optional().describe('Updated status'), origin: z.object({ type: z.enum(['manual', 'inferred', 'imported']), reference: z.string(), context: z.string().optional(), }).optional(), }); const DeleteLoreSchema = z.object({ lore_id: z.string().describe('The ID of the lore to delete'), confirm: z.boolean().describe('Must be true to confirm deletion'), }); const ArchiveLoreSchema = z.object({ lore_id: z.string().describe('The ID of the lore to archive'), }); const RestoreLoreSchema = z.object({ lore_id: z.string().describe('The ID of the lore to restore'), }); const CreateRelationSchema = z.object({ from_lore_id: z.string().describe('The ID of the source lore'), to_lore_id: z.string().describe('The ID of the target lore'), type: z.enum(['succeeds', 'challenges', 'supports', 'depends_on', 'bound_to']).describe('The type of relationship'), strength: z.number().min(0).max(1).optional().default(1.0).describe('Strength of the relationship (0.0-1.0)'), metadata: z.record(z.unknown()).optional().describe('Additional metadata for the relationship'), }); const DeleteRelationSchema = z.object({ from_lore_id: z.string().describe('The ID of the source lore'), to_lore_id: z.string().describe('The ID of the target lore'), type: z.enum(['supersedes', 'contradicts', 'supports', 'depends_on', 'relates_to']).describe('The type of relationship to delete'), }); const ListRelationsSchema = z.object({ lore_id: z.string().describe('The ID of the lore to list relationships for'), direction: z.enum(['from', 'to', 'both']).optional().default('both').describe('Direction of relationships to list'), }); const GetRealmStatsSchema = z.object({ realm_path: z.string().describe('The realm path to get statistics for'), }); const SemanticSearchLoresSchema = z.object({ query: z.string().describe('Natural language query to find semantically similar lores'), realm_path: z.string().optional().describe('Realm path to search in (searches all realms if not specified)'), threshold: z.number().min(0).max(1).optional().default(0.7).describe('Similarity threshold (0-1, higher is more similar)'), limit: z.number().optional().default(20).describe('Maximum number of results'), }); const FindSimilarLoresSchema = z.object({ lore_id: z.string().describe('The ID of the lore to find similar lores for'), limit: z.number().optional().default(10).describe('Maximum number of similar lores to return'), threshold: z.number().min(0).max(1).optional().default(0.5).describe('Similarity threshold (0-1)'), }); export class LoreHubServer { private server: Server; private db: Database; constructor(dbPathOrInstance?: string | Database) { this.server = new Server( { name: 'lorehub', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); if (typeof dbPathOrInstance === 'object' && dbPathOrInstance !== null) { // Assume it's a Database instance or mock this.db = dbPathOrInstance as Database; } else { this.db = new Database(dbPathOrInstance || getDbPath()); } this.setupHandlers(); } private setupHandlers() { // Handle tool listing this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_lores', description: 'Search lores across all realms with optional filters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (supports wildcards * and ?)' }, realm_path: { type: 'string', description: 'Realm path to search in (searches all realms if not specified)' }, type: { type: 'string', enum: ['decree', 'assumption', 'constraint', 'requirement', 'risk', 'lesson', 'quest', 'other'], description: 'Filter by lore type' }, province: { type: 'string', description: 'Filter by province (for monorepos)' }, limit: { type: 'number', description: 'Maximum number of results', default: 50 }, }, required: ['query'], }, }, { name: 'list_lores', description: 'List lores from all realms with optional filters', inputSchema: { type: 'object', properties: { realm_path: { type: 'string', description: 'Realm path to list lores from (lists from all realms if not specified)' }, type: { type: 'string', enum: ['decree', 'assumption', 'constraint', 'requirement', 'risk', 'lesson', 'quest', 'other'], description: 'Filter by lore type' }, province: { type: 'string', description: 'Filter by province (for monorepos)' }, limit: { type: 'number', description: 'Maximum number of results', default: 20 }, }, required: [], }, }, { name: 'get_lore', description: 'Get a specific lore by ID', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to retrieve' }, }, required: ['lore_id'], }, }, { name: 'list_realms', description: 'List all realms in the LoreHub database', inputSchema: { type: 'object', properties: {}, }, }, { name: 'create_lore', description: 'Create a new lore in a realm', inputSchema: { type: 'object', properties: { realm_path: { type: 'string', description: 'Realm path where the lore should be created' }, content: { type: 'string', description: 'The main content of the lore' }, why: { type: 'string', description: 'Additional context or reasoning for this lore' }, type: { type: 'string', enum: ['decree', 'assumption', 'constraint', 'requirement', 'risk', 'lesson', 'quest', 'other'], description: 'The type of lore' }, provinces: { type: 'array', items: { type: 'string' }, description: 'Provinces this lore applies to (for monorepos)' }, sigils: { type: 'array', items: { type: 'string' }, description: 'Sigils for categorizing the lore' }, confidence: { type: 'number', minimum: 0, maximum: 100, description: 'Confidence level (0-100)', default: 80 }, origin: { type: 'object', properties: { type: { type: 'string', enum: ['manual', 'inferred', 'imported'], default: 'manual' }, reference: { type: 'string', default: 'mcp' }, context: { type: 'string' } }, default: { type: 'manual', reference: 'mcp' } } }, required: ['realm_path', 'content', 'type'], }, }, { name: 'update_lore', description: 'Update an existing lore', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to update' }, content: { type: 'string', description: 'Updated content of the lore' }, why: { type: 'string', description: 'Updated context or reasoning' }, type: { type: 'string', enum: ['decree', 'assumption', 'constraint', 'requirement', 'risk', 'lesson', 'quest', 'other'], description: 'Updated lore type' }, provinces: { type: 'array', items: { type: 'string' }, description: 'Updated provinces list' }, sigils: { type: 'array', items: { type: 'string' }, description: 'Updated sigils list' }, confidence: { type: 'number', minimum: 0, maximum: 100, description: 'Updated confidence level' }, status: { type: 'string', enum: ['living', 'ancient', 'whispered', 'proclaimed', 'archived'], description: 'Updated status' }, origin: { type: 'object', properties: { type: { type: 'string', enum: ['manual', 'inferred', 'imported'] }, reference: { type: 'string' }, context: { type: 'string' } } } }, required: ['lore_id'], }, }, { name: 'delete_lore', description: 'Permanently delete a lore (requires confirmation)', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to delete' }, confirm: { type: 'boolean', description: 'Must be true to confirm deletion' }, }, required: ['lore_id', 'confirm'], }, }, { name: 'archive_lore', description: 'Archive a lore (soft delete)', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to archive' }, }, required: ['lore_id'], }, }, { name: 'restore_lore', description: 'Restore an archived lore', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to restore' }, }, required: ['lore_id'], }, }, { name: 'create_relation', description: 'Create a relationship between two lores', inputSchema: { type: 'object', properties: { from_lore_id: { type: 'string', description: 'The ID of the source lore' }, to_lore_id: { type: 'string', description: 'The ID of the target lore' }, type: { type: 'string', enum: ['supersedes', 'contradicts', 'supports', 'depends_on', 'relates_to'], description: 'The type of relationship' }, strength: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of the relationship (0.0-1.0)', default: 1.0 }, metadata: { type: 'object', description: 'Additional metadata for the relationship' }, }, required: ['from_lore_id', 'to_lore_id', 'type'], }, }, { name: 'delete_relation', description: 'Delete a relationship between two lores', inputSchema: { type: 'object', properties: { from_lore_id: { type: 'string', description: 'The ID of the source lore' }, to_lore_id: { type: 'string', description: 'The ID of the target lore' }, type: { type: 'string', enum: ['supersedes', 'contradicts', 'supports', 'depends_on', 'relates_to'], description: 'The type of relationship to delete' }, }, required: ['from_lore_id', 'to_lore_id', 'type'], }, }, { name: 'list_relations', description: 'List all relationships for a lore', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to list relationships for' }, direction: { type: 'string', enum: ['from', 'to', 'both'], description: 'Direction of relationships to list', default: 'both' }, }, required: ['lore_id'], }, }, { name: 'get_realm_stats', description: 'Get detailed statistics about a realm', inputSchema: { type: 'object', properties: { realm_path: { type: 'string', description: 'The realm path to get statistics for' }, }, required: ['realm_path'], }, }, { name: 'semantic_search_lores', description: 'Search lores using semantic similarity to find conceptually related lores', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query to find semantically similar lores' }, realm_path: { type: 'string', description: 'Realm path to search in (searches all realms if not specified)' }, threshold: { type: 'number', description: 'Similarity threshold (0-1, higher is more similar)', default: 0.7, minimum: 0, maximum: 1 }, limit: { type: 'number', description: 'Maximum number of results', default: 20 }, }, required: ['query'], }, }, { name: 'find_similar_lores', description: 'Find lores similar to a given lore', inputSchema: { type: 'object', properties: { lore_id: { type: 'string', description: 'The ID of the lore to find similar lores for' }, limit: { type: 'number', description: 'Maximum number of similar lores to return', default: 10 }, threshold: { type: 'number', description: 'Similarity threshold (0-1)', default: 0.5, minimum: 0, maximum: 1 }, }, required: ['lore_id'], }, }, ], }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case 'search_lores': return this.searchLores(request.params.arguments); case 'list_lores': return this.listLores(request.params.arguments); case 'get_lore': return this.getLore(request.params.arguments); case 'list_realms': return this.listRealms(request.params.arguments); case 'create_lore': return this.createLore(request.params.arguments); case 'update_lore': return this.updateLore(request.params.arguments); case 'delete_lore': return this.deleteLore(request.params.arguments); case 'archive_lore': return this.archiveLore(request.params.arguments); case 'restore_lore': return this.restoreLore(request.params.arguments); case 'create_relation': return this.createRelation(request.params.arguments); case 'delete_relation': return this.deleteRelation(request.params.arguments); case 'list_relations': return this.listRelations(request.params.arguments); case 'get_realm_stats': return this.getRealmStats(request.params.arguments); case 'semantic_search_lores': return this.semanticSearchLores(request.params.arguments); case 'find_similar_lores': return this.findSimilarLores(request.params.arguments); default: throw new Error(`Tool not found: ${request.params.name}`); } }); } private async searchLores(args: unknown) { const params = SearchLoresSchema.parse(args); let realmsToSearch = this.db.listRealms(); let requestedRealm = null; // If specific realm requested, filter to just that realm if (params.realm_path) { const realm = this.db.findRealmByPath(params.realm_path); if (!realm) { throw new Error(`Realm not found at path: ${params.realm_path}`); } realmsToSearch = [realm]; requestedRealm = realm; } // Search across all selected realms let allLores: Array<Lore & { realmName: string; realmPath: string }> = []; for (const realm of realmsToSearch) { const lores = this.db.searchLores(realm.id, params.query); allLores.push(...lores.map(l => ({ ...l, realmName: realm.name, realmPath: realm.path }))); } // Apply filters if (params.type) { allLores = allLores.filter(l => l.type === params.type); } if (params.province) { allLores = allLores.filter(l => l.provinces.includes(params.province!)); } // Sort by date allLores.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); // Apply limit const limit = params.limit || 50; if (allLores.length > limit) { allLores = allLores.slice(0, limit); } return { content: [ { type: 'text', text: JSON.stringify({ lores: allLores.map(l => ({ ...this.formatLore(l), realm: { name: l.realmName, path: l.realmPath, }, })), total: allLores.length, searchedRealms: requestedRealm ? 1 : realmsToSearch.length, }, null, 2), }, ], }; } private async listLores(args: unknown) { const params = ListLoresSchema.parse(args); let realmsToList = this.db.listRealms(); let requestedRealm = null; // If specific realm requested, filter to just that realm if (params.realm_path) { const realm = this.db.findRealmByPath(params.realm_path); if (!realm) { throw new Error(`Realm not found at path: ${params.realm_path}`); } realmsToList = [realm]; requestedRealm = realm; } // List lores from all selected realms let allLores: Array<Lore & { realmName: string; realmPath: string }> = []; for (const realm of realmsToList) { let lores: Lore[]; if (params.type) { lores = this.db.listLoresByType(realm.id, params.type as LoreType); } else if (params.province) { lores = this.db.listLoresByProvince(realm.id, params.province); } else { lores = this.db.listLoresByRealm(realm.id); } allLores.push(...lores.map(l => ({ ...l, realmName: realm.name, realmPath: realm.path }))); } // Sort by creation date descending allLores.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); // Apply limit const limit = params.limit || 20; if (allLores.length > limit) { allLores = allLores.slice(0, limit); } return { content: [ { type: 'text', text: JSON.stringify({ lores: allLores.map(l => ({ ...this.formatLore(l), realm: { name: l.realmName, path: l.realmPath, }, })), total: allLores.length, listedRealms: requestedRealm ? 1 : realmsToList.length, }, null, 2), }, ], }; } private async getLore(args: unknown) { const params = GetLoreSchema.parse(args); const lore = this.db.findLore(params.lore_id); if (!lore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } const realm = this.db.listRealms().find(r => r.id === lore.realmId); return { content: [ { type: 'text', text: JSON.stringify({ lore: this.formatLore(lore), realm: realm ? { id: realm.id, name: realm.name, path: realm.path, } : undefined, }, null, 2), }, ], }; } private async listRealms(_args: unknown) { const realms = this.db.listRealms(); return { content: [ { type: 'text', text: JSON.stringify({ realms: realms.map(r => ({ id: r.id, name: r.name, path: r.path, isMonorepo: r.isMonorepo, provinces: r.provinces, lastSeen: r.lastSeen.toISOString(), createdAt: r.createdAt.toISOString(), loreCount: this.db.getRealmLoreCount(r.id), })), total: realms.length, }, null, 2), }, ], }; } private formatLore(lore: Lore) { return { id: lore.id, content: lore.content, why: lore.why, type: lore.type, provinces: lore.provinces, sigils: lore.sigils, confidence: lore.confidence, status: lore.status, origin: lore.origin, createdAt: lore.createdAt.toISOString(), updatedAt: lore.updatedAt.toISOString(), }; } private async createLore(args: unknown) { const params = CreateLoreSchema.parse(args); // Find or create the realm by path let realm = this.db.findRealmByPath(params.realm_path); if (!realm) { // Auto-initialize the realm if it doesn't exist const realmName = params.realm_path.split('/').pop() || 'unknown'; realm = this.db.createRealm({ name: realmName, path: params.realm_path, isMonorepo: false, provinces: params.provinces || [], }); } // Create the lore const lore = await this.db.createLore({ realmId: realm.id, content: params.content, why: params.why, type: params.type as LoreType, provinces: params.provinces, sigils: params.sigils, confidence: params.confidence, origin: params.origin, }); return { content: [ { type: 'text', text: JSON.stringify({ lore: this.formatLore(lore), realm: { id: realm.id, name: realm.name, path: realm.path, created: !realm.lastSeen || realm.lastSeen.toISOString() === realm.createdAt.toISOString(), }, message: !realm.lastSeen || realm.lastSeen.toISOString() === realm.createdAt.toISOString() ? 'Realm initialized and lore created successfully' : 'Lore created successfully', }, null, 2), }, ], }; } private async updateLore(args: unknown) { const params = UpdateLoreSchema.parse(args); // Verify the lore exists before updating const existingLore = this.db.findLore(params.lore_id); if (!existingLore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } // Update the lore const updatedLore = await this.db.updateLore(params.lore_id, { content: params.content, why: params.why, type: params.type as LoreType | undefined, provinces: params.provinces, sigils: params.sigils, confidence: params.confidence, status: params.status as any, origin: params.origin, }); if (!updatedLore) { throw new Error(`Failed to update lore with ID: ${params.lore_id}`); } // Get the realm for context const realm = this.db.listRealms().find(r => r.id === updatedLore.realmId); return { content: [ { type: 'text', text: JSON.stringify({ lore: this.formatLore(updatedLore), realm: realm ? { id: realm.id, name: realm.name, path: realm.path, } : undefined, message: 'Lore updated successfully', }, null, 2), }, ], }; } private async deleteLore(args: unknown) { const params = DeleteLoreSchema.parse(args); // Require explicit confirmation if (!params.confirm) { throw new Error('Deletion must be confirmed by setting confirm: true'); } // Verify the lore exists before deleting const lore = this.db.findLore(params.lore_id); if (!lore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } // Get the realm for context before deletion const realm = this.db.listRealms().find(r => r.id === lore.realmId); // Delete the lore this.db.deleteLore(params.lore_id); return { content: [ { type: 'text', text: JSON.stringify({ deleted: true, lore_id: params.lore_id, realm: realm ? { id: realm.id, name: realm.name, path: realm.path, } : undefined, message: 'Lore permanently deleted', }, null, 2), }, ], }; } private async archiveLore(args: unknown) { const params = ArchiveLoreSchema.parse(args); // Verify the lore exists before archiving const lore = this.db.findLore(params.lore_id); if (!lore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } // Check if already archived if (lore.status === 'archived') { throw new Error('Lore is already archived'); } // Archive the lore (soft delete) this.db.softDeleteLore(params.lore_id); // Get the updated lore const archivedLore = this.db.findLore(params.lore_id); if (!archivedLore) { throw new Error(`Failed to archive lore with ID: ${params.lore_id}`); } // Get the realm for context const realm = this.db.listRealms().find(r => r.id === archivedLore.realmId); return { content: [ { type: 'text', text: JSON.stringify({ lore: this.formatLore(archivedLore), realm: realm ? { id: realm.id, name: realm.name, path: realm.path, } : undefined, message: 'Lore archived successfully', }, null, 2), }, ], }; } private async restoreLore(args: unknown) { const params = RestoreLoreSchema.parse(args); // Verify the lore exists before restoring const lore = this.db.findLore(params.lore_id); if (!lore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } // Check if not archived if (lore.status !== 'archived') { throw new Error('Lore is not archived'); } // Restore the lore this.db.restoreLore(params.lore_id); // Get the updated lore const restoredLore = this.db.findLore(params.lore_id); if (!restoredLore) { throw new Error(`Failed to restore lore with ID: ${params.lore_id}`); } // Get the realm for context const realm = this.db.listRealms().find(r => r.id === restoredLore.realmId); return { content: [ { type: 'text', text: JSON.stringify({ lore: this.formatLore(restoredLore), realm: realm ? { id: realm.id, name: realm.name, path: realm.path, } : undefined, message: 'Lore restored successfully', }, null, 2), }, ], }; } private async createRelation(args: unknown) { const params = CreateRelationSchema.parse(args); // Verify both lores exist const fromLore = this.db.findLore(params.from_lore_id); if (!fromLore) { throw new Error(`Source lore not found with ID: ${params.from_lore_id}`); } const toLore = this.db.findLore(params.to_lore_id); if (!toLore) { throw new Error(`Target lore not found with ID: ${params.to_lore_id}`); } // Verify lores are in the same realm if (fromLore.realmId !== toLore.realmId) { throw new Error('Cannot create relation between lores in different realms'); } // Create the relation const relation = await this.db.createRelation({ fromLoreId: params.from_lore_id, toLoreId: params.to_lore_id, type: params.type as RelationType, strength: params.strength, metadata: params.metadata, }); return { content: [ { type: 'text', text: JSON.stringify({ relation: { fromLoreId: relation.fromLoreId, toLoreId: relation.toLoreId, type: relation.type, strength: relation.strength, metadata: relation.metadata, createdAt: relation.createdAt.toISOString(), }, fromLore: { id: fromLore.id, content: fromLore.content, }, toLore: { id: toLore.id, content: toLore.content, }, message: 'Relation created successfully', }, null, 2), }, ], }; } private async deleteRelation(args: unknown) { const params = DeleteRelationSchema.parse(args); // Verify the relation exists by checking if the lores exist const fromLore = this.db.findLore(params.from_lore_id); if (!fromLore) { throw new Error(`Source lore not found with ID: ${params.from_lore_id}`); } const toLore = this.db.findLore(params.to_lore_id); if (!toLore) { throw new Error(`Target lore not found with ID: ${params.to_lore_id}`); } // Delete the relation await this.db.deleteRelation(params.from_lore_id, params.to_lore_id, params.type); return { content: [ { type: 'text', text: JSON.stringify({ deleted: true, fromLoreId: params.from_lore_id, toLoreId: params.to_lore_id, type: params.type, message: 'Relation deleted successfully', }, null, 2), }, ], }; } private async listRelations(args: unknown) { const params = ListRelationsSchema.parse(args); // Verify the lore exists const lore = this.db.findLore(params.lore_id); if (!lore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } // Get all relations for the lore const allRelations = this.db.listRelationsByLore(params.lore_id); // Filter by direction let relations = allRelations; if (params.direction === 'from') { relations = allRelations.filter(r => r.fromLoreId === params.lore_id); } else if (params.direction === 'to') { relations = allRelations.filter(r => r.toLoreId === params.lore_id); } // Get lore details for related lores const relatedLoreIds = new Set<string>(); relations.forEach(r => { if (r.fromLoreId !== params.lore_id) relatedLoreIds.add(r.fromLoreId); if (r.toLoreId !== params.lore_id) relatedLoreIds.add(r.toLoreId); }); const relatedLores = new Map<string, any>(); for (const loreId of relatedLoreIds) { const relatedLore = this.db.findLore(loreId); if (relatedLore) { relatedLores.set(loreId, { id: relatedLore.id, content: relatedLore.content, type: relatedLore.type, }); } } return { content: [ { type: 'text', text: JSON.stringify({ lore: { id: lore.id, content: lore.content, type: lore.type, }, relations: relations.map(r => ({ fromLoreId: r.fromLoreId, toLoreId: r.toLoreId, type: r.type, strength: r.strength, metadata: r.metadata, createdAt: r.createdAt.toISOString(), direction: r.fromLoreId === params.lore_id ? 'outgoing' : 'incoming', relatedLore: relatedLores.get(r.fromLoreId === params.lore_id ? r.toLoreId : r.fromLoreId), })), total: relations.length, direction: params.direction, }, null, 2), }, ], }; } private async getRealmStats(args: unknown) { const params = GetRealmStatsSchema.parse(args); // Find the realm by path const realm = this.db.findRealmByPath(params.realm_path); if (!realm) { throw new Error(`Realm not found at path: ${params.realm_path}`); } // Get all lores for the realm const allLores = this.db.listLoresByRealm(realm.id); // Calculate statistics const stats = { totalLores: allLores.length, loresByType: {} as Record<LoreType, number>, loresByStatus: {} as Record<string, number>, loresByProvince: {} as Record<string, number>, averageConfidence: 0, highConfidenceLores: 0, lowConfidenceLores: 0, recentLores: [] as any[], loreGrowth: { lastWeek: 0, lastMonth: 0, lastQuarter: 0, }, topSigils: [] as { sigil: string; count: number }[], relationStats: { totalRelations: 0, byType: {} as Record<string, number>, }, }; // Count lores by type for (const type of ['decree', 'wisdom', 'belief', 'constraint', 'requirement', 'risk', 'quest', 'saga', 'story', 'anomaly', 'other'] as LoreType[]) { stats.loresByType[type] = 0; } // Count lores by status for (const status of ['living', 'ancient', 'whispered', 'proclaimed', 'archived']) { stats.loresByStatus[status] = 0; } // Tag frequency map const sigilFrequency = new Map<string, number>(); // Time boundaries for growth metrics const now = new Date(); const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); let totalConfidence = 0; // Process lores for (const lore of allLores) { // Type counts stats.loresByType[lore.type]++; // Status counts const status = lore.status || 'active'; if (!(status in stats.loresByStatus)) { stats.loresByStatus[status] = 0; } stats.loresByStatus[status]!++; // Province counts for (const province of lore.provinces) { stats.loresByProvince[province] = (stats.loresByProvince[province] || 0) + 1; } // Confidence metrics totalConfidence += lore.confidence; if (lore.confidence >= 90) stats.highConfidenceLores++; if (lore.confidence < 50) stats.lowConfidenceLores++; // Sigil frequency for (const sigil of lore.sigils) { sigilFrequency.set(sigil, (sigilFrequency.get(sigil) || 0) + 1); } // Growth metrics if (lore.createdAt >= oneWeekAgo) stats.loreGrowth.lastWeek++; if (lore.createdAt >= oneMonthAgo) stats.loreGrowth.lastMonth++; if (lore.createdAt >= threeMonthsAgo) stats.loreGrowth.lastQuarter++; } // Calculate averages if (allLores.length > 0) { stats.averageConfidence = Math.round(totalConfidence / allLores.length); } // Get top 10 sigils const sortedSigils = Array.from(sigilFrequency.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([sigil, count]) => ({ sigil, count })); stats.topSigils = sortedSigils; // Get 5 most recent lores stats.recentLores = allLores .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) .slice(0, 5) .map(l => ({ id: l.id, content: l.content, type: l.type, createdAt: l.createdAt.toISOString(), })); // Get relation statistics const allRelations = []; for (const lore of allLores) { const relations = this.db.listRelationsByLore(lore.id); allRelations.push(...relations); } // Deduplicate relations (since we're getting both directions) const uniqueRelations = new Map<string, any>(); for (const rel of allRelations) { const key = `${rel.fromLoreId}-${rel.toLoreId}-${rel.type}`; uniqueRelations.set(key, rel); } stats.relationStats.totalRelations = uniqueRelations.size; for (const rel of uniqueRelations.values()) { stats.relationStats.byType[rel.type] = (stats.relationStats.byType[rel.type] || 0) + 1; } return { content: [ { type: 'text', text: JSON.stringify({ realm: { id: realm.id, name: realm.name, path: realm.path, isMonorepo: realm.isMonorepo, provinces: realm.provinces, createdAt: realm.createdAt.toISOString(), lastSeen: realm.lastSeen.toISOString(), }, statistics: stats, }, null, 2), }, ], }; } private async semanticSearchLores(args: unknown) { const params = SemanticSearchLoresSchema.parse(args); try { // Perform semantic search const results = await this.db.semanticSearchLores(params.query, { realmId: params.realm_path ? this.db.findRealmByPath(params.realm_path)?.id : undefined, threshold: params.threshold, limit: params.limit, includeScore: true }); // Enrich results with realm information const enrichedResults = results.map(lore => { const realm = this.db.listRealms().find(r => r.id === lore.realmId); return { ...this.formatLore(lore), similarity: lore.similarity, realm: realm ? { name: realm.name, path: realm.path, } : undefined, }; }); return { content: [ { type: 'text', text: JSON.stringify({ lores: enrichedResults, total: enrichedResults.length, query: params.query, searchType: 'semantic', threshold: params.threshold, }, null, 2), }, ], }; } catch (error) { // Fallback to regular search if semantic search fails console.error('Semantic search failed, falling back to keyword search:', error); return this.searchLores({ query: params.query, realm_path: params.realm_path, limit: params.limit, }); } } private async findSimilarLores(args: unknown) { const params = FindSimilarLoresSchema.parse(args); // Verify the lore exists const lore = this.db.findLore(params.lore_id); if (!lore) { throw new Error(`Lore not found with ID: ${params.lore_id}`); } try { // Find similar lores const similarLores = await this.db.findSimilarLores(params.lore_id, { limit: params.limit, threshold: params.threshold }); // Enrich results with realm information const enrichedResults = similarLores.map(similarLore => { const realm = this.db.listRealms().find(r => r.id === similarLore.realmId); return { ...this.formatLore(similarLore), similarity: similarLore.similarity, realm: realm ? { name: realm.name, path: realm.path, } : undefined, }; }); return { content: [ { type: 'text', text: JSON.stringify({ originalLore: { ...this.formatLore(lore), realm: this.db.listRealms().find(r => r.id === lore.realmId)?.name, }, similarLores: enrichedResults, total: enrichedResults.length, threshold: params.threshold, }, null, 2), }, ], }; } catch (error) { console.error('Finding similar lores failed:', error); throw new Error(`Failed to find similar lores: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getServerInfo() { return { name: 'lorehub', version: '0.1.0', capabilities: { tools: {}, }, }; } async listTools() { // Return the tools directly for testing return [ { name: 'search_lores', description: 'Search lores across all realms with optional filters', }, { name: 'list_lores', description: 'List lores from all realms with optional filters', }, { name: 'get_lore', description: 'Get a specific lore by ID', }, { name: 'list_realms', description: 'List all realms in the LoreHub database', }, { name: 'create_lore', description: 'Create a new lore in a realm', }, { name: 'update_lore', description: 'Update an existing lore', }, { name: 'delete_lore', description: 'Permanently delete a lore (requires confirmation)', }, { name: 'archive_lore', description: 'Archive a lore (soft delete)', }, { name: 'restore_lore', description: 'Restore an archived lore', }, { name: 'create_relation', description: 'Create a relationship between two lores', }, { name: 'delete_relation', description: 'Delete a relationship between two lores', }, { name: 'list_relations', description: 'List all relationships for a lore', }, { name: 'get_realm_stats', description: 'Get detailed statistics about a realm', }, { name: 'semantic_search_lores', description: 'Search lores using semantic similarity to find conceptually related lores', }, { name: 'find_similar_lores', description: 'Find lores similar to a given lore', }, ]; } async callTool(name: string, args: any) { // Direct implementation for testing switch (name) { case 'search_lores': const searchResult = await this.searchLores(args); return JSON.parse(searchResult.content[0]!.text!); case 'list_lores': const listResult = await this.listLores(args); return JSON.parse(listResult.content[0]!.text!); case 'get_lore': const getResult = await this.getLore(args); return JSON.parse(getResult.content[0]!.text!); case 'list_realms': const realmsResult = await this.listRealms(args); return JSON.parse(realmsResult.content[0]!.text!); case 'create_lore': const createResult = await this.createLore(args); return JSON.parse(createResult.content[0]!.text!); case 'update_lore': const updateResult = await this.updateLore(args); return JSON.parse(updateResult.content[0]!.text!); case 'delete_lore': const deleteResult = await this.deleteLore(args); return JSON.parse(deleteResult.content[0]!.text!); case 'archive_lore': const archiveResult = await this.archiveLore(args); return JSON.parse(archiveResult.content[0]!.text!); case 'restore_lore': const restoreResult = await this.restoreLore(args); return JSON.parse(restoreResult.content[0]!.text!); case 'create_relation': const createRelResult = await this.createRelation(args); return JSON.parse(createRelResult.content[0]!.text!); case 'delete_relation': const deleteRelResult = await this.deleteRelation(args); return JSON.parse(deleteRelResult.content[0]!.text!); case 'list_relations': const listRelResult = await this.listRelations(args); return JSON.parse(listRelResult.content[0]!.text!); case 'get_realm_stats': const statsResult = await this.getRealmStats(args); return JSON.parse(statsResult.content[0]!.text!); case 'semantic_search_lores': const semanticResult = await this.semanticSearchLores(args); return JSON.parse(semanticResult.content[0]!.text!); case 'find_similar_lores': const similarResult = await this.findSimilarLores(args); return JSON.parse(similarResult.content[0]!.text!); default: throw new Error(`Tool not found: ${name}`); } } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('LoreHub MCP server started'); } close() { this.db.close(); } }