@mcp-suite/database
Introduzione
Il pacchetto @mcp-suite/database fornisce un'astrazione per la creazione e gestione di database SQLite utilizzati dai server MCP Suite. Offre due funzionalita principali: una factory per creare connessioni database (createDatabase) e un sistema di migrazioni incrementali (runMigrations).
packages/database/
├── package.json
├── tsconfig.json
└── src/
├── index.ts # Re-export dei moduli pubblici
├── connection.ts # createDatabase(), DatabaseOptions
└── migrations.ts # runMigrations(), Migration
Dipendenze:
@mcp-suite/core- Per la gerarchia di errori e i tipi condivisibetter-sqlite3- Driver SQLite nativo sincrono ad alte prestazioni
createDatabase()
La funzione createDatabase e la factory principale per creare connessioni al database. Gestisce automaticamente la creazione delle directory, la configurazione del WAL mode e l'abilitazione delle foreign key.
Interfaccia DatabaseOptions
export interface DatabaseOptions {
serverName: string; // Nome del server (diventa il nome del file .db)
dataDir?: string; // Directory personalizzata (default: ~/.mcp-suite/data/)
inMemory?: boolean; // Se true, usa un database in memoria (per i test)
}
Implementazione
import Database from 'better-sqlite3';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
const DEFAULT_DATA_DIR = join(homedir(), '.mcp-suite', 'data');
export function createDatabase(options: DatabaseOptions): Database.Database {
// Modalita in-memory per i test
if (options.inMemory) {
return new Database(':memory:');
}
// Crea la directory se non esiste
const dataDir = options.dataDir || DEFAULT_DATA_DIR;
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
}
// Crea il database con il nome del server
const dbPath = join(dataDir, `${options.serverName}.db`);
const dir = dirname(dbPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
const db = new Database(dbPath);
// Configurazione SQLite ottimale
db.pragma('journal_mode = WAL'); // Write-Ahead Logging
db.pragma('foreign_keys = ON'); // Integrta referenziale
return db;
}
Flusso di creazione
createDatabase({ serverName: 'scrum-board' })
│
├── options.inMemory?
│ └── Si ──► new Database(':memory:')
│
├── Determina dataDir
│ └── options.dataDir || ~/.mcp-suite/data/
│
├── Crea directory (se non esiste)
│ └── mkdirSync(dataDir, { recursive: true })
│
├── Costruisce percorso database
│ └── ~/.mcp-suite/data/scrum-board.db
│
├── Crea connessione SQLite
│ └── new Database(dbPath)
│
├── Configura pragma
│ ├── journal_mode = WAL
│ └── foreign_keys = ON
│
└── Restituisce ──► Database.Database
Directory dei Database
Percorso di default
Il percorso predefinito e ~/.mcp-suite/data/ che si espande a:
Struttura della directory
~/.mcp-suite/data/
├── scrum-board.db # Sprint, storie, task
├── scrum-board.db-wal # WAL file (Write-Ahead Log)
├── scrum-board.db-shm # Shared memory per WAL
├── standup-notes.db # Note di standup
├── time-tracking.db # Registrazioni tempo
├── snippet-manager.db # Snippet di codice
├── project-economics.db # Budget, costi, economia
├── retrospective-manager.db # Retrospettive e action items
├── environment-manager.db # Variabili d'ambiente
└── ... # Un file per ogni server con persistenza
Personalizzazione del percorso
# Globale (tutti i server)
export MCP_SUITE_DATA_DIR=/var/data/mcp-suite
# Per un singolo server
export MCP_SUITE_SCRUM_BOARD_DATA_DIR=/var/data/scrum
WAL Journal Mode
Il Write-Ahead Logging (WAL) e un journal mode di SQLite che offre significativi vantaggi rispetto al default (rollback journal):
┌─────────────────────────────────────────────────────┐
│ Modalita WAL │
│ │
│ Lettori ──► Leggono dal file principale │
│ (non bloccati dalle scritture) │
│ │
│ Scrittore ──► Scrive nel file WAL │
│ (non blocca i lettori) │
│ │
│ Checkpoint ──► Periodicamente, il WAL viene │
│ sincronizzato nel file principale │
└─────────────────────────────────────────────────────┘
Vantaggi del WAL mode:
- Letture concorrenti: piu lettori simultanei senza blocchi
- Scritture non bloccanti: le scritture non bloccano le letture
- Performance migliori: meno operazioni I/O per le transazioni
- Crash recovery: il WAL file permette il recupero dopo crash
Foreign Keys
L'abilitazione delle foreign key con PRAGMA foreign_keys = ON garantisce l'integrita referenziale:
-- Con foreign_keys ON, questa operazione fallira se la story
-- non esiste nella tabella stories
INSERT INTO tasks (title, storyId) VALUES ('Bug fix', 999);
-- Error: FOREIGN KEY constraint failed
Sistema di Migrazioni
Il sistema di migrazioni permette ai server di evolvere il proprio schema database in modo incrementale e sicuro.
Interfaccia Migration
export interface Migration {
version: number; // Numero progressivo della migrazione
description: string; // Descrizione leggibile della migrazione
up: string; // SQL da eseguire per applicare la migrazione
}
Funzione runMigrations
export function runMigrations(db: Database.Database, migrations: Migration[]): void {
// 1. Crea la tabella _migrations se non esiste
db.exec(`
CREATE TABLE IF NOT EXISTS _migrations (
version INTEGER PRIMARY KEY,
description TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
// 2. Legge le migrazioni gia applicate
const applied = db
.prepare('SELECT version FROM _migrations ORDER BY version')
.all() as Array<{ version: number }>;
const appliedVersions = new Set(applied.map((m) => m.version));
// 3. Ordina le migrazioni per versione
const sorted = [...migrations].sort((a, b) => a.version - b.version);
// 4. Applica solo le migrazioni nuove
const insertMigration = db.prepare(
'INSERT INTO _migrations (version, description) VALUES (?, ?)',
);
for (const migration of sorted) {
if (appliedVersions.has(migration.version)) continue;
db.exec(migration.up); // Esegue SQL
insertMigration.run(migration.version, migration.description); // Registra
}
}
Flusso delle Migrazioni
runMigrations(db, migrations)
│
├── Crea tabella _migrations (se non esiste)
│ └── version | description | applied_at
│
├── Legge versioni gia applicate
│ └── SELECT version FROM _migrations
│
├── Per ogni migrazione non applicata (in ordine):
│ ├── Esegue SQL (db.exec(migration.up))
│ └── Registra nella tabella _migrations
│
└── Completato (schema aggiornato)
Come i Server Definiscono i Propri Schemi
Ogni server che necessita di persistenza definisce le proprie migrazioni nel file store/service:
// In servers/scrum-board/src/services/scrum-store.ts
const migrations: Migration[] = [
{
version: 1,
description: 'Create sprints, stories, and tasks tables',
up: `
CREATE TABLE IF NOT EXISTS sprints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
startDate TEXT NOT NULL,
endDate TEXT NOT NULL,
goals TEXT NOT NULL DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'planning',
createdAt TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
acceptanceCriteria TEXT NOT NULL DEFAULT '[]',
storyPoints INTEGER NOT NULL DEFAULT 0,
priority TEXT NOT NULL DEFAULT 'medium',
status TEXT NOT NULL DEFAULT 'todo',
sprintId INTEGER REFERENCES sprints(id),
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'todo',
assignee TEXT,
storyId INTEGER NOT NULL REFERENCES stories(id),
sprintId INTEGER REFERENCES sprints(id),
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
);
`,
},
// Futura migrazione per aggiungere colonne o indici:
// {
// version: 2,
// description: 'Add tags column to tasks',
// up: `ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT '[]';`,
// },
];
E nel costruttore dello Store:
export class ScrumStore {
private db: Database.Database;
constructor(options?: { inMemory?: boolean; dataDir?: string }) {
this.db = createDatabase({
serverName: 'scrum-board', // => scrum-board.db
inMemory: options?.inMemory,
dataDir: options?.dataDir,
});
runMigrations(this.db, migrations); // Applica le migrazioni
}
}
Pattern degli Array JSON
SQLite non ha un tipo nativo per gli array. MCP Suite usa la convenzione di serializzare gli array come stringhe JSON:
// Salvataggio: JSON.stringify
const stmt = db.prepare('INSERT INTO sprints (goals) VALUES (?)');
stmt.run(JSON.stringify(['Goal 1', 'Goal 2']));
// Lettura: JSON.parse
function toSprint(row: SprintRow): Sprint {
return {
...row,
goals: JSON.parse(row.goals) as string[],
};
}
Questo pattern e usato in tutti i server per campi come:
goals(sprint) - array di stringheacceptanceCriteria(story) - array di stringhetags(snippet, task) - array di stringhe
Database In-Memory per i Test
Per i test, ogni store supporta la modalita in-memory che non crea file su disco:
import { ScrumStore } from './services/scrum-store.js';
// In un test
const store = new ScrumStore({ inMemory: true });
// Il database vive solo in RAM
// Le migrazioni vengono applicate normalmente
// Alla fine del test, il database viene automaticamente distrutto
Questo approccio garantisce:
- Isolamento: ogni test ha il proprio database
- Velocita: nessuna operazione I/O su disco
- Pulizia automatica: il database scompare quando l'oggetto viene garbage collected