lorehub
Version:
Capture and surface the collective wisdom of your codebase
1,447 lines (1,314 loc) • 49.1 kB
text/typescript
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();
}
}