Skip to main content

12 - Inter-Server Collaboration and EventBus

How MCP servers communicate: EventBus with 29 typed events, ClientManager for cross-server invocation, collaboration matrix.

What do you think about this article?

At a Glance

How MCP servers communicate: EventBus with 29 typed events, ClientManager for cross-server invocation, collaboration matrix.

Introduction: How MCP Servers Communicate

In an ecosystem with 22 active MCP servers, collaboration between servers becomes essential. A single server manages a specific domain (scrum, time-tracking, CI/CD), but real-world workflows cross multiple domains: when a sprint closes, metrics need to update, costs need to calculate, and a retrospective should be created automatically.

Tech-MCP implements two complementary mechanisms for inter-server communication: the EventBus (async fire-and-forget pub/sub) and the ClientManager (synchronous RPC calls). Together, they enable building end-to-end flows that traverse the entire suite without direct coupling between servers.

What You'll Learn in This Article

  • EventBus architecture and the Publish/Subscribe pattern in MCP Suite
  • The 29 typed events organized across 11 domains
  • The collaboration matrix: who publishes, who subscribes
  • ClientManager for synchronous RPC calls between servers
  • Server classification: bi-directional, publisher-only, subscriber-only, pass-through
  • End-to-end flows: sprint lifecycle, time and costs, DevOps, quality
  • Pattern matching with micromatch for wildcard subscriptions

EventBus: The Heart of Asynchronous Collaboration

The EventBus is the mechanism that allows MCP Suite servers to collaborate in an asynchronous and decoupled manner. When a server performs a significant action (sprint creation, time logging, build completion), it publishes a typed event that other servers can subscribe to and react to automatically.


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

This pattern is known as Publish/Subscribe (Pub/Sub) and offers fundamental advantages:

  • Decoupling: the publisher doesn't know about the subscribers
  • Extensibility: adding a new subscriber requires no changes to the publisher
  • Optionality: the EventBus is always optional; every server works perfectly without it

EventBus Interface

The EventBus interface defines the contract for any event bus implementation:


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

EventBus Methods

Method Description
publish Publishes a typed event with its payload
subscribe Subscribes to a specific event. Returns an unsubscribe function
subscribePattern Subscribes to events matching a glob pattern (e.g. scrum:*)
clear Removes all subscriptions

LocalEventBus: In-Process Implementation

The LocalEventBus class is the default implementation, based on Node.js EventEmitter with pattern support 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> {
    // Notify exact subscribers
    this.emitter.emit(event, payload);

    // Notify pattern subscribers
    for (const sub of this.patternSubs) {
      if (micromatch.isMatch(event, sub.pattern)) {
        try {
          await sub.handler(event, payload);
        } catch {
          // Subscriber errors don't block publishing
        }
      }
    }
  }

  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 = [];
  }
}
    

Key features: max 100 listeners per event, pattern matching via micromatch for wildcards (scrum:*, *:completed), subscriber errors caught silently to avoid interrupting publishing, and zero external dependencies beyond micromatch.

Full Type Safety with EventMap

Every event is typed in both name and payload through the EventMap type. The TypeScript compiler verifies at compile-time that the payload is correct:


// TypeScript compiler verifies the payload is correct
eventBus.publish('scrum:sprint-started', {
  sprintId: '42',
  name: 'Sprint 15',
  startDate: '2025-01-01',
  endDate: '2025-01-14',
});

// Compilation error: missing field 'name'
eventBus.publish('scrum:sprint-started', {
  sprintId: '42',
});
    

The 29 Typed Events Across 11 Domains

MCP Suite defines 29 typed events organized across 11 domains. Each event follows the domain:action-in-kebab-case format and has a strongly typed TypeScript payload.

Domain Map

Domain Events Functional Area
code:* 3 Code and Git
cicd:* 2 CI/CD
scrum:* 4 Project Management
time:* 2 Time Tracking
db:* 2 Database
test:* 2 Testing
docs:* 2 Documentation
perf:* 2 Performance
retro:* 2 Retrospectives
economics:* 2 Project Economics
standup:* 1 Standup
Total 29

Complete Event Reference

All 29 Events with Servers and Triggers

Domain Event 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 (retrospective closure)
economics economics:budget-alert project-economics get-budget-status
economics:cost-updated project-economics log-cost
standup standup:report-generated standup-notes log-standup

Typed Payload Examples

Each event has a specific TypeScript payload. Here are representative examples for the main domains:


// 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;   // e.g. 80
  remaining: number;
}

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

Server Integration Patterns

EventBus integration in servers follows a standardized 4-step pattern that ensures separation of concerns and complete optionality.

1. EventBus Creation (index.ts)


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

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

2. Injection in 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 that publishes events
  registerCreateItem(suite.server, store, suite.eventBus);

  // Read-only tool: no eventBus needed
  registerListItems(suite.server, store);

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

  return suite;
}
    

3. Publishing in Tool Handlers (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 may be undefined
    eventBus?.publish('domain:item-created', {
      itemId: item.id,
      // ...typed payload
    });

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

4. Subscription (collaboration.ts)


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

EventBus Design Principles

  • Fire-and-Forget: eventBus?.publish() with optional chaining handles the undefined case; no await is used
  • Only mutating tools publish: creation, update, and deletion tools publish events; read-only tools do not
  • Isolated collaboration: event reaction logic is always in collaboration.ts, never in tool handlers
  • Contained errors: subscriber failures never impact the publisher

Pattern Matching with micromatch

The subscribePattern method allows subscribing to groups of events using glob patterns, powered by the micromatch library:


// All events in the scrum domain
eventBus.subscribePattern('scrum:*', (event, payload) => {
  console.log(`Scrum event: ${event}`, payload);
});

// All completion events
eventBus.subscribePattern('*:*-completed', (event, payload) => {
  console.log(`Completed: ${event}`, payload);
});

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

Collaboration Matrix: Publishers and Subscribers

The collaboration matrix documents all event flows between the 22 servers in the suite. Each server is classified based on its role in the event network.

Who Subscribes to What

Active Subscriptions

Subscriber Server Subscribed Event Reaction
agile-metrics scrum:sprint-completed Cache velocity data
scrum:task-updated Track transitions for cycle time
scrum:story-completed Update throughput
time-tracking scrum:task-updated Auto-start/stop timer (future)
project-economics time:entry-logged Convert time to cost
scrum:sprint-completed Sprint cost snapshot
scrum-board retro:action-item-created Auto-create task (future)
retrospective-manager scrum:sprint-completed Auto-create pending retro (future)
cicd:build-failed Add discussion item (future)
standup-notes scrum:task-updated Progress context for standup (future)
cicd:build-failed Add potential blocker (future)

Server Classification

The 22 servers in the suite are classified into four categories based on their role in the collaboration network:

Bi-Directional Servers (Publish + Subscribe)

These 5 servers actively participate in the event network in both directions:

Bi-Directional Servers

Server Publishes Subscribes collaboration.ts
scrum-board 4 events 1 event Yes
time-tracking 1 event 1 event Yes
project-economics 2 events 2 events Yes
retrospective-manager 1 event 2 events Yes
standup-notes 1 event 2 events Yes

Publisher-Only Servers (7 servers)

These servers generate events but don't react to external events:

  • cicd-monitor (2 events): DevOps event hub
  • code-review (2 events): code analysis notifications
  • dependency-manager (1 event): vulnerability alerts
  • db-schema-explorer (1 event): index suggestions
  • test-generator (2 events): test generation notifications
  • api-documentation (2 events): documentation updates
  • performance-profiler (2 events): performance reports

Subscriber-Only Servers (1 server)

agile-metrics (3 incoming events): computes metrics from scrum events without publishing its own events directly from tools.

Pass-Through Servers (9 servers)

These servers accept the EventBus in their factory but currently neither publish nor subscribe. They are prepared for future expansion:

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

ClientManager: Synchronous RPC Calls Between Servers

The ClientManager is the second inter-server communication mechanism. Unlike the EventBus (asynchronous fire-and-forget), the ClientManager allows an MCP server to call a tool on another server and get the result synchronously (request/response).


Calling Server            Client Manager          Target Server
      |                        |                       |
      |-- callTool('target',   |                       |
      |   'tool') ------------>|                       |
      |                        |-- MCP JSON-RPC ------>|
      |                        |                       |
      |                        |<-- JSON-RPC response--|
      |<-- result -------------|                       |
      |                        |                       |
      |  (synchronous: caller waits for the result)    |
    

EventBus vs ClientManager

Feature EventBus (Pub/Sub) ClientManager (RPC)
Communication Asynchronous, fire-and-forget Synchronous, request/response
Direction One-to-many (broadcast) One-to-one (direct call)
Coupling None (publisher ignores subscribers) Low (knows server name and tool)
Typical use Notifications, automatic updates Data queries, result enrichment
Errors Ignored (don't impact publisher) Handled with graceful degradation
Latency Microseconds (in-process) Milliseconds
Example "Sprint created" -> update metrics "Give me sprint data" -> calculate velocity

Rule of thumb: use the EventBus when you want to notify other servers, use the ClientManager when you need data from another server.

Cross-Server Call Example with ClientManager


export function registerForecastBudget(
  server: McpServer,
  store: EconomicsStore,
  clientManager?: McpClientManager,
): void {
  server.tool(
    'forecast-budget',
    'Predict when the budget will be exhausted',
    {
      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: retrieve data from time-tracking
      if (includeTimeData && clientManager) {
        const timeResult = await clientManager.callTool(
          'time-tracking',     // Target server
          'get-timesheet',     // Tool to invoke
          {},                  // Arguments
        );

        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) }],
      };
    },
  );
}
    

The 6 Implemented Wiring Scenarios

MCP Suite implements 6 cross-server communication scenarios involving 5 calling servers and 3 target servers:

Cross-Server Wiring Scenarios

# Caller Target Target Tool Purpose
1 agile-metrics scrum-board get-sprint Calculate velocity with real sprint data
2 agile-metrics time-tracking get-timesheet Enrich cycle-time with tracked hours
3 project-economics time-tracking get-timesheet Calculate labor cost from timesheet
4 data-mock-generator db-schema-explorer explore-schema Generate mock data from real DB schema
5 test-generator codebase-knowledge explain-module Generate tests from module analysis
6 standup-notes scrum-board sprint-board Status report with sprint data

Graceful Degradation

The fundamental principle of wiring is graceful degradation: every tool works perfectly without the ClientManager. Wiring enriches the result, but is never mandatory:


// The if (param && clientManager) pattern guarantees:
// 1. Without clientManager: tool works normally
// 2. Without trigger parameter: wiring is not activated
// 3. With both: result is enriched with external data
if (enrichFromExternal && clientManager) {
  // cross-server call
}
    

End-to-End Flows

The true power of collaboration emerges in end-to-end flows that traverse multiple servers. These flows arise from the natural composition of events, without centralized orchestration.

Flow 1: Sprint Lifecycle


[Sprint Creation]
scrum-board:create-sprint
    |
    +-- scrum:sprint-started -----> (informational, future expansion)

[Work During Sprint]
scrum-board:update-task-status
    |
    +-- scrum:task-updated -------> agile-metrics  (cycle time)
    |                           +-> time-tracking  (auto-timer future)
    |                           +-> standup-notes  (standup context)

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

Flow 2: Time and Cost Tracking


[Time Registration]
time-tracking:log-time / stop-timer
    |
    +-- time:entry-logged --------> project-economics  (cost conversion)

[Direct Cost Registration]
project-economics:log-cost
    |
    +-- economics:cost-updated ---> (informational)

[Budget Check]
project-economics:get-budget-status
    |
    +-- economics:budget-alert ---> (notification if >80%)
    

Flow 3: DevOps Cycle


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

Flow 4: Quality and Review


[Code Analysis]
code-review:analyze-diff
    |
    +-- code:commit-analyzed -----> (informational)

code-review:suggest-improvements
    |
    +-- code:review-completed ----> (informational, codebase-knowledge future)

[Vulnerabilities]
dependency-manager:check-vulnerabilities
    |
    +-- code:dependency-alert ----> (informational)
    

Flow 5: Retrospective and Continuous Improvement


[Sprint Completed]
scrum:sprint-completed -----------> retrospective-mgr (auto-create retro)

[Action Item Generation]
retrospective-mgr:generate-action-items
    |
    +-- retro:action-item-created -> scrum-board (auto-create task future)
    

Complete Network Diagram

The complete flow forms a network where scrum-board is the central hub with 4 outgoing events reaching 4 different servers. agile-metrics is the largest subscriber with 3 incoming events. time-tracking and project-economics form a chain: logged time automatically becomes cost through the time:entry-logged event.

Testing EventBus and Wiring

For testing collaboration, Tech-MCP provides a MockEventBus that records published events without side effects:


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 = []; }

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

// Usage in tests
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);
});
    

For cross-server wiring, tests use InMemoryTransport to connect servers in-process without network. The order is critical: the target server must connect to the transport before the client, because the client immediately sends the initialize message upon connection.

Future Developments

The collaboration network is designed to grow. Among the planned developments:

  • Redis EventBus: for multi-process deployment, enabling collaboration between servers in separate processes
  • New events: scrum:backlog-prioritized, time:timer-started/time:timer-stopped, deploy:release-created
  • Full collaboration handler implementation: many handlers currently contain placeholders awaiting implementation
  • Workflow orchestrator: a server using subscribePattern('*') to coordinate complex multi-step flows

Conclusions

Inter-server collaboration represents the architectural layer that transforms a collection of independent servers into an integrated ecosystem. The EventBus provides decoupling for notifications and automatic reactions, while the ClientManager enables data enrichment through synchronous cross-server queries.

The 29 typed events, organized across 11 domains, create a collaboration network where every significant action can trigger automatic reactions across multiple servers. The server classification into bi-directional, publisher-only, subscriber-only, and pass-through ensures architectural clarity and facilitates future expansion.

In the next article, we will tackle professional testing and production deployment, analyzing how to test MCP servers with test harnesses, InMemoryTransport, and how to prepare them for production environments with configuration, logging, and monitoring.

The complete inter-server collaboration code is available in the Tech-MCP GitHub repository.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects