iTerm MCP Server

import { createClient } from '@libsql/client'; import { Entity, Relation, SearchResult } from '../types/index.js'; // Types for configuration interface DatabaseConfig { url: string; authToken?: string; } export class DatabaseManager { private static instance: DatabaseManager; private client; private constructor(config: DatabaseConfig) { if (!config.url) { throw new Error('Database URL is required'); } this.client = createClient({ url: config.url, authToken: config.authToken, }); } public static async get_instance( config: DatabaseConfig, ): Promise<DatabaseManager> { if (!DatabaseManager.instance) { DatabaseManager.instance = new DatabaseManager(config); await DatabaseManager.instance.initialize(); } return DatabaseManager.instance; } // Convert array to vector string representation with validation private array_to_vector_string( numbers: number[] | undefined, ): string { // If no embedding provided, create a default zero vector if (!numbers || !Array.isArray(numbers)) { return '[0.0, 0.0, 0.0, 0.0]'; } // Validate vector dimensions match schema (4 dimensions for testing) if (numbers.length !== 4) { throw new Error( `Vector must have exactly 4 dimensions, got ${numbers.length}. Please provide a 4D vector or omit for default zero vector.`, ); } // Validate all elements are numbers and convert NaN/Infinity to 0 const sanitized_numbers = numbers.map((n) => { if (typeof n !== 'number' || isNaN(n) || !isFinite(n)) { console.warn( `Invalid vector value detected, using 0.0 instead of: ${n}`, ); return 0.0; } return n; }); return `[${sanitized_numbers.join(', ')}]`; } // Extract vector from binary format private async extract_vector( embedding: Uint8Array, ): Promise<number[]> { const result = await this.client.execute({ sql: 'SELECT vector_extract(?) as vec', args: [embedding], }); const vecStr = result.rows[0].vec as string; return JSON.parse(vecStr); } // Entity operations async create_entities( entities: Array<{ name: string; entityType: string; observations: string[]; embedding?: number[]; }>, ): Promise<void> { try { for (const entity of entities) { // Validate entity name if ( !entity.name || typeof entity.name !== 'string' || entity.name.trim() === '' ) { throw new Error('Entity name must be a non-empty string'); } // Validate entity type if ( !entity.entityType || typeof entity.entityType !== 'string' || entity.entityType.trim() === '' ) { throw new Error( `Invalid entity type for entity "${entity.name}"`, ); } // Validate observations if ( !Array.isArray(entity.observations) || entity.observations.length === 0 ) { throw new Error( `Entity "${entity.name}" must have at least one observation`, ); } if ( !entity.observations.every( (obs) => typeof obs === 'string' && obs.trim() !== '', ) ) { throw new Error( `Entity "${entity.name}" has invalid observations. All observations must be non-empty strings`, ); } // Start a transaction const txn = await this.client.transaction('write'); try { const vector_string = this.array_to_vector_string( entity.embedding, ); // First try to update const result = await txn.execute({ sql: 'UPDATE entities SET entity_type = ?, embedding = vector32(?) WHERE name = ?', args: [entity.entityType, vector_string, entity.name], }); // If no rows affected, do insert if (result.rowsAffected === 0) { await txn.execute({ sql: 'INSERT INTO entities (name, entity_type, embedding) VALUES (?, ?, vector32(?))', args: [entity.name, entity.entityType, vector_string], }); } // Clear old observations await txn.execute({ sql: 'DELETE FROM observations WHERE entity_name = ?', args: [entity.name], }); // Add new observations for (const observation of entity.observations) { await txn.execute({ sql: 'INSERT INTO observations (entity_name, content) VALUES (?, ?)', args: [entity.name, observation], }); } await txn.commit(); } catch (error) { await txn.rollback(); throw error; } } } catch (error) { // Wrap all errors with context throw new Error( `Entity operation failed: ${ error instanceof Error ? error.message : String(error) }`, ); } } async search_similar( embedding: number[], limit: number = 5, ): Promise<SearchResult[]> { try { // Validate input vector if (!Array.isArray(embedding)) { throw new Error('Search embedding must be an array'); } const vector_string = this.array_to_vector_string(embedding); // Use vector_top_k to find similar entities, excluding zero vectors const results = await this.client.execute({ sql: ` SELECT e.name, e.entity_type, e.embedding, vector_distance_cos(e.embedding, vector32(?)) as distance FROM entities e WHERE e.embedding IS NOT NULL AND e.embedding != vector32('[0.0, 0.0, 0.0, 0.0]') ORDER BY distance ASC LIMIT ? `, args: [vector_string, limit], }); // Get observations for each entity const search_results: SearchResult[] = []; for (const row of results.rows) { try { const observations = await this.client.execute({ sql: 'SELECT content FROM observations WHERE entity_name = ?', args: [row.name], }); const entity_embedding = await this.extract_vector( row.embedding as Uint8Array, ); search_results.push({ entity: { name: row.name as string, entityType: row.entity_type as string, observations: observations.rows.map( (obs) => obs.content as string, ), embedding: entity_embedding, }, distance: row.distance as number, }); } catch (error) { console.warn( `Failed to process search result for entity "${ row.name }": ${ error instanceof Error ? error.message : String(error) }`, ); // Continue processing other results continue; } } return search_results; } catch (error) { throw new Error( `Similarity search failed: ${ error instanceof Error ? error.message : String(error) }`, ); } } async get_entity(name: string): Promise<Entity> { const entity_result = await this.client.execute({ sql: 'SELECT name, entity_type, embedding FROM entities WHERE name = ?', args: [name], }); if (entity_result.rows.length === 0) { throw new Error(`Entity not found: ${name}`); } const observations_result = await this.client.execute({ sql: 'SELECT content FROM observations WHERE entity_name = ?', args: [name], }); const embedding = entity_result.rows[0].embedding ? await this.extract_vector( entity_result.rows[0].embedding as Uint8Array, ) : undefined; return { name: entity_result.rows[0].name as string, entityType: entity_result.rows[0].entity_type as string, observations: observations_result.rows.map( (row) => row.content as string, ), embedding, }; } async search_entities(query: string): Promise<Entity[]> { const results = await this.client.execute({ sql: ` SELECT DISTINCT e.name, e.entity_type, e.embedding FROM entities e LEFT JOIN observations o ON e.name = o.entity_name WHERE e.name LIKE ? OR e.entity_type LIKE ? OR o.content LIKE ? `, args: [`%${query}%`, `%${query}%`, `%${query}%`], }); const entities: Entity[] = []; for (const row of results.rows) { const name = row.name as string; const observations = await this.client.execute({ sql: 'SELECT content FROM observations WHERE entity_name = ?', args: [name], }); const embedding = row.embedding ? await this.extract_vector(row.embedding as Uint8Array) : undefined; entities.push({ name, entityType: row.entity_type as string, observations: observations.rows.map( (obs) => obs.content as string, ), embedding, }); } return entities; } async get_recent_entities(limit = 10): Promise<Entity[]> { const results = await this.client.execute({ sql: 'SELECT name, entity_type, embedding FROM entities ORDER BY created_at DESC LIMIT ?', args: [limit], }); const entities: Entity[] = []; for (const row of results.rows) { const name = row.name as string; const observations = await this.client.execute({ sql: 'SELECT content FROM observations WHERE entity_name = ?', args: [name], }); const embedding = row.embedding ? await this.extract_vector(row.embedding as Uint8Array) : undefined; entities.push({ name, entityType: row.entity_type as string, observations: observations.rows.map( (obs) => obs.content as string, ), embedding, }); } return entities; } // Relation operations async create_relations(relations: Relation[]): Promise<void> { try { if (relations.length === 0) return; // Prepare batch statements for all relations const batch_statements = relations.map((relation) => ({ sql: 'INSERT INTO relations (source, target, relation_type) VALUES (?, ?, ?)', args: [relation.from, relation.to, relation.relationType], })); // Execute all inserts in a single batch transaction await this.client.batch(batch_statements, 'write'); } catch (error) { throw new Error( `Failed to create relations: ${ error instanceof Error ? error.message : String(error) }`, ); } } async delete_entity(name: string): Promise<void> { try { // Check if entity exists first const existing = await this.client.execute({ sql: 'SELECT name FROM entities WHERE name = ?', args: [name], }); if (existing.rows.length === 0) { throw new Error(`Entity not found: ${name}`); } // Prepare batch statements for deletion const batch_statements = [ { // Delete associated observations first (due to foreign key) sql: 'DELETE FROM observations WHERE entity_name = ?', args: [name], }, { // Delete associated relations (due to foreign key) sql: 'DELETE FROM relations WHERE source = ? OR target = ?', args: [name, name], }, { // Delete the entity sql: 'DELETE FROM entities WHERE name = ?', args: [name], }, ]; // Execute all deletions in a single batch transaction await this.client.batch(batch_statements, 'write'); } catch (error) { throw new Error( `Failed to delete entity "${name}": ${ error instanceof Error ? error.message : String(error) }`, ); } } async delete_relation( source: string, target: string, type: string, ): Promise<void> { try { const result = await this.client.execute({ sql: 'DELETE FROM relations WHERE source = ? AND target = ? AND relation_type = ?', args: [source, target, type], }); if (result.rowsAffected === 0) { throw new Error( `Relation not found: ${source} -> ${target} (${type})`, ); } } catch (error) { throw new Error( `Failed to delete relation: ${ error instanceof Error ? error.message : String(error) }`, ); } } async get_relations_for_entities( entities: Entity[], ): Promise<Relation[]> { if (entities.length === 0) return []; const entity_names = entities.map((e) => e.name); const placeholders = entity_names.map(() => '?').join(','); const results = await this.client.execute({ sql: ` SELECT source as from_entity, target as to_entity, relation_type FROM relations WHERE source IN (${placeholders}) OR target IN (${placeholders}) `, args: [...entity_names, ...entity_names], }); return results.rows.map((row) => ({ from: row.from_entity as string, to: row.to_entity as string, relationType: row.relation_type as string, })); } // Graph operations async read_graph(): Promise<{ entities: Entity[]; relations: Relation[]; }> { const recent_entities = await this.get_recent_entities(); const relations = await this.get_relations_for_entities( recent_entities, ); return { entities: recent_entities, relations }; } async search_nodes( query: string | number[], ): Promise<{ entities: Entity[]; relations: Relation[] }> { try { let entities: Entity[]; if (Array.isArray(query)) { // Validate vector query if (!query.every((n) => typeof n === 'number')) { throw new Error('Vector query must contain only numbers'); } // Vector similarity search const results = await this.search_similar(query); entities = results.map((r) => r.entity); } else { // Validate text query if (typeof query !== 'string') { throw new Error('Text query must be a string'); } if (query.trim() === '') { throw new Error('Text query cannot be empty'); } // Text-based search entities = await this.search_entities(query); } // If no entities found, return empty result if (entities.length === 0) { return { entities: [], relations: [] }; } const relations = await this.get_relations_for_entities( entities, ); return { entities, relations }; } catch (error) { throw new Error( `Node search failed: ${ error instanceof Error ? error.message : String(error) }`, ); } } // Database operations public get_client() { return this.client; } public async initialize() { try { // Create tables if they don't exist - each as a single statement await this.client.execute(` CREATE TABLE IF NOT EXISTS entities ( name TEXT PRIMARY KEY, entity_type TEXT NOT NULL, embedding F32_BLOB(4), -- 4-dimension vector for testing created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); await this.client.execute(` CREATE TABLE IF NOT EXISTS observations ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_name TEXT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (entity_name) REFERENCES entities(name) ) `); await this.client.execute(` CREATE TABLE IF NOT EXISTS relations ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, target TEXT NOT NULL, relation_type TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (source) REFERENCES entities(name), FOREIGN KEY (target) REFERENCES entities(name) ) `); // Create all indexes in a single batch transaction await this.client.batch( [ { sql: 'CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name)', args: [], }, { sql: 'CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_name)', args: [], }, { sql: 'CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source)', args: [], }, { sql: 'CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target)', args: [], }, { sql: 'CREATE INDEX IF NOT EXISTS idx_entities_embedding ON entities(libsql_vector_idx(embedding))', args: [], }, ], 'write', ); } catch (error) { throw new Error( `Database initialization failed: ${ error instanceof Error ? error.message : String(error) }`, ); } } public async close() { try { await this.client.close(); } catch (error) { console.error('Error closing database connection:', error); } } } export type { DatabaseConfig };