Introduzione: Dalla Scrittura del Codice alla Produzione
Nei dodici articoli precedenti di questa serie abbiamo costruito un ecosistema MCP completo: server con tool, risorse, persistenza SQLite, comunicazione inter-server tramite EventBus e ClientManager. Ma un server MCP professionale non e completo senza una strategia di testing rigorosa e una preparazione adeguata per la produzione.
Testare un server MCP presenta sfide specifiche: i tool comunicano attraverso il protocollo JSON-RPC,
gli eventi viaggiano tramite bus asincroni, e la comunicazione cross-server coinvolge trasporti in-memory.
Il pacchetto @mcp-suite/testing del progetto
Tech-MCP risolve queste
complessità fornendo utility dedicate come TestHarness, InMemoryTransport e
MockEventBus.
Cosa Imparerai in Questo Articolo
- La piramide dei test MCP: unit, integration e wiring test
- Come testare lo store in isolamento con database in-memory
- Il pacchetto
@mcp-suite/testing: TestHarness, InMemoryTransport, MockEventBus - Integration test end-to-end dei tool con
createTestHarness() - Verifica degli eventi pubblicati con
MockEventBus - Wiring test per la comunicazione cross-server
- Best practice di sicurezza: validazione input, prepared statement, rate limiting
- Performance: WAL mode SQLite, indici, transazioni batch
- Deploy in produzione: logging strutturato, graceful shutdown, Docker
La Piramide dei Test MCP
Un server MCP professionale richiede tre livelli di testing, organizzati in una piramide che bilancia velocità di esecuzione e copertura funzionale. Alla base troviamo i test più veloci e numerosi, al vertice quelli più complessi ma meno frequenti:
/\
/ \
/ W \ Wiring Test
/ I R \ (cross-server con InMemoryTransport)
/ I I \
/ N N G \
/────────────\
/ INTEGRATION \ Tool Test
/ (test harness) \ (tool → store → risultato)
/──────────────────\
/ \
/ UNIT TEST \ Store Test
/ (store in-memory) \ (logica pura, nessun MCP)
/──────────────────────────\
I Tre Livelli di Test MCP
| Livello | Cosa Testa | Velocita | Setup Richiesto |
|---|---|---|---|
| Unit | Metodi dello store (logica di persistenza) | Velocissimo | Solo store in-memory |
| Integration | Tool end-to-end tramite protocollo MCP | Veloce | TestHarness + InMemoryTransport |
| Wiring | Comunicazione cross-server reale | Medio | Più server + ClientManager |
Unit Test dello Store
Il primo livello della piramide testa la logica di persistenza in isolamento, senza coinvolgere
il protocollo MCP. Lo store viene istanziato con un database in-memory (inMemory: true), garantendo
che ogni test parta da uno stato pulito e che l'esecuzione sia estremamente veloce.
Gli unit test dello store verificano tutte le operazioni CRUD (Create, Read, Update, Delete), i vincoli di integrita (UNIQUE constraints), i filtri, le ricerche e la serializzazione/deserializzazione dei dati JSON.
import { describe, it, expect, beforeEach } from "vitest";
import { NotesStore } from "../../src/services/notes-store.js";
describe("NotesStore", () => {
let store: NotesStore;
beforeEach(() => {
// Ogni test ha un database fresco in-memory
store = new NotesStore({ inMemory: true });
});
it("should add and retrieve a note", () => {
const note = store.addNote({
title: "Test",
content: "Contenuto di test",
tags: ["tag1", "tag2"],
});
expect(note.id).toBe(1);
expect(note.title).toBe("Test");
expect(note.tags).toEqual(["tag1", "tag2"]);
const retrieved = store.getNote(1);
expect(retrieved).toEqual(note);
});
it("should return undefined for non-existent note", () => {
const note = store.getNote(999);
expect(note).toBeUndefined();
});
it("should list notes ordered by updatedAt desc", () => {
store.addNote({ title: "Prima", content: "A" });
store.addNote({ title: "Seconda", content: "B" });
store.addNote({ title: "Terza", content: "C" });
const notes = store.listNotes();
expect(notes).toHaveLength(3);
expect(notes[0].title).toBe("Terza");
});
it("should handle UNIQUE constraint on title", () => {
store.addNote({ title: "Unico", content: "A" });
expect(() => store.addNote({ title: "Unico", content: "B" }))
.toThrow();
});
it("should search notes by content", () => {
store.addNote({ title: "JS", content: "Arrow functions e closures" });
store.addNote({ title: "TS", content: "Tipi generici e interfacce" });
const results = store.searchNotes("functions");
expect(results).toHaveLength(1);
expect(results[0].title).toBe("JS");
});
it("should delete a note and return true", () => {
store.addNote({ title: "Da cancellare", content: "X" });
expect(store.deleteNote(1)).toBe(true);
expect(store.getNote(1)).toBeUndefined();
});
});
Best Practice per Unit Test dello Store
- Usa
beforeEachconinMemory: trueper garantire isolamento totale tra i test - Testa l'intero ciclo CRUD: create, read, update, delete
- Verifica gli edge case: record inesistente, constraint violati, filtri vuoti
- Testa la serializzazione/deserializzazione JSON (array di tag, oggetti annidati)
- Non testare le query SQL direttamente: testa il comportamento osservabile dello store
Integration Test dei Tool con TestHarness
Il secondo livello della piramide testa i tool end-to-end attraverso il protocollo MCP.
Il pacchetto @mcp-suite/testing fornisce la funzione createTestHarness()
che crea una coppia client-server collegata in-memory, permettendo di invocare i tool esattamente
come farebbe un client MCP reale.
Come Funziona createTestHarness()
La funzione crea un InMemoryTransport pair, collega il server e il client, e restituisce
un oggetto TestHarness con il client pronto per invocare i tool:
export async function createTestHarness(server: McpServer): Promise<TestHarness> {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "test-client", version: "1.0.0" });
await server.connect(serverTransport); // Server PRIMA
await client.connect(clientTransport); // Client DOPO
return {
client,
close: async () => {
await client.close();
await server.close();
},
};
}
L'ordine di connessione e fondamentale: il server deve connettersi prima del client. Questo perchè il server deve essere pronto a gestire la negoziazione delle capabilities che il client avvia immediatamente dopo la connessione.
Test Completo di un Tool
Ecco un esempio completo che testa il tool add-note verificando il formato della risposta MCP,
il contenuto del risultato e la gestione degli errori:
import { describe, it, expect, afterEach } from "vitest";
import { createTestHarness, MockEventBus, type TestHarness } from "@mcp-suite/testing";
import { createNotesServer } from "../../src/server.js";
describe("add-note tool", () => {
let harness: TestHarness;
afterEach(async () => {
if (harness) await harness.close();
});
it("should add a note and return it as JSON", async () => {
const { server } = createNotesServer({ storeOptions: { inMemory: true } });
harness = await createTestHarness(server);
const result = await harness.client.callTool({
name: "add-note",
arguments: {
title: "Test Note",
content: "Hello World",
tags: ["test"],
},
});
// Verifica formato risultato MCP
const content = result.content as Array<{ type: string; text: string }>;
expect(content[0].type).toBe("text");
// Verifica contenuto
const note = JSON.parse(content[0].text);
expect(note.title).toBe("Test Note");
expect(note.content).toBe("Hello World");
expect(note.tags).toEqual(["test"]);
expect(note.id).toBeDefined();
});
it("should return error for duplicate title", async () => {
const { server } = createNotesServer({ storeOptions: { inMemory: true } });
harness = await createTestHarness(server);
await harness.client.callTool({
name: "add-note",
arguments: { title: "Duplicato", content: "Primo" },
});
const result = await harness.client.callTool({
name: "add-note",
arguments: { title: "Duplicato", content: "Secondo" },
});
expect(result.isError).toBe(true);
});
});
MockEventBus: Verifica della Pubblicazione Eventi
Il MockEventBus e una implementazione di test dell'interfaccia EventBus che
registra tutti gli eventi pubblicati senza propagarli. Fornisce due metodi chiave per le asserzioni:
wasPublished(eventName): verifica se un evento specifico e stato pubblicatogetPublishedEvents(eventName): restituisce l'array di tutti gli eventi pubblicati con quel nome, incluso il payload
it("should publish event when note is added", async () => {
const eventBus = new MockEventBus();
const { server } = createNotesServer({
eventBus,
storeOptions: { inMemory: true },
});
harness = await createTestHarness(server);
await harness.client.callTool({
name: "add-note",
arguments: { title: "Evento", content: "Test" },
});
// Verifica che l'evento sia stato pubblicato
expect(eventBus.wasPublished("notes:created")).toBe(true);
// Verifica il payload dell'evento
const events = eventBus.getPublishedEvents("notes:created");
expect(events[0].payload).toMatchObject({ title: "Evento" });
});
Pattern di Test con MockEventBus
Il MockEventBus permette di verificare che i tool pubblichino gli eventi corretti senza bisogno di configurare un bus reale o sottoscrivere handler. Questo approccio isola il test del tool dalla logica di collaborazione, seguendo il principio di responsabilità singola anche nei test.
Wiring Test Cross-Server
Il terzo livello della piramide testa la comunicazione reale tra server MCP.
I wiring test verificano che un server possa invocare tool su un altro server attraverso
il McpClientManager, simulando uno scenario di produzione completo.
import { describe, it, expect, afterEach } from "vitest";
import { createTestHarness, type TestHarness } from "@mcp-suite/testing";
import { McpClientManager } from "@mcp-suite/client-manager";
import { createInsightEngineServer } from "../../src/server.js";
import { createAgileMetricsServer } from "../../../agile-metrics/src/server.js";
describe("insight-engine -> agile-metrics wiring", () => {
let callerHarness: TestHarness;
let clientManager: McpClientManager;
afterEach(async () => {
if (callerHarness) await callerHarness.close();
if (clientManager) await clientManager.disconnectAll();
});
it("should fetch velocity from agile-metrics", async () => {
// 1. Crea il server target (quello che viene chiamato)
const targetSuite = createAgileMetricsServer({
storeOptions: { inMemory: true },
});
// 2. Configura la connessione in-memory tramite ClientManager
clientManager = new McpClientManager();
const [ct, st] = McpClientManager.createInMemoryPair();
await targetSuite.server.connect(st);
await clientManager.connectInMemoryWithTransport("agile-metrics", ct);
// 3. Crea il server caller (quello che chiama)
const callerSuite = createInsightEngineServer({
clientManager,
storeOptions: { inMemory: true },
});
callerHarness = await createTestHarness(callerSuite.server);
// 4. Invoca il tool che effettua chiamata cross-server
const result = await callerHarness.client.callTool({
name: "health-dashboard",
arguments: {},
});
// 5. Verifica il risultato
const content = result.content as Array<{ type: string; text: string }>;
const dashboard = JSON.parse(content[0].text);
expect(dashboard.dataSources["agile-metrics"]).toBe("available");
});
});
Struttura di un Wiring Test
Ogni wiring test segue un flusso preciso in 5 fasi:
- Crea il server target in-memory: il server che verrà chiamato dal caller
- Configura il transport: crea un
InMemoryTransportpair e collega il target al ClientManager - Crea il server caller con il ClientManager: il server che effettua la chiamata cross-server
- Crea il TestHarness per il caller: permette di invocare i tool del caller come se fosse un client MCP
- Verifica il risultato: assicura che i dati del server target siano stati recuperati correttamente
Organizzazione dei File di Test
Nel progetto Tech-MCP, i file di test seguono una struttura standardizzata per ogni server:
servers/my-server/
tests/
services/
my-store.test.ts # Unit test dello store
tools/
add-item.test.ts # Integration test del tool
get-stats.test.ts # Integration test del tool
get-stats-wiring.test.ts # Wiring test cross-server
server.test.ts # Test della factory (opzionale)
La configurazione di Vitest e minimale e condivisa tramite il workspace:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
include: ["tests/**/*.test.ts"],
},
});
Best Practice di Sicurezza
Portare un server MCP in produzione richiede attenzione alla sicurezza su più livelli. Ogni tool e un potenziale punto di ingresso per input malevoli, e ogni endpoint HTTP esposto necessità di protezione adeguata.
Validazione Input con Zod
L'SDK MCP gestisce automaticamente la validazione degli argomenti tramite gli schemi Zod dichiarati nella definizione del tool. Usare vincoli precisi e la prima linea di difesa:
import { z } from "zod";
server.tool(
"create-project",
"Crea un nuovo progetto",
{
name: z.string().min(1).max(100),
budget: z.number().positive().optional(),
priority: z.enum(["low", "medium", "high"]).default("medium"),
tags: z.array(z.string()).max(10).optional(),
startDate: z.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Formato: YYYY-MM-DD")
.optional(),
},
async (args) => {
// args e già validato e tipizzato da Zod
// ...
},
);
Prepared Statement e SQL Injection
Lo store non deve mai eseguire SQL costruito da stringhe concatenate. La libreria better-sqlite3
usa prepared statement per default, eliminando il rischio di SQL injection:
// CORRETTO: prepared statement con parametri posizionali
const stmt = this.db.prepare("SELECT * FROM items WHERE title = ?");
const item = stmt.get(title);
// SBAGLIATO: SQL injection vulnerabile
const item = this.db.exec(`SELECT * FROM items WHERE title = '






