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