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: 






