knowledgegraph-mcp
Version:
MCP server for enabling persistent knowledge storage for Claude through a knowledge graph with multiple storage backends
232 lines • 8.53 kB
JavaScript
import Database from 'better-sqlite3';
import { StorageType } from '../types.js';
import { mkdirSync, existsSync } from 'fs';
import { dirname } from 'path';
/**
* SQLite storage provider for lightweight deployments
*/
export class SQLiteStorageProvider {
config;
db = null;
constructor(config) {
this.config = config;
if (config.type !== StorageType.SQLITE) {
throw new Error(`Only SQLite is supported, got: ${config.type}`);
}
}
/**
* Initialize SQLite database and create tables
*/
async initialize() {
try {
// Extract file path from connection string
// Format: sqlite:///path/to/database.db or sqlite://./database.db
const dbPath = this.extractDbPath(this.config.connectionString);
// Ensure the directory exists for file-based databases (not in-memory)
if (dbPath !== ':memory:' && !dbPath.startsWith('./')) {
const dbDir = dirname(dbPath);
if (!existsSync(dbDir)) {
try {
mkdirSync(dbDir, { recursive: true });
console.log(`Created SQLite database directory: ${dbDir}`);
}
catch (error) {
console.warn(`Failed to create SQLite database directory: ${error}`);
}
}
}
this.db = new Database(dbPath, {
verbose: this.config.options?.verbose ? console.log : undefined,
fileMustExist: false
});
// Enable WAL mode for better concurrency
this.db.pragma('journal_mode = WAL');
// Create tables
this.createTables();
console.log(`SQLite database initialized at: ${dbPath}`);
}
catch (error) {
throw new Error(`Failed to initialize SQLite database: ${error}`);
}
}
/**
* Close database connection
*/
async close() {
try {
if (this.db) {
this.db.close();
this.db = null;
}
}
catch (error) {
console.warn(`Error closing SQLite database: ${error}`);
}
}
/**
* Health check for SQLite storage
*/
async healthCheck() {
try {
if (this.db) {
// Simple query to check if database is accessible
this.db.prepare('SELECT 1').get();
return true;
}
return false;
}
catch {
return false;
}
}
/**
* Load knowledge graph from SQLite database
*/
async loadGraph(project) {
if (!this.db)
throw new Error('SQLite database not initialized');
try {
// Load entities
const entitiesStmt = this.db.prepare(`
SELECT name, entity_type, observations, tags
FROM entities
WHERE project = ?
ORDER BY updated_at DESC, name
`);
const entityRows = entitiesStmt.all(project);
const entities = entityRows.map((row) => ({
name: row.name,
entityType: row.entity_type,
observations: JSON.parse(row.observations || '[]'),
tags: JSON.parse(row.tags || '[]')
}));
// Load relations
const relationsStmt = this.db.prepare(`
SELECT from_entity, to_entity, relation_type
FROM relations
WHERE project = ?
ORDER BY from_entity, to_entity, created_at DESC
`);
const relationRows = relationsStmt.all(project);
const relations = relationRows.map((row) => ({
from: row.from_entity,
to: row.to_entity,
relationType: row.relation_type
}));
return { entities, relations };
}
catch (error) {
throw new Error(`Failed to load graph for project ${project}: ${error}`);
}
}
/**
* Save knowledge graph to SQLite database
*/
async saveGraph(graph, project) {
if (!this.db)
throw new Error('SQLite database not initialized');
try {
// Use transaction for atomicity
const transaction = this.db.transaction(() => {
// Clear existing data
this.db.prepare('DELETE FROM entities WHERE project = ?').run(project);
this.db.prepare('DELETE FROM relations WHERE project = ?').run(project);
// Insert entities
const insertEntity = this.db.prepare(`
INSERT INTO entities (project, name, entity_type, observations, tags, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`);
for (const entity of graph.entities) {
insertEntity.run(project, entity.name, entity.entityType, JSON.stringify(entity.observations), JSON.stringify(entity.tags || []));
}
// Insert relations
const insertRelation = this.db.prepare(`
INSERT INTO relations (project, from_entity, to_entity, relation_type, created_at, updated_at)
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))
`);
for (const relation of graph.relations) {
insertRelation.run(project, relation.from, relation.to, relation.relationType);
}
});
transaction();
}
catch (error) {
throw new Error(`Failed to save graph for project ${project}: ${error}`);
}
}
/**
* Extract database file path from connection string
*/
extractDbPath(connectionString) {
// Handle different SQLite connection string formats:
// sqlite:///absolute/path/to/db.sqlite
// sqlite://./relative/path/to/db.sqlite
// sqlite://:memory: (in-memory database)
if (connectionString.startsWith('sqlite:///')) {
return connectionString.substring(9); // Remove 'sqlite://' to get '/absolute/path'
}
else if (connectionString.startsWith('sqlite://')) {
return connectionString.substring(9); // Remove 'sqlite://' to get './relative/path'
}
else if (connectionString === 'sqlite://:memory:') {
return ':memory:';
}
else if (connectionString === ':memory:') {
return ':memory:';
}
else {
// Assume it's a direct file path
return connectionString;
}
}
/**
* Create database tables
*/
createTables() {
if (!this.db)
throw new Error('Database not initialized');
// Create entities table
this.db.exec(`
CREATE TABLE IF NOT EXISTS entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT NOT NULL,
name TEXT NOT NULL,
entity_type TEXT NOT NULL,
observations TEXT DEFAULT '[]',
tags TEXT DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(project, name)
)
`);
// Create relations table
this.db.exec(`
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT NOT NULL,
from_entity TEXT NOT NULL,
to_entity TEXT NOT NULL,
relation_type TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(project, from_entity, to_entity, relation_type)
)
`);
// Create indexes for better performance
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project);
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type);
CREATE INDEX IF NOT EXISTS idx_relations_project ON relations(project);
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
`);
}
/**
* Get SQLite database instance (for search strategies)
*/
getSQLiteDatabase() {
return this.db;
}
}
//# sourceMappingURL=sqlite-storage.js.map