Introduzione: I 6 Pacchetti Condivisi di Tech-MCP
In un monorepo con 22 server MCP, la duplicazione del codice sarebbe un problema devastante. Ogni server avrebbe bisogno della propria factory, del proprio logger, della propria gestione database, moltiplicando la complessità di manutenzione per 22. La soluzione architetturale adottata dal progetto Tech-MCP e quella dei pacchetti condivisi: 6 librerie interne che concentrano tutta la logica trasversale in un unico punto, riutilizzabile da ogni server.
Questi pacchetti non sono semplici utility: rappresentano l'infrastruttura fondamentale su cui poggia ogni singolo server. La factory crea server in 4 righe di codice, l'EventBus abilita la collaborazione tra server con 29 eventi tipizzati, il database fornisce persistenza SQLite con migrazioni automatiche, il testing permette test in-process senza rete, il CLI gestisce i server da terminale e il Client Manager orchestra la comunicazione sincrona server-to-server.
I 6 Pacchetti Condivisi
| Pacchetto | Responsabilità | Dipendenze Chiave |
|---|---|---|
| @mcp-suite/core | Server factory, configurazione, logger, errori, tipi | @modelcontextprotocol/sdk, zod |
| @mcp-suite/event-bus | 29 eventi tipizzati, LocalEventBus, pattern matching | micromatch |
| @mcp-suite/database | createDatabase, WAL mode, migrazioni incrementali | better-sqlite3 |
| @mcp-suite/testing | TestHarness, MockEventBus, InMemoryTransport | @modelcontextprotocol/sdk |
| @mcp-suite/cli | Comandi list, start, status per gestione server | commander |
| @mcp-suite/client-manager | Pool client, 3 trasporti, comunicazione server-to-server | @modelcontextprotocol/sdk |
@mcp-suite/core: Il Cuore della Suite
Il pacchetto @mcp-suite/core e il fondamento su cui ogni server viene costruito.
Fornisce cinque moduli critici: la factory per la creazione di server, il
sistema di configurazione con validazione Zod, un logger strutturato
che scrive su stderr, una gerarchia di errori tipizzati e i tipi di dominio
condivisi tra tutti i 22 server.
packages/core/
+-- package.json
+-- tsconfig.json
+-- src/
+-- index.ts # Re-export di tutti i moduli
+-- server-factory.ts # createMcpServer, startServer, McpSuiteServer
+-- config.ts # loadConfig, ServerConfigSchema
+-- logger.ts # Logger strutturato su stderr
+-- errors.ts # Gerarchia errori tipizzati
+-- types.ts # Tipi di dominio condivisi
CreateServerOptions e McpSuiteServer
Il cuore di @mcp-suite/core e la coppia di interfacce che definiscono come un server
viene creato e cosa rappresenta una volta istanziato. CreateServerOptions descrive
le opzioni di ingresso, mentre McpSuiteServer rappresenta il contratto centrale
dell'architettura: ogni server la implementa, ogni funzione di registrazione tool la riceve.
export interface CreateServerOptions {
name: string; // Nome univoco del server (es. 'scrum-board')
version: string; // Versione semantica (es. '0.1.0')
description?: string; // Descrizione leggibile
config?: Partial<ServerConfig>; // Override della configurazione
eventBus?: EventBus; // EventBus opzionale per collaborazione
}
export interface McpSuiteServer {
name: string; // Nome univoco del server
server: McpServer; // Istanza del server MCP dall'SDK ufficiale
config: ServerConfig; // Configurazione caricata e validata con Zod
logger: Logger; // Logger strutturato (scrive su stderr)
eventBus?: EventBus; // EventBus opzionale (undefined se non fornito)
httpServer?: Server; // Riferimento al server HTTP (se trasporto HTTP)
}
createMcpServer(): La Factory Principale
La funzione createMcpServer() e la factory che ogni server invoca per crearsi.
In sole poche righe orchestra quattro operazioni fondamentali: carica la configurazione dalle
variabili d'ambiente, crea il logger con il livello configurato, istanzia il McpServer
dall'SDK ufficiale e restituisce il bundle completo McpSuiteServer.
export function createMcpServer(options: CreateServerOptions): McpSuiteServer {
// 1. Carica configurazione da env + override
const config = loadConfig(options.name, options.config);
// 2. Crea logger con il livello configurato
const logger = new Logger(options.name, config.logLevel);
logger.info(`Initializing ${options.name} v${options.version}`);
// 3. Istanzia il McpServer dall'SDK ufficiale
const server = new McpServer({
name: options.name,
version: options.version,
});
// 4. Restituisce il bundle completo
return { server, config, logger, eventBus: options.eventBus };
}
Flusso della Factory
CreateServerOptions
|
+--> loadConfig(name, overrides) --> ServerConfig
|
+--> new Logger(name, logLevel) --> Logger
|
+--> new McpServer({name, version}) --> McpServer
|
+--> { server, config, logger, eventBus } --> McpSuiteServer
startServer(): Avvio con Trasporto Automatico
Una volta creato il McpSuiteServer, la funzione startServer() lo avvia
selezionando automaticamente il trasporto in base alla configurazione. Il trasporto STDIO usa
stdin/stdout per la comunicazione JSON-RPC, mentre il trasporto HTTP
usa il protocollo Streamable HTTP dell'SDK MCP con modalità stateful
(ogni sessione ha un UUID).
Nota del 2026-10-30: la modalità stateful con session UUID segue le revisioni del protocollo
fino alla 2025-11-25. La revisione 2026-07-28 ha rimosso le sessioni e l'header
Mcp-Session-Id (fonte: changelog della specifica, consultato il 2026-10-30).
Il codice di Tech-MCP qui descritto non è stato riscritto per la nuova revisione: vedi il riquadro
in testa all'articolo 5 della serie per le differenze.
export async function startStdioServer(suite: McpSuiteServer): Promise<void> {
const transport = new StdioServerTransport();
suite.logger.info('Starting server with STDIO transport');
await suite.server.connect(transport);
}
export async function startHttpServer(suite: McpSuiteServer): Promise<void> {
const port = suite.config.port ?? 3000;
const app = createMcpExpressApp();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await suite.server.connect(transport);
app.post('/mcp', async (req, res) => {
await transport.handleRequest(req, res, req.body);
});
app.get('/mcp', async (req, res) => {
await transport.handleRequest(req, res);
});
app.get('/health', (_req, res) => {
res.json({ status: 'ok', server: suite.name });
});
suite.httpServer = app.listen(port);
}
export async function startServer(suite: McpSuiteServer): Promise<void> {
if (suite.config.transport === 'http') {
await startHttpServer(suite);
} else {
await startStdioServer(suite);
}
}
config.ts: Configurazione con Validazione Zod
Il sistema di configurazione segue una cascata di priorità: variabile specifica
per server, poi variabile globale, poi override programmatico, infine default definito nello schema Zod.
Lo schema ServerConfigSchema definisce i parametri validi e i loro valori predefiniti.
export const ServerConfigSchema = z.object({
transport: z.enum(['stdio', 'http']).default('stdio'),
port: z.number().optional(),
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
dataDir: z.string().optional(),
eventBus: z.object({
type: z.enum(['local', 'redis']).default('local'),
redisUrl: z.string().optional(),
}).default({ type: 'local' }),
});
La funzione loadConfig() converte il nome del server in formato variabile d'ambiente,
permettendo configurazioni granulari per ogni server:
# Globale (tutti i server)
MCP_SUITE_TRANSPORT=http
# Specifico per scrum-board
MCP_SUITE_SCRUM_BOARD_LOG_LEVEL=debug
MCP_SUITE_SCRUM_BOARD_TRANSPORT=stdio
logger.ts: Logger Strutturato su stderr
Il Logger scrive log strutturati in formato JSON su stderr. Questa scelta e
fondamentale: il protocollo MCP usa stdout per la comunicazione
JSON-RPC, quindi i log devono andare su un canale separato. Se i log andassero su stdout,
corromperebbero il flusso JSON-RPC e il client MCP non potrebbe comunicare con il server.
export class Logger {
private level: number;
constructor(
private readonly name: string,
level: LogLevel = 'info',
) {
this.level = LOG_LEVELS[level];
}
info(message: string, data?: Record<string, unknown>): void {
this.log('info', message, data);
}
private log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
if (LOG_LEVELS[level] < this.level) return;
const entry = {
timestamp: new Date().toISOString(),
level,
server: this.name,
message,
...data,
};
process.stderr.write(JSON.stringify(entry) + '\n');
}
}
perchè stderr?
Il protocollo MCP usa stdout esclusivamente per i messaggi JSON-RPC tra client e server.
Se il logger scrivesse su stdout, i messaggi di log si mescolerebbero con il protocollo,
corrompendo la comunicazione. stderr e il canale designato per la diagnostica,
mantenendo la separazione tra dati e log.
errors.ts: Gerarchia di Errori Tipizzati
Il pacchetto definisce una gerarchia di errori tipizzati che permette ai server di gestire i diversi tipi di fallimento in modo uniforme. Ogni errore ha un codice machine-readable e dettagli opzionali per il debugging.
Gerarchia degli Errori
| Classe | Codice | Uso |
|---|---|---|
McpSuiteError |
- | Classe base di tutti gli errori |
ConfigError |
CONFIG_ERROR | Configurazione non valida o mancante |
ConnectionError |
CONNECTION_ERROR | Problemi di connessione a database o servizi |
ToolExecutionError |
TOOL_EXECUTION_ERROR | Errore durante l'esecuzione di un tool MCP |
NotFoundError |
NOT_FOUND | Risorsa richiesta non trovata |
ValidationError |
VALIDATION_ERROR | Input dell'utente non valido |
import { NotFoundError, ToolExecutionError } from '@mcp-suite/core';
// In uno store
const sprint = db.prepare('SELECT * FROM sprints WHERE id = ?').get(id);
if (!sprint) {
throw new NotFoundError('Sprint', String(id));
// Messaggio: "Sprint with id '42' not found"
}
// In un tool handler
try {
const result = store.createSprint(input);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (error) {
if (error instanceof NotFoundError) {
return { content: [{ type: 'text', text: error.message }], isError: true };
}
throw new ToolExecutionError('Failed to create sprint', error);
}
types.ts: Tipi di Dominio Condivisi
Il modulo types.ts definisce oltre 30 tipi di dominio condivisi tra i server,
organizzati per area funzionale. Questo garantisce che tutti i server usino le stesse strutture
dati per concetti comuni come task, sprint, metriche e risultati dei tool.
export interface ToolSuccess<T = unknown> {
success: true;
data: T;
metadata?: Record<string, unknown>;
}
export interface ToolError {
success: false;
error: string;
code: string;
details?: unknown;
}
export type ToolResult<T = unknown> = ToolSuccess<T> | ToolError;
Organizzazione dei Tipi per Dominio
| Dominio | Tipi Principali | Usati da |
|---|---|---|
| Code & Git | FileReference, GitCommitInfo, CodeIssue |
code-review, codebase-knowledge |
| Project Management | TaskStatus, UserStory, SprintInfo |
scrum-board, agile-metrics |
| Economics | BudgetInfo, BudgetCategory, CostEntry |
project-economics |
| CI/CD | PipelineStatus, PipelineRun, PipelineStage |
cicd-monitor |
| Database | TableInfo, ColumnInfo, IndexInfo |
db-schema-explorer |
Il file index.ts ri-esporta tutto in modo organizzato, permettendo ai server
di importare qualsiasi componente da un unico punto di ingresso:
import {
createMcpServer,
type McpSuiteServer,
type EventBus,
Logger,
NotFoundError
} from '@mcp-suite/core';
@mcp-suite/event-bus: Collaborazione Inter-Server con Eventi Tipizzati
Il pacchetto @mcp-suite/event-bus implementa un sistema di eventi tipizzato per la
collaborazione asincrona tra server. Definisce 29 eventi organizzati per dominio,
un'interfaccia EventBus generica e un'implementazione locale basata su
EventEmitter di Node.js con supporto per pattern matching
tramite la libreria micromatch.
packages/event-bus/
+-- package.json
+-- tsconfig.json
+-- src/
+-- index.ts # Re-export dei moduli pubblici
+-- bus.ts # Interfaccia EventBus, tipi handler
+-- events.ts # EventMap con tutti i 29 eventi tipizzati
+-- local-bus.ts # LocalEventBus (implementazione in-process)
Interfaccia EventBus
L'interfaccia EventBus definisce il contratto per qualsiasi implementazione
del bus di eventi. Espone quattro metodi: publish per emettere eventi tipizzati,
subscribe per ascoltare un evento specifico, subscribePattern per
ascoltare eventi che corrispondono a un pattern wildcard e clear per rimuovere
tutte le sottoscrizioni.
export interface EventBus {
publish<E extends EventName>(
event: E, payload: EventPayload<E>
): Promise<void>;
subscribe<E extends EventName>(
event: E, handler: EventHandler<E>
): () => void;
subscribePattern(
pattern: string, handler: PatternHandler
): () => void;
clear(): void;
}
// Handler per evento specifico (payload tipizzato)
export type EventHandler<E extends EventName> = (
payload: EventPayload<E>
) => void | Promise<void>;
// Handler per pattern (nome evento + payload generico)
export type PatternHandler = (
event: string, payload: unknown
) => void | Promise<void>;
EventMap: I 29 Eventi Organizzati per Dominio
L'EventMap e un'interfaccia TypeScript che mappa ogni nome evento al suo payload
tipizzato. Gli eventi seguono la convenzione dominio:azione e coprono 11 domini
funzionali, dal code review alle retrospettive, dal CI/CD allo standup.
Mappa dei Domini degli Eventi
| Dominio | Pattern | Num. Eventi | Esempi |
|---|---|---|---|
| Code & Git | code:* |
3 | commit-analyzed, review-completed, dependency-alert |
| CI/CD | cicd:* |
2 | pipeline-completed, build-failed |
| Scrum | scrum:* |
4 | sprint-started, sprint-completed, task-updated, story-completed |
| Time Tracking | time:* |
2 | entry-logged, timesheet-generated |
| Database | db:* |
2 | schema-changed, index-suggestion |
| Testing | test:* |
2 | generated, coverage-report |
| Docs | docs:* |
2 | api-updated, stale-detected |
| Performance | perf:* |
2 | bottleneck-found, profile-completed |
| Retrospettive | retro:* |
2 | action-item-created, completed |
| Economics | economics:* |
2 | budget-alert, cost-updated |
| Standup | standup:* |
1 | report-generated |
I tipi derivati dall'EventMap permettono la tipizzazione forte in fase di compilazione:
// Nome di qualsiasi evento valido (union di 29 stringhe letterali)
export type EventName = keyof EventMap;
// Payload tipizzato per un evento specifico
export type EventPayload<E extends EventName> = EventMap[E];
// EventPayload<'scrum:sprint-started'>
// => { sprintId: string; name: string; startDate: string; endDate: string }
LocalEventBus: Implementazione In-Process
La LocalEventBus e l'implementazione predefinita basata su EventEmitter
di Node.js. Quando viene pubblicato un evento, il bus emette prima per i subscriber diretti,
poi verifica tutti i pattern subscriber usando micromatch per il matching wildcard.
export class LocalEventBus implements EventBus {
private emitter = new EventEmitter();
private patternSubs: PatternSubscription[] = [];
constructor() {
this.emitter.setMaxListeners(100);
}
async publish<E extends EventName>(
event: E, payload: EventPayload<E>
): Promise<void> {
// 1. Emetti per subscriber diretti
this.emitter.emit(event, payload);
// 2. Verifica pattern subscriber
for (const sub of this.patternSubs) {
if (micromatch.isMatch(event, sub.pattern)) {
try {
await sub.handler(event, payload);
} catch {
// Errori catturati silenziosamente
}
}
}
}
subscribe<E extends EventName>(
event: E, handler: EventHandler<E>
): () => void {
this.emitter.on(event, handler);
return () => { this.emitter.off(event, handler); };
}
subscribePattern(pattern: string, handler: PatternHandler): () => void {
const sub = { pattern, handler };
this.patternSubs.push(sub);
return () => {
const index = this.patternSubs.indexOf(sub);
if (index >= 0) this.patternSubs.splice(index, 1);
};
}
clear(): void {
this.emitter.removeAllListeners();
this.patternSubs = [];
}
}
Pattern Fire-and-Forget e Wildcard
I tool pubblicano eventi usando l'operatore optional chaining (?.)
per gestire il caso in cui l'EventBus non sia presente. Questo pattern fire-and-forget
garantisce che il tool non attenda mai il completamento degli handler, e che gli errori negli
handler non propaghino al tool chiamante.
// Nel tool handler: fire-and-forget
eventBus?.publish('scrum:sprint-started', {
sprintId: String(sprint.id),
name: sprint.name,
startDate: sprint.startDate,
endDate: sprint.endDate,
});
La libreria micromatch supporta pattern wildcard avanzati per le sottoscrizioni:
Pattern Wildcard Supportati
| Pattern | Corrisponde a | Esempio |
|---|---|---|
scrum:* |
Tutti gli eventi scrum | scrum:sprint-started, scrum:task-updated |
*:*-completed |
Tutti gli eventi di completamento | scrum:sprint-completed, retro:completed |
*:*-alert |
Tutti gli alert | code:dependency-alert, economics:budget-alert |
{scrum,time}:* |
Eventi scrum o time | scrum:task-updated, time:entry-logged |
// Sottoscrizione a tutti gli eventi del dominio scrum
eventBus.subscribePattern('scrum:*', (eventName, payload) => {
console.log(`Evento scrum: ${eventName}`, payload);
});
// Sottoscrizione a tutti gli eventi di completamento
eventBus.subscribePattern('*:*-completed', (eventName, payload) => {
console.log(`Completamento: ${eventName}`, payload);
});
// Cancellare una sottoscrizione
const unsubscribe = eventBus.subscribe('scrum:task-updated', handler);
unsubscribe(); // rimuove il listener
@mcp-suite/database: Persistenza SQLite con WAL e Migrazioni
Il pacchetto @mcp-suite/database fornisce un'astrazione per la creazione e gestione
di database SQLite. Offre due funzionalità principali: una factory per creare
connessioni database (createDatabase) e un sistema di migrazioni incrementali
(runMigrations). Usa better-sqlite3 come driver nativo sincrono ad
alte prestazioni.
packages/database/
+-- package.json
+-- tsconfig.json
+-- src/
+-- index.ts # Re-export dei moduli pubblici
+-- connection.ts # createDatabase(), DatabaseOptions
+-- migrations.ts # runMigrations(), Migration
createDatabase(): La Factory del Database
La funzione createDatabase gestisce automaticamente la creazione delle directory,
la configurazione del WAL mode e l'abilitazione delle foreign key.
Supporta anche la modalità in-memory per i test.
export interface DatabaseOptions {
serverName: string; // Nome del server (diventa nome file .db)
dataDir?: string; // Directory custom (default: ~/.mcp-suite/data/)
inMemory?: boolean; // Se true, database in memoria (per test)
}
export function createDatabase(options: DatabaseOptions): Database.Database {
if (options.inMemory) {
return new Database(':memory:');
}
const dataDir = options.dataDir || DEFAULT_DATA_DIR;
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
}
const dbPath = join(dataDir, `${options.serverName}.db`);
const db = new Database(dbPath);
// Configurazione SQLite ottimale
db.pragma('journal_mode = WAL'); // Write-Ahead Logging
db.pragma('foreign_keys = ON'); // Integrita referenziale
return db;
}
perchè WAL Mode?
Il Write-Ahead Logging (WAL) e un journal mode di SQLite che offre vantaggi significativi: letture concorrenti senza blocchi, scritture non bloccanti, migliori performance I/O e crash recovery automatico. I lettori leggono dal file principale mentre lo scrittore scrive nel file WAL, senza interferenze reciproche. Periodicamente un checkpoint sincronizza il WAL nel file principale.
La struttura della directory database di default e ~/.mcp-suite/data/, con un file
.db per ogni server che necessità di persistenza:
~/.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
+-- retrospective-manager.db # Retrospettive
+-- environment-manager.db # Variabili d'ambiente
Sistema di Migrazioni Incrementali
Il sistema di migrazioni permette ai server di evolvere il proprio schema database in modo
incrementale e sicuro. Ogni migrazione ha un numero di versione progressivo, una descrizione
leggibile e l'SQL da eseguire. La tabella _migrations tiene traccia delle
migrazioni già applicate.
export interface Migration {
version: number; // Numero progressivo
description: string; // Descrizione leggibile
up: string; // SQL da eseguire
}
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 già 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 e applica solo le migrazioni nuove
const sorted = [...migrations].sort((a, b) => a.version - b.version);
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);
insertMigration.run(migration.version, migration.description);
}
}
Come i Server Usano il Database
Ogni server che necessità di persistenza definisce le proprie migrazioni e le applica nel costruttore del proprio Store:
// 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,
storyPoints INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'todo',
sprintId INTEGER REFERENCES sprints(id),
createdAt TEXT NOT NULL DEFAULT (datetime('now'))
);
`,
},
];
export class ScrumStore {
private db: Database.Database;
constructor(options?: { inMemory?: boolean }) {
this.db = createDatabase({
serverName: 'scrum-board',
inMemory: options?.inMemory,
});
runMigrations(this.db, migrations);
}
}
@mcp-suite/testing: TestHarness e MockEventBus
Il pacchetto @mcp-suite/testing fornisce utility per testare i server MCP in modo
isolato, senza richiedere un client reale o una connessione di rete. Offre due
componenti principali: un TestHarness basato su InMemoryTransport
dell'SDK MCP e un MockEventBus per verificare l'emissione di eventi nei test.
TestHarness: Test In-Process
Il TestHarness crea una coppia client-server connessa in memoria usando
InMemoryTransport.createLinkedPair(). Il client di test può chiamare tool,
elencare risorse e verificare risultati esattamente come un client reale, ma senza
processi esterni, porte di rete o file STDIO.
export interface TestHarness {
client: Client; // Client MCP collegato al server
close: () => Promise<void>; // Funzione per chiudere la connessione
}
export async function createTestHarness(server: McpServer): Promise<TestHarness> {
// 1. Crea coppia di trasporti collegati in memoria
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
// 2. Crea un client di test
const client = new Client({
name: 'test-client',
version: '1.0.0',
});
// 3. Collega server e client ai rispettivi trasporti
await server.connect(serverTransport);
await client.connect(clientTransport);
// 4. Restituisce client e funzione di cleanup
return {
client,
close: async () => {
await client.close();
await server.close();
},
};
}
Architettura del TestHarness
+-------------------------------------------------------+
| Processo di Test |
| |
| +------------+ InMemoryTransport +------------+ |
| | Client |<--------------------->| Server | |
| | (test) | (collegamento | (MCP) | |
| | | bidirezionale | | |
| | callTool | in memoria) | tools | |
| | listTools | | resources | |
| +------------+ +------------+ |
| |
| Nessun processo esterno |
| Nessuna porta di rete |
| Nessun file STDIO |
+-------------------------------------------------------+
MockEventBus: Verifica degli Eventi nei Test
Il MockEventBus implementa l'interfaccia EventBus registrando tutti
gli eventi pubblicati per permettere asserzioni nei test. Offre metodi di utilita come
wasPublished() e getPublishedEvents().
export class MockEventBus implements EventBus {
public published: PublishedEvent[] = [];
async publish<E extends EventName>(
event: E, payload: EventPayload<E>
): Promise<void> {
this.published.push({ event, payload, timestamp: new Date() });
// Esegue anche gli handler registrati (per testare i subscriber)
}
// Metodi di utilita per le asserzioni
wasPublished(eventName: string): boolean {
return this.published.some((e) => e.event === eventName);
}
getPublishedEvents(eventName?: string): PublishedEvent[] {
if (eventName) {
return this.published.filter((e) => e.event === eventName);
}
return this.published;
}
clear(): void {
this.published = [];
}
}
Esempio Completo di Test con Vitest
Ecco un test completo che usa sia il TestHarness che il MockEventBus per verificare la creazione di uno sprint e l'emissione dell'evento corrispondente:
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestHarness, type TestHarness } from '@mcp-suite/testing';
import { MockEventBus } from '@mcp-suite/testing';
describe('Scrum Board Server', () => {
let harness: TestHarness;
let eventBus: MockEventBus;
beforeAll(async () => {
eventBus = new MockEventBus();
const suite = createScrumBoardServer(eventBus);
harness = await createTestHarness(suite.server);
});
afterAll(async () => {
await harness.close();
});
it('should create a sprint', async () => {
const result = await harness.client.callTool({
name: 'create-sprint',
arguments: {
name: 'Sprint 1',
startDate: '2025-01-13',
endDate: '2025-01-24',
goals: ['Completare autenticazione'],
},
});
expect(result.content).toBeDefined();
expect(result.isError).toBeUndefined();
});
it('should emit sprint-started event', async () => {
expect(eventBus.wasPublished('scrum:sprint-started')).toBe(true);
const events = eventBus.getPublishedEvents('scrum:sprint-started');
expect(events).toHaveLength(1);
expect(events[0].payload).toMatchObject({
name: 'Sprint 1',
startDate: '2025-01-13',
});
});
it('should list tools', async () => {
const tools = await harness.client.listTools();
const toolNames = tools.tools.map(t => t.name);
expect(toolNames).toContain('create-sprint');
expect(toolNames).toContain('sprint-board');
});
});
@mcp-suite/cli: Gestione Server da Terminale
Il pacchetto @mcp-suite/cli fornisce un'interfaccia a riga di comando basata su
Commander.js per gestire i server MCP Suite. Offre tre comandi: list
per elencare tutti i server disponibili, start per avviare un server specifico e
status per verificare quali server sono compilati.
# Elencare tutti i server disponibili
npx @mcp-suite/cli list
# Avviare un server con trasporto STDIO (default)
npx @mcp-suite/cli start scrum-board
# Avviare un server con trasporto HTTP
npx @mcp-suite/cli start scrum-board --transport http
# Verificare lo stato di compilazione
npx @mcp-suite/cli status
Il CLI scansiona la directory servers/ per scoprire automaticamente tutti i server
disponibili e verifica la presenza del file dist/index.js compilato prima di avviare
un server. Il comando start lancia il processo Node.js come sottoprocesso,
inoltrando stdin/stdout/stderr e impostando le variabili d'ambiente appropriate.
npx @mcp-suite/cli status
MCP Suite Status:
Total servers: 22
Built: 20
Not built: 2
Built servers:
+ agile-metrics
+ api-documentation
+ scrum-board
...
Not built:
x docker-compose
x performance-profiler
@mcp-suite/client-manager: Pool di Client per Comunicazione Server-to-Server
Il pacchetto @mcp-suite/client-manager gestisce un pool di client MCP
per la comunicazione sincrona server-to-server. Permette a un server di chiamare tool esposti da
altri server come se fossero funzioni locali, astraendo completamente i dettagli di trasporto.
A differenza dell'EventBus (fire-and-forget, asincrono), il Client Manager offre comunicazione
request/response sincrona.
ServerRegistryEntry e McpClientManager
Il Client Manager funziona con un pattern registry + lazy connection + pool: si registrano i server target, le connessioni vengono create solo al primo utilizzo e vengono riutilizzate nelle chiamate successive.
export interface ServerRegistryEntry {
name: string; // Nome univoco del server target
transport: 'stdio' | 'http' | 'in-memory'; // Tipo di trasporto
command?: string; // Comando per STDIO (es. 'node')
args?: string[]; // Argomenti per STDIO
url?: string; // URL per HTTP (es. 'http://localhost:3018/mcp')
env?: Record<string, string>; // Variabili d'ambiente per STDIO
}
class McpClientManager {
// Registrazione
register(entry: ServerRegistryEntry): void;
registerMany(entries: ServerRegistryEntry[]): void;
// Connessione (lazy)
getClient(serverName: string): Promise<Client>;
// Chiamate RPC
callTool(serverName: string, toolName: string,
args?: Record<string, unknown>): Promise<unknown>;
readResource(serverName: string, uri: string): Promise<unknown>;
// Gestione ciclo vita
disconnect(serverName: string): Promise<void>;
disconnectAll(): Promise<void>;
// Query
getRegisteredServers(): string[];
isConnected(serverName: string): boolean;
}
I 3 Trasporti Supportati
Il Client Manager supporta tre tipi di trasporto, ognuno ottimale per uno scenario diverso:
Trasporti del Client Manager
| Trasporto | Scenario | Dettagli |
|---|---|---|
| HTTP | Server in processi o macchine separate | Usa Streamable HTTP dell'SDK MCP |
| STDIO | Server come sottoprocessi | Comunica via stdin/stdout del processo |
| InMemory | Test e scenari in-process | Usa InMemoryTransport.createLinkedPair() |
// Trasporto HTTP
clientManager.register({
name: 'scrum-board',
transport: 'http',
url: 'http://localhost:3018/mcp',
});
// Trasporto STDIO
clientManager.register({
name: 'scrum-board',
transport: 'stdio',
command: 'node',
args: ['servers/scrum-board/dist/index.js'],
env: { MCP_SUITE_TRANSPORT: 'stdio' },
});
// Trasporto InMemory (per test)
const [clientTransport, serverTransport] = McpClientManager.createInMemoryPair();
await targetServer.connect(serverTransport);
await clientManager.connectInMemoryWithTransport('target-server', clientTransport);
Connessione Lazy e Graceful Degradation
Il Client Manager crea i client in modo lazy: la connessione viene stabilità
solo alla prima chiamata callTool(). Le chiamate successive riutilizzano il client
dalla cache. Se un server target non e raggiungibile, i tool chiamanti usano il pattern di
graceful degradation: continuano a funzionare senza l'arricchimento cross-server.
// Esempio completo: uso in un server
const clientManager = new McpClientManager();
clientManager.registerMany([
{
name: 'scrum-board',
transport: 'http',
url: process.env.MCP_SUITE_SCRUM_BOARD_URL || 'http://localhost:3018/mcp',
},
{
name: 'time-tracking',
transport: 'http',
url: process.env.MCP_SUITE_TIME_TRACKING_URL || 'http://localhost:3022/mcp',
},
]);
// Chiamata cross-server con graceful degradation
if (enrichFromExternal && clientManager) {
try {
const result = await clientManager.callTool('scrum-board', 'get-sprint',
{ sprintId: 1 });
// usa il risultato per arricchire i dati
} catch (error) {
logger.warn('Cross-server call failed, continuing without enrichment');
}
}
// Pulizia alla fine
await clientManager.disconnectAll();
Riepilogo: Come i Pacchetti Collaborano
I 6 pacchetti condivisi formano un'architettura a strati dove ogni server può essere creato, configurato, testato e orchestrato con poche righe di codice:
Ciclo di Vita di un Server MCP
- Creazione:
createMcpServer()da@mcp-suite/corecrea il server con configurazione, logger ed EventBus - Persistenza:
createDatabase()da@mcp-suite/databasecrea il database SQLite con WAL mode - Migrazioni:
runMigrations()applica lo schema incrementale - Eventi:
LocalEventBusda@mcp-suite/event-busabilita la collaborazione fire-and-forget - Cross-server:
McpClientManagerda@mcp-suite/client-managerabilita chiamate RPC sincrone - Avvio:
startServer()avvia con il trasporto appropriato - Testing:
createTestHarness()da@mcp-suite/testingpermette test in-process - Gestione:
@mcp-suite/cligestisce i server dal terminale
Conclusioni
I pacchetti condivisi rappresentano il cuore architetturale di Tech-MCP. Senza di essi, ogni
server dovrebbe reimplementare la factory, il logger, la gestione database, il sistema eventi
e la comunicazione cross-server - moltiplicando la complessità per 22.
Grazie a questa architettura, creare un nuovo server e questione di poche righe: basta importare
da @mcp-suite/core, definire le migrazioni con @mcp-suite/database
e registrare i tool.
Nel prossimo articolo passeremo dai pacchetti condivisi ai server concreti, esplorando i server di produttività: code-review per l'analisi del codice, dependency-manager per la gestione delle dipendenze e project-scaffolding per lo scaffolding automatico di nuovi progetti. Vedremo come questi server sfruttano l'infrastruttura dei pacchetti condivisi per offrire funzionalità avanzate con codice minimale.
Il codice completo di tutti i pacchetti condivisi e disponibile nel repository Tech-MCP su GitHub.

Commenti
Caricamento commenti...
Accedi per commentare