Introduzione: Organizzare un Progetto MCP su Larga Scala
Quando si sviluppa un singolo server MCP, la struttura del progetto e semplice: un package.json,
qualche file TypeScript, e si e pronti a partire. Ma quando il numero di server cresce, la complessità
organizzativa esplode. Come condividere codice comune senza pubblicare su npm? Come garantire che tutti i
server si compilino nell'ordine corretto? Come mantenere coerenza nella struttura e nei pattern?
La risposta a queste domande e il monorepo: un singolo repository che contiene tutti i server e i pacchetti condivisi. In questo articolo analizziamo l'architettura monorepo del progetto Tech-MCP, le scelte tecnologiche alla base (TypeScript, SQLite, STDIO/HTTP) e il pattern a 4 strati che ogni server segue per garantire uniformita e manutenibilità.
Cosa Imparerai in Questo Articolo
- Come strutturare un monorepo MCP con pnpm workspaces e Turborepo
- La directory structure completa con
packages/eservers/ - perchè TypeScript, SQLite e STDIO/HTTP sono le scelte ottimali per server MCP
- Il pattern architetturale a 4 strati:
index.ts,server.ts,tools/,services/ - L'interfaccia
McpSuiteServere la factorycreateMcpServer() - La pipeline di build con Turborepo e il grafo delle dipendenze
perchè un Monorepo per Server MCP?
Un monorepo raccoglie tutti i componenti di un progetto in un unico repository Git. Per una suite di server MCP, i vantaggi sono significativi:
-
Condivisione codice senza pubblicazione: i pacchetti in
packages/sono disponibili a tutti i server tramite il protocolloworkspace:di pnpm, senza bisogno di pubblicarli su npm - Build atomiche e ordinate: Turborepo analizza il grafo delle dipendenze e compila i pacchetti nell'ordine corretto, parallelizzando dove possibile
- Versioning unificato: tutti i pacchetti e server si evolvono insieme, eliminando problemi di compatibilità tra versioni
-
Developer Experience superiore: un singolo
pnpm installe un singolopnpm buildper l'intero progetto -
Refactoring sicuro: modificare un'interfaccia in
@mcp-suite/coreevidenzia immediatamente tutti i server che devono essere aggiornati
Struttura Completa del Monorepo
Il progetto Tech-MCP e organizzato in tre directory principali: la radice con i file di configurazione,
packages/ per le librerie condivise e servers/ per i server MCP indipendenti.
mcp-suite/
├── package.json # Root: script globali, engine constraints
├── pnpm-workspace.yaml # Definisce packages/* e servers/* come workspace
├── turbo.json # Pipeline di build con Turborepo
├── tsconfig.base.json # Configurazione TypeScript condivisa
│
├── packages/ # Librerie condivise (6 pacchetti)
│ ├── core/ # Factory server, config, logger, errori, tipi
│ ├── event-bus/ # EventBus tipizzato con 29 eventi
│ ├── database/ # Connessione SQLite + migrazioni
│ ├── testing/ # Harness di test + MockEventBus
│ ├── cli/ # CLI per gestire i server
│ └── client-manager/ # Pool di client MCP per comunicazione server-to-server
│
├── servers/ # 22 MCP server indipendenti
│ ├── scrum-board/ # Gestione sprint, storie, task
│ ├── standup-notes/ # Note per gli standup giornalieri
│ ├── time-tracking/ # Tracciamento tempo
│ ├── agile-metrics/ # Metriche agili (velocity, burndown)
│ ├── code-review/ # Analisi e review del codice
│ ├── test-generator/ # Generazione test automatici
│ ├── cicd-monitor/ # Monitoraggio pipeline CI/CD
│ ├── docker-compose/ # Gestione Docker Compose
│ ├── db-schema-explorer/ # Esplorazione schemi database
│ ├── dependency-manager/ # Gestione dipendenze progetto
│ ├── api-documentation/ # Documentazione API
│ ├── codebase-knowledge/ # Knowledge base del codice
│ ├── data-mock-generator/ # Generazione dati mock
│ ├── environment-manager/ # Gestione variabili d'ambiente
│ ├── http-client/ # Client HTTP per test API
│ ├── log-analyzer/ # Analisi log applicativi
│ ├── performance-profiler/ # Profilazione performance
│ ├── project-economics/ # Economia di progetto (budget, costi)
│ ├── project-scaffolding/ # Scaffolding nuovi progetti
│ ├── regex-builder/ # Costruzione espressioni regolari
│ ├── retrospective-manager/# Gestione retrospettive agili
│ └── snippet-manager/ # Libreria di snippet di codice
│
└── docs/ # Documentazione del progetto
Questa struttura realizza il principio di indipendenza dei server: ogni server e una cartella
autonoma con il proprio package.json, tsconfig.json e codice sorgente. Allo stesso tempo,
la collaborazione opzionale e garantita dai pacchetti condivisi in packages/.
I 6 Pacchetti Condivisi
I pacchetti in packages/ forniscono le fondamenta comuni a tutti i server:
Pacchetti Condivisi e le Loro Responsabilità
| Pacchetto | Responsabilità | Dipendenze |
|---|---|---|
@mcp-suite/core |
Factory server, configurazione, logger, errori, tipi condivisi | event-bus |
@mcp-suite/event-bus |
EventBus tipizzato con 29 eventi e pattern pub/sub | Nessuna |
@mcp-suite/database |
Connessione SQLite con WAL mode e sistema di migrazioni | core |
@mcp-suite/testing |
Harness di test e MockEventBus per unit testing | core, event-bus |
@mcp-suite/cli |
CLI per gestire, avviare e monitorare i server | core, event-bus, client-manager |
@mcp-suite/client-manager |
Pool di client MCP per comunicazione server-to-server | core |
Decisioni Progettuali: perchè TypeScript, SQLite e STDIO
Le tecnologie alla base del progetto non sono state scelte casualmente. Ogni decisione risponde a requisiti specifici di un sistema MCP distribuito.
perchè TypeScript?
TypeScript e il linguaggio ideale per server MCP per quattro ragioni fondamentali:
-
Type-safety end-to-end: i tipi definiti in
@mcp-suite/coresono condivisi tra tutti i server, garantendo coerenza a compile-time. Un cambio di interfaccia si propaga immediatamente come errore in tutti i server che la utilizzano -
ESM nativo: target ES2022 con
"module": "Node16"per compatibilità nativa con Node.js moderno, senza bisogno di bundler o transpiler aggiuntivi -
Declaration maps: ogni pacchetto genera
.d.tse.d.ts.map, permettendo il "Go to Definition" attraverso tutto il monorepo direttamente nell'IDE -
Strict mode:
"strict": trueintsconfig.base.jsonper massima sicurezza sui tipi e prevenzione di errori runtime
perchè SQLite (better-sqlite3)?
Per la persistenza locale di ogni server, SQLite e la scelta ottimale:
-
Zero configurazione: nessun server database da installare o gestire. Il file
.dbviene creato automaticamente alla prima esecuzione -
File-based: ogni server ha il proprio file database in
~/.mcp-suite/data/, completamente indipendente dagli altri server -
Sincrono:
better-sqlite3usa binding C++ sincroni, ideale per operazioni locali veloci senza la complessità delle Promise per operazioni di I/O locale - WAL mode: Write-Ahead Logging abilitato per performance ottimali in lettura concorrente, fondamentale quando più tool accedono contemporaneamente allo store
- Portabile: il database si sposta semplicemente copiando il file, facilitando backup e migrazione
Trasporti: STDIO e HTTP
MCP Suite supporta due trasporti, selezionabili via variabile d'ambiente MCP_SUITE_TRANSPORT:
Confronto tra Trasporti MCP
| Caratteristica | STDIO (default) | HTTP (Streamable HTTP) |
|---|---|---|
| Caso d'uso | Uso locale con Claude Desktop, Cursor, VS Code | Deployment remoti, comunicazione inter-server |
| Configurazione | Nessuna porta, nessun conflitto di rete | Porta dedicata per ogni server |
| Sicurezza | Comunicazione locale, nessuna esposizione | Protocollo stateful con session UUID |
| Scalabilità | Un processo per connessione | Container, scaling orizzontale |
| Route | stdin/stdout | POST/GET/DELETE /mcp + GET /health |
STDIO e il trasporto predefinito, ideale per lo sviluppo locale. HTTP diventa necessario quando i server devono comunicare tra loro tramite il Client Manager o quando si effettua il deploy su server remoti.
Configurazione del Monorepo
Tre file nella directory radice definiscono la struttura del monorepo: il workspace pnpm,
la pipeline Turborepo e il package.json root.
pnpm-workspace.yaml
Questo file dichiara quali directory contengono i workspace del monorepo:
# pnpm-workspace.yaml
packages:
- "packages/*"
- "servers/*"
Con questa configurazione, pnpm tratta ogni sottocartella di packages/ e servers/
come un workspace indipendente. Le dipendenze interne vengono risolte tramite il protocollo
workspace:* invece di cercarle su npm.
turbo.json
Turborepo gestisce la pipeline di build, definendo l'ordine di compilazione e le dipendenze tra task:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"clean": {
"cache": false
}
}
}
La direttiva chiave e "dependsOn": ["^build"]. Il simbolo ^ indica che il task
build di ogni workspace dipende dal build delle sue dipendenze interne. Questo
garantisce che @mcp-suite/core venga compilato prima di qualsiasi server che lo utilizza.
package.json del Server con Dipendenze Interne
Ogni server dichiara le proprie dipendenze, sia esterne (npm) sia interne (workspace):
{
"name": "@mcp-suite/scrum-board",
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"clean": "rm -rf dist"
},
"dependencies": {
"@mcp-suite/core": "workspace:*",
"@mcp-suite/event-bus": "workspace:*",
"@mcp-suite/database": "workspace:*",
"@modelcontextprotocol/sdk": "^1.0.0",
"better-sqlite3": "^11.0.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.0",
"typescript": "^5.7.0"
}
}
La sintassi "workspace:*" indica a pnpm di risolvere la dipendenza dal workspace locale
invece di cercarla su npm. Questo significa che le modifiche ai pacchetti condivisi sono immediatamente
disponibili a tutti i server dopo una ricompilazione.
La Pipeline di Build
Quando si esegue pnpm build dalla radice del monorepo, Turborepo analizza il grafo delle
dipendenze e pianifica la build ottimale:
pnpm build
│
├── @mcp-suite/event-bus (nessuna dipendenza interna)
├── @mcp-suite/core (dipende da event-bus)
├── @mcp-suite/database (dipende da core)
├── @mcp-suite/testing (dipende da core, event-bus)
├── @mcp-suite/client-manager (dipende da core)
├── @mcp-suite/cli (dipende da core, event-bus, client-manager)
│
└── servers/* (tutti dipendono da core, event-bus, database)
├── scrum-board
├── standup-notes
├── time-tracking
└── ... (altri 19 server compilati in parallelo)
Turborepo compila prima event-bus (nessuna dipendenza), poi core (dipende da event-bus),
poi database e gli altri pacchetti, e infine tutti i server in parallelo. La cache integrata
di Turborepo salta la ricompilazione dei pacchetti che non sono cambiati, rendendo le build incrementali
estremamente veloci.
Il Pattern Server a 4 Strati
Ogni server MCP nella suite segue un pattern architetturale rigoroso a 4 strati. Questo pattern garantisce uniformita, manutenibilità e testabilità in tutti i 22 server. Non si tratta di una convenzione opzionale, ma di un contratto architetturale: ogni nuovo server deve aderire a questa struttura.
servers/nome-server/
├── package.json # Dipendenze e script
├── tsconfig.json # Estende tsconfig.base.json
└── src/
├── index.ts # Strato 1: Entry point, bootstrap
├── server.ts # Strato 2: Factory, registrazione tool
├── tools/ # Strato 3: Un file per tool
│ ├── create-sprint.ts
│ ├── get-sprint.ts
│ └── ...
├── services/ # Strato 4: Store SQLite, logica di business
│ └── scrum-store.ts
└── collaboration.ts # Handler eventi cross-server (opzionale)
I 4 Strati e le Loro Responsabilità
| Strato | File | Responsabilità | Dipendenze |
|---|---|---|---|
| 1. Entry Point | index.ts |
Bootstrap: crea EventBus, chiama factory, avvia trasporto | @mcp-suite/core, @mcp-suite/event-bus |
| 2. Factory | server.ts |
Crea McpSuiteServer, istanzia store, registra tool |
@mcp-suite/core, services, tools |
| 3. Tools | tools/*.ts |
Definizione singolo tool: schema Zod + handler async | @modelcontextprotocol/sdk, services |
| 4. Services | services/*.ts |
Persistenza SQLite, migrazioni, logica di dominio | @mcp-suite/database |
Strato 1: Entry Point (index.ts)
L'entry point e il file più semplice dell'intero server. Ha esattamente tre responsabilità:
creare un'istanza di LocalEventBus, chiamare la factory del server, e avviare il trasporto.
#!/usr/bin/env node
import { startServer } from '@mcp-suite/core';
import { LocalEventBus } from '@mcp-suite/event-bus';
import { createScrumBoardServer } from './server.js';
const eventBus = new LocalEventBus();
const suite = createScrumBoardServer(eventBus);
startServer(suite).catch((error) => {
console.error('Failed to start scrum-board server:', error);
process.exit(1);
});
Lo shebang #!/usr/bin/env node permette l'esecuzione diretta come binario.
L'EventBus viene creato qui e iniettato nel server tramite Dependency Injection.
La funzione startServer() sceglie automaticamente il trasporto (STDIO o HTTP) in base
alla configurazione. Gli errori fatali terminano il processo con process.exit(1).
Strato 2: Factory (server.ts)
La factory e il cuore del server. Crea l'oggetto McpSuiteServer, istanzia i servizi di persistenza,
registra tutti i tool e configura la collaborazione con altri server.
import { createMcpServer, type McpSuiteServer, type EventBus } from '@mcp-suite/core';
import { ScrumStore } from './services/scrum-store.js';
import { registerCreateSprint } from './tools/create-sprint.js';
import { registerGetSprint } from './tools/get-sprint.js';
import { registerCreateStory } from './tools/create-story.js';
import { registerCreateTask } from './tools/create-task.js';
import { registerUpdateTaskStatus } from './tools/update-task-status.js';
import { registerSprintBoard } from './tools/sprint-board.js';
import { registerGetBacklog } from './tools/get-backlog.js';
import { setupCollaborationHandlers } from './collaboration.js';
export function createScrumBoardServer(eventBus?: EventBus): McpSuiteServer {
const suite = createMcpServer({
name: 'scrum-board',
version: '0.1.0',
description: 'MCP server for managing sprints, user stories, tasks',
eventBus,
});
const store = new ScrumStore();
// Registra tutti i tool
registerCreateSprint(suite.server, store, suite.eventBus);
registerGetSprint(suite.server, store);
registerCreateStory(suite.server, store);
registerCreateTask(suite.server, store);
registerUpdateTaskStatus(suite.server, store, suite.eventBus);
registerSprintBoard(suite.server, store);
registerGetBacklog(suite.server, store);
// Collaborazione cross-server (solo se EventBus presente)
if (suite.eventBus) {
setupCollaborationHandlers(suite.eventBus, store);
}
suite.logger.info('All scrum-board tools registered');
return suite;
}
Notare come createMcpServer() restituisce un oggetto McpSuiteServer già configurato.
Lo store viene creato una sola volta e condiviso tra tutti i tool tramite le funzioni di registrazione.
L'eventBus e opzionale: se non viene passato, il server funziona in modalità standalone.
Strato 3: Tool Registration (tools/)
Ogni tool e un file separato che esporta una funzione registerXxx(). Questa funzione riceve
il McpServer, lo store e opzionalmente l'EventBus, e registra un singolo tool
con il suo schema di validazione Zod e il handler asincrono.
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import type { EventBus } from '@mcp-suite/core';
import type { ScrumStore } from '../services/scrum-store.js';
export function registerCreateSprint(
server: McpServer,
store: ScrumStore,
eventBus?: EventBus,
): void {
server.tool(
'create-sprint', // Nome del tool
'Create a new sprint with a name, date range, and goals', // Descrizione per l'LLM
{ // Schema Zod
name: z.string().describe('Sprint name (e.g. "Sprint 12")'),
startDate: z.string().describe('Start date ISO (YYYY-MM-DD)'),
endDate: z.string().describe('End date ISO (YYYY-MM-DD)'),
goals: z.array(z.string()).describe('Sprint goals'),
},
async ({ name, startDate, endDate, goals }) => { // Handler
try {
const sprint = store.createSprint({ name, startDate, endDate, goals });
// Pubblicazione evento fire-and-forget
eventBus?.publish('scrum:sprint-started', {
sprintId: String(sprint.id),
name: sprint.name,
startDate: sprint.startDate,
endDate: sprint.endDate,
});
return {
content: [{ type: 'text' as const, text: JSON.stringify(sprint, null, 2) }],
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `Failed to create sprint: 






