Salta al contenuto principale

12 - Collaborazione Inter-Server e EventBus

Come i server MCP comunicano: EventBus con 29 eventi tipizzati, ClientManager per invocazione cross-server, matrice di collaborazione.

Cosa ne pensi di questo articolo?

12 - Collaborazione Inter-Server e EventBus
MCPFoto:Harry TuckerPexels

In Breve

Come i server MCP comunicano: EventBus con 29 eventi tipizzati, ClientManager per invocazione cross-server, matrice di collaborazione.

Introduzione: Come Comunicano i Server MCP

In un ecosistema con 22 server MCP attivi, la collaborazione tra server diventa essenziale. Un singolo server gestisce un dominio specifico (scrum, time-tracking, CI/CD), ma i flussi di lavoro reali attraversano più domini: quando uno sprint si chiude, le metriche devono aggiornarsi, i costi devono calcolarsi, la retrospettiva deve crearsi automaticamente.

Tech-MCP implementa due meccanismi complementari per la comunicazione inter-server: l'EventBus (pub/sub asincrono fire-and-forget) e il ClientManager (chiamate RPC sincrone). Insieme, permettono di costruire flussi end-to-end che attraversano l'intera suite senza accoppiamento diretto tra i server.

Cosa Imparerai in Questo Articolo

  • L'architettura dell'EventBus e il pattern Publish/Subscribe in MCP Suite
  • I 29 eventi tipizzati organizzati in 11 domini
  • La matrice di collaborazione: chi pubblica, chi sottoscrive
  • Il ClientManager per chiamate RPC sincrone tra server
  • La classificazione dei server: bidirezionali, publisher-only, subscriber-only, pass-through
  • Flussi end-to-end: ciclo di vita sprint, tempo e costi, DevOps, quality
  • Pattern matching con micromatch per sottoscrizioni wildcard

EventBus: Il Cuore della Collaborazione Asincrona

L'EventBus e il meccanismo che permette ai server MCP Suite di collaborare in modo asincrono e disaccoppiato. Quando un server esegue un'azione significativa (creazione sprint, log tempo, completamento build), pubblica un evento tipizzato che altri server possono sottoscrivere per reagire automaticamente.


Server A                    EventBus                    Server B
   |                           |                           |
   |-- publish(evento) ------->|                           |
   |                           |-- handler(payload) ------>|
   |                           |                           |
   |   (fire-and-forget)       |   (delivery garantita     |
   |                           |    in-process)            |
    

Questo pattern e noto come Publish/Subscribe (Pub/Sub) e offre vantaggi fondamentali:

  • Disaccoppiamento: il publisher non conosce i subscriber
  • Estensibilita: aggiungere un nuovo subscriber non richiede modifiche al publisher
  • Opzionalita: l'EventBus e sempre opzionale; ogni server funziona perfettamente anche senza

Interfaccia EventBus

L'interfaccia EventBus definisce il contratto per qualsiasi implementazione del bus eventi:


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;
}
    

Metodi dell'EventBus

Metodo Descrizione
publish Pubblica un evento tipizzato con il suo payload
subscribe Sottoscrive a un evento specifico. Ritorna una funzione di unsubscribe
subscribePattern Sottoscrive a eventi che matchano un pattern glob (es. scrum:*)
clear Rimuove tutte le sottoscrizioni

LocalEventBus: Implementazione In-Process

La classe LocalEventBus e l'implementazione predefinita, basata su Node.js EventEmitter con supporto pattern via micromatch:


import { EventEmitter } from "node:events";
import micromatch from "micromatch";

interface PatternSubscription {
  pattern: string;
  handler: PatternHandler;
}

export class LocalEventBus implements EventBus {
  private emitter = new EventEmitter();
  private patternSubs: PatternSubscription[] = [];

  constructor() {
    this.emitter.setMaxListeners(100);
  }

  async publish(event: string, payload: unknown): Promise<void> {
    // Notifica subscriber esatti
    this.emitter.emit(event, payload);

    // Notifica subscriber pattern
    for (const sub of this.patternSubs) {
      if (micromatch.isMatch(event, sub.pattern)) {
        try {
          await sub.handler(event, payload);
        } catch {
          // Errori nei subscriber non bloccano la pubblicazione
        }
      }
    }
  }

  subscribe(event: string, handler: EventHandler): () => void {
    this.emitter.on(event, handler);
    return () => this.emitter.off(event, handler);
  }

  subscribePattern(pattern: string, handler: PatternHandler): () => void {
    const sub: PatternSubscription = { 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 = [];
  }
}
    

Caratteristiche chiave: max 100 listener per evento, pattern matching via micromatch per wildcard (scrum:*, *:completed), errori nei pattern handler catturati silenziosamente per non interrompere la pubblicazione, e zero dipendenze esterne oltre micromatch.

Tipizzazione Completa con EventMap

Ogni evento e tipizzato sia nel nome che nel payload grazie alla mappa EventMap. Il compilatore TypeScript verifica a compile-time che il payload sia corretto:


// Il compilatore TypeScript verifica che il payload sia corretto
eventBus.publish('scrum:sprint-started', {
  sprintId: '42',
  name: 'Sprint 15',
  startDate: '2025-01-01',
  endDate: '2025-01-14',
});

// Errore di compilazione: manca il campo 'name'
eventBus.publish('scrum:sprint-started', {
  sprintId: '42',
});
    

I 29 Eventi Tipizzati in 11 Domini

MCP Suite definisce 29 eventi tipizzati organizzati in 11 domini. Ogni evento segue il formato dominio:azione-in-kebab-case e possiede un payload TypeScript fortemente tipizzato.

Mappa dei Domini

Dominio N. Eventi Area Funzionale
code:* 3 Codice e Git
cicd:* 2 CI/CD
scrum:* 4 Project Management
time:* 2 Time Tracking
db:* 2 Database
test:* 2 Testing
docs:* 2 Documentazione
perf:* 2 Performance
retro:* 2 Retrospettive
economics:* 2 Economia di Progetto
standup:* 1 Standup
Totale 29

Riferimento Completo degli Eventi

Tutti i 29 Eventi con Server e Trigger

Dominio Evento Publisher Trigger (Tool)
code code:commit-analyzed code-review analyze-diff
code:review-completed code-review suggest-improvements
code:dependency-alert dependency-manager check-vulnerabilities
cicd cicd:pipeline-completed cicd-monitor get-pipeline-status
cicd:build-failed cicd-monitor get-pipeline-status
scrum scrum:sprint-started scrum-board create-sprint
scrum:sprint-completed scrum-board close-sprint
scrum:task-updated scrum-board update-task-status
scrum:story-completed scrum-board close-sprint
time time:entry-logged time-tracking log-time, stop-timer
time:timesheet-generated time-tracking get-timesheet
db db:schema-changed db-schema-explorer explore-schema
db:index-suggestion db-schema-explorer suggest-indexes
test test:generated test-generator generate-unit-tests
test:coverage-report test-generator analyze-coverage
docs docs:api-updated api-documentation extract-endpoints
docs:stale-detected api-documentation find-undocumented
perf perf:bottleneck-found performance-profiler find-bottlenecks
perf:profile-completed performance-profiler benchmark-compare
retro retro:action-item-created retrospective-manager generate-action-items
retro:completed retrospective-manager (chiusura retrospettiva)
economics economics:budget-alert project-economics get-budget-status
economics:cost-updated project-economics log-cost
standup standup:report-generated standup-notes log-standup

Esempio di Payload Tipizzato

Ogni evento possiede un payload TypeScript specifico. Ecco alcuni esempi rappresentativi per ciascun dominio principale:


// scrum:sprint-completed
{
  sprintId: string;
  name: string;
  velocity: number;
  completedStories: number;
  incompleteStories: number;
}

// time:entry-logged
{
  taskId: string;
  userId: string;
  minutes: number;
  date: string;       // ISO 8601
}

// cicd:build-failed
{
  pipelineId: string;
  error: string;
  stage: string;
  branch: string;
}

// economics:budget-alert
{
  project: string;
  percentUsed: number;
  threshold: number;   // es. 80
  remaining: number;
}

// code:dependency-alert
{
  package: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  advisory: string;
}
    

Pattern di Integrazione nei Server

L'integrazione dell'EventBus nei server segue un pattern standardizzato a 4 step che garantisce separazione delle responsabilità e opzionalita completa.

1. Creazione dell'EventBus (index.ts)


// servers/<nome>/src/index.ts
import { LocalEventBus } from '@mcp-suite/event-bus';

const eventBus = new LocalEventBus();
const suite = createMyServer(eventBus);
    

2. Iniezione nel Server (server.ts)


export function createMyServer(eventBus?: EventBus): McpSuiteServer {
  const suite = createMcpServer({
    name: 'my-server',
    version: '0.1.0',
    eventBus,
  });

  const store = new MyStore();

  // Tool che pubblica eventi
  registerCreateItem(suite.server, store, suite.eventBus);

  // Tool read-only: non serve l'eventBus
  registerListItems(suite.server, store);

  // Collaboration handlers
  if (suite.eventBus) {
    setupCollaborationHandlers(suite.eventBus, store);
  }

  return suite;
}
    

3. Pubblicazione negli Handler dei Tool (tools/*.ts)


export function registerCreateItem(
  server: McpServer,
  store: MyStore,
  eventBus?: EventBus,
): void {
  server.tool('create-item', 'Create a new item', schema, async (args) => {
    const item = store.create(args);

    // Fire-and-forget: eventBus può essere undefined
    eventBus?.publish('domain:item-created', {
      itemId: item.id,
      // ...payload tipizzato
    });

    return { content: [{ type: 'text', text: JSON.stringify(item) }] };
  });
}
    

4. Sottoscrizione (collaboration.ts)


export function setupCollaborationHandlers(
  eventBus: EventBus,
  store: MyStore,
): void {
  eventBus.subscribe('other-domain:event', (payload) => {
    // Reagisci all'evento
    store.updateSomething(payload);
  });
}
    

Principi di Design dell'EventBus

  • Fire-and-Forget: eventBus?.publish() con optional chaining gestisce il caso undefined; non si usa await
  • Solo tool mutanti pubblicano: tool di creazione, aggiornamento e cancellazione pubblicano eventi; tool di lettura no
  • Collaborazione isolata: la logica di reazione agli eventi e sempre in collaboration.ts, mai nei tool handler
  • Errori contenuti: il fallimento di un subscriber non impatta mai il publisher

Pattern Matching con micromatch

Il metodo subscribePattern permette di sottoscrivere a gruppi di eventi usando pattern glob, grazie alla libreria micromatch:


// Tutti gli eventi del dominio scrum
eventBus.subscribePattern('scrum:*', (event, payload) => {
  console.log(`Scrum event: ${event}`, payload);
});

// Tutti gli eventi di completamento
eventBus.subscribePattern('*:*-completed', (event, payload) => {
  console.log(`Completed: ${event}`, payload);
});

// Workflow orchestrator: intercetta TUTTI gli eventi
eventBus.subscribePattern('*', async (event, payload) => {
  const workflows = store.getActiveWorkflowsByTrigger(event);
  for (const workflow of workflows) {
    if (matchesConditions(workflow.triggerConditions, payload)) {
      await executeWorkflow(workflow, payload);
    }
  }
});
    

Matrice di Collaborazione: Publisher e Subscriber

La matrice di collaborazione documenta tutti i flussi di eventi tra i 22 server della suite. Ogni server e classificato in base al suo ruolo nella rete di eventi.

Chi Sottoscrive Cosa

Sottoscrizioni Attive

Server Subscriber Evento Sottoscritto Reazione
agile-metrics scrum:sprint-completed Cache dati velocity
scrum:task-updated Tracking transizioni per cycle time
scrum:story-completed Aggiornamento throughput
time-tracking scrum:task-updated Auto-start/stop timer (futuro)
project-economics time:entry-logged Conversione tempo in costo
scrum:sprint-completed Snapshot costo sprint
scrum-board retro:action-item-created Auto-creazione task (futuro)
retrospective-manager scrum:sprint-completed Auto-creazione retro pendente (futuro)
cicd:build-failed Aggiunta item discussione (futuro)
standup-notes scrum:task-updated Contesto progress per standup (futuro)
cicd:build-failed Aggiunta blocker potenziale (futuro)

Classificazione dei Server

I 22 server della suite si classificano in quattro categorie in base al loro ruolo nella rete di collaborazione:

Server Bidirezionali (Pubblicano + Sottoscrivono)

Questi 5 server partecipano attivamente alla rete di eventi in entrambe le direzioni:

Server Bidirezionali

Server Pubblica Sottoscrive File collaboration.ts
scrum-board 4 eventi 1 evento Si
time-tracking 1 evento 1 evento Si
project-economics 2 eventi 2 eventi Si
retrospective-manager 1 evento 2 eventi Si
standup-notes 1 evento 2 eventi Si

Server Solo Publisher (7 server)

Generano eventi ma non reagiscono a eventi esterni:

  • cicd-monitor (2 eventi): hub per eventi DevOps
  • code-review (2 eventi): notifiche analisi codice
  • dependency-manager (1 evento): alert vulnerabilità
  • db-schema-explorer (1 evento): suggerimenti indici
  • test-generator (2 eventi): notifiche generazione test
  • api-documentation (2 eventi): aggiornamenti docs
  • performance-profiler (2 eventi): report performance

Server Solo Subscriber (1 server)

agile-metrics (3 eventi in ingresso): calcola metriche da eventi scrum senza pubblicare eventi propri direttamente dai tool.

Server Pass-Through (9 server)

Questi server accettano l'EventBus nel factory ma attualmente non pubblicano ne sottoscrivono. Sono predisposti per espansioni future:

  • docker-compose, log-analyzer, data-mock-generator
  • codebase-knowledge, regex-builder, http-client
  • snippet-manager, project-scaffolding, environment-manager

ClientManager: Chiamate RPC Sincrone tra Server

Il ClientManager e il secondo meccanismo di comunicazione inter-server. A differenza dell'EventBus (fire-and-forget asincrono), il ClientManager consente a un server MCP di chiamare un tool su un altro server e ottenere il risultato in modo sincrono (request/response).


Server Chiamante          Client Manager          Server Target
      |                        |                       |
      |-- callTool('target',   |                       |
      |   'tool') ------------>|                       |
      |                        |-- MCP JSON-RPC ------>|
      |                        |                       |
      |                        |<-- JSON-RPC response--|
      |<-- risultato ----------|                       |
      |                        |                       |
      |  (sincrono: il chiamante attende il risultato) |
    

EventBus vs ClientManager

Caratteristica EventBus (Pub/Sub) ClientManager (RPC)
Comunicazione Asincrona, fire-and-forget Sincrona, request/response
Direzione Uno-a-molti (broadcast) Uno-a-uno (chiamata diretta)
Accoppiamento Nessuno (publisher ignora subscriber) Basso (conosce nome server e tool)
Uso tipico Notifiche, aggiornamenti automatici Query dati, arricchimento risultati
Errori Ignorati (non impattano il publisher) Gestiti con graceful degradation
Latenza Microsecondi (in-process) Millisecondi
Esempio "Sprint creato" -> aggiorna metriche "Dammi i dati sprint" -> calcola velocity

Regola pratica: usa l'EventBus quando vuoi notificare altri server, usa il ClientManager quando hai bisogno di dati da un altro server.

Esempio di Chiamata cross-server con ClientManager


export function registerForecastBudget(
  server: McpServer,
  store: EconomicsStore,
  clientManager?: McpClientManager,
): void {
  server.tool(
    'forecast-budget',
    'Prevedi quando il budget si esaurira',
    {
      projectName: z.string(),
      includeTimeData: z.boolean().optional().default(false),
    },
    async ({ projectName, includeTimeData }) => {
      const forecast = store.forecastBudget(projectName);
      const result: Record<string, unknown> = { ...forecast };

      // Cross-server call: recupera dati dal time-tracking
      if (includeTimeData && clientManager) {
        const timeResult = await clientManager.callTool(
          'time-tracking',     // Server target
          'get-timesheet',     // Tool da invocare
          {},                  // Argomenti
        );

        const content = (timeResult as {
          content: Array<{ type: string; text: string }>;
        }).content;
        const timesheet = JSON.parse(content[0].text);

        result.laborAnalysis = {
          trackedHours: timesheet.totalMinutes / 60,
          estimatedLaborCost: (timesheet.totalMinutes / 60) * 50,
        };
      }

      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    },
  );
}
    

I 6 Scenari di Wiring Implementati

MCP Suite implementa 6 scenari di comunicazione cross-server che coinvolgono 5 server chiamanti e 3 server target:

Scenari di Cross-Server Wiring

# Chiamante Target Tool Target Scopo
1 agile-metrics scrum-board get-sprint Calcolo velocity con dati sprint reali
2 agile-metrics time-tracking get-timesheet Arricchimento cycle-time con ore tracciate
3 project-economics time-tracking get-timesheet Calcolo costo lavoro da timesheet
4 data-mock-generator db-schema-explorer explore-schema Generazione mock da schema DB reale
5 test-generator codebase-knowledge explain-module Generazione test da analisi modulo
6 standup-notes scrum-board sprint-board Report di stato con dati sprint

Graceful Degradation

Il principio fondamentale del wiring e la graceful degradation: ogni tool funziona perfettamente anche senza ClientManager. Il wiring arricchisce il risultato, ma non e mai obbligatorio:


// Il pattern if (param && clientManager) garantisce:
// 1. Senza clientManager: il tool funziona normalmente
// 2. Senza il parametro trigger: il wiring non viene attivato
// 3. Con entrambi: il risultato viene arricchito con dati esterni
if (enrichFromExternal && clientManager) {
  // cross-server call
}
    

Flussi End-to-End

La vera potenza della collaborazione emerge nei flussi end-to-end che attraversano più server. Questi flussi nascono dalla composizione naturale degli eventi, senza orchestrazione centralizzata.

Flusso 1: Ciclo di Vita Sprint


[Creazione Sprint]
scrum-board:create-sprint
    |
    +-- scrum:sprint-started -----> (informativo, espansione futura)

[Lavoro durante lo Sprint]
scrum-board:update-task-status
    |
    +-- scrum:task-updated -------> agile-metrics  (cycle time)
    |                           +-> time-tracking  (auto-timer futuro)
    |                           +-> standup-notes  (contesto standup)

[Chiusura Sprint]
scrum-board:close-sprint
    |
    +-- scrum:sprint-completed ---> agile-metrics       (velocity)
    |                           +-> project-economics   (snapshot costo)
    |                           +-> retrospective-mgr   (auto-crea retro)
    |
    +-- scrum:story-completed ----> agile-metrics       (throughput)
    

Flusso 2: Tracking Tempo e Costi


[Registrazione Tempo]
time-tracking:log-time / stop-timer
    |
    +-- time:entry-logged --------> project-economics  (conversione costo)

[Registrazione Costo Diretto]
project-economics:log-cost
    |
    +-- economics:cost-updated ---> (informativo)

[Verifica Budget]
project-economics:get-budget-status
    |
    +-- economics:budget-alert ---> (notifica se >80%)
    

Flusso 3: Ciclo DevOps


[Monitoraggio Pipeline]
cicd-monitor:get-pipeline-status
    |
    +-- cicd:pipeline-completed --> (informativo)
    |
    +-- cicd:build-failed -------> retrospective-mgr  (item discussione)
                                +-> standup-notes      (blocker)
    

Flusso 4: Quality e Review


[Analisi Codice]
code-review:analyze-diff
    |
    +-- code:commit-analyzed -----> (informativo)

code-review:suggest-improvements
    |
    +-- code:review-completed ----> (informativo, codebase-knowledge futuro)

[Vulnerabilità]
dependency-manager:check-vulnerabilities
    |
    +-- code:dependency-alert ----> (informativo)
    

Flusso 5: Retrospettiva e Miglioramento Continuo


[Sprint Completato]
scrum:sprint-completed -----------> retrospective-mgr (auto-crea retro)

[Generazione Action Items]
retrospective-mgr:generate-action-items
    |
    +-- retro:action-item-created -> scrum-board (auto-crea task futuro)
    

Diagramma Completo della Rete

Il flusso completo forma una rete dove scrum-board e l'hub centrale con 4 eventi in uscita che raggiungono 4 server diversi. agile-metrics e il maggiore subscriber con 3 eventi in ingresso. time-tracking e project-economics formano una catena: il tempo registrato diventa automaticamente costo attraverso l'evento time:entry-logged.

Testing dell'EventBus e del Wiring

Per testare la collaborazione, Tech-MCP fornisce un MockEventBus che registra gli eventi pubblicati senza effetti collaterali:


export class MockEventBus implements EventBus {
  public published: Array<{ event: string; payload: unknown }> = [];

  async publish(event: string, payload: unknown): Promise<void> {
    this.published.push({ event, payload });
  }

  subscribe(): () => void { return () => {}; }
  subscribePattern(): () => void { return () => {}; }
  clear(): void { this.published = []; }

  // Utility per asserzioni
  wasPublished(eventName: string): boolean {
    return this.published.some((e) => e.event === eventName);
  }
}

// Uso nei test
it('should publish time:entry-logged event', async () => {
  const eventBus = new MockEventBus();
  const { server } = createTimeTrackingServer({
    eventBus,
    storeOptions: { inMemory: true },
  });
  const harness = await createTestHarness(server);

  await harness.client.callTool({
    name: 'log-time',
    arguments: { taskId: 'TASK-1', durationMinutes: 60 },
  });

  expect(eventBus.wasPublished('time:entry-logged')).toBe(true);
});
    

Per il wiring cross-server, i test usano InMemoryTransport per connettere i server in-process senza rete. L'ordine e critico: il server target deve connettersi al transport prima del client, perchè il client invia immediatamente il messaggio initialize al momento della connessione.

Sviluppi Futuri

La rete di collaborazione e progettata per crescere. Tra gli sviluppi pianificati:

  • Redis EventBus: per deployment multi-processo, permettendo la collaborazione tra server in processi separati
  • Nuovi eventi: scrum:backlog-prioritized, time:timer-started/time:timer-stopped, deploy:release-created
  • Implementazione completa dei collaboration handler: molti handler contengono attualmente placeholder in attesa di implementazione
  • Workflow orchestrator: un server che usa subscribePattern('*') per coordinare flussi complessi multi-step

Conclusioni

La collaborazione inter-server rappresenta il livello architetturale che trasforma una collezione di server indipendenti in un ecosistema integrato. L'EventBus fornisce il disaccoppiamento per notifiche e reazioni automatiche, mentre il ClientManager abilita l'arricchimento dei dati attraverso query sincrone cross-server.

I 29 eventi tipizzati, organizzati in 11 domini, creano una rete di collaborazione dove ogni azione significativa può innescare reazioni automatiche in server multipli. La classificazione dei server in bidirezionali, publisher-only, subscriber-only e pass-through garantisce chiarezza architetturale e facilità l'espansione futura.

Nel prossimo articolo affronteremo il testing professionale e il deploy in produzione, analizzando come testare i server MCP con test harness, InMemoryTransport, e come prepararli per ambienti di produzione con configurazione, logging e monitoring.

Il codice completo della collaborazione inter-server e disponibile nel repository Tech-MCP su GitHub.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti