In large organizations, the number of MCP servers grows quickly. A payments MCP server, a customer data MCP server, a product catalog server, an analytics server – each maintained by different teams. Without a registry, every agent developer must manually configure each server’s URL, credentials, and capabilities. A registry solves this: publish once, discover everywhere. This lesson builds an MCP server registry, a discovery client, and covers service mesh integration patterns for enterprise deployments.

Registry Data Model
// A registry entry describes one MCP server
/**
* @typedef {Object} RegistryEntry
* @property {string} id - Unique server identifier (slug)
* @property {string} name - Human-readable name
* @property {string} description - What this server does
* @property {string} url - Base URL for Streamable HTTP transport
* @property {string} version - Server version (semver)
* @property {string[]} tags - Capability tags for discovery (e.g., ['products', 'inventory'])
* @property {Object} auth - Authentication requirements
* @property {string} auth.type - 'none' | 'bearer' | 'oauth2'
* @property {string} [auth.tokenEndpoint] - OAuth token endpoint if auth.type === 'oauth2'
* @property {string} healthUrl - Health check endpoint
* @property {Date} lastSeen - Last successful health check
* @property {'healthy' | 'degraded' | 'down'} status - Current health status
*/
Simple Registry Server
// registry-server.js - A lightweight HTTP registry for MCP servers
import express from 'express';
const app = express();
app.use(express.json());
// In-memory store (use Redis or PostgreSQL in production)
const registry = new Map();
// Register a server
app.post('/servers', (req, res) => {
const entry = {
...req.body,
registeredAt: new Date().toISOString(),
lastSeen: new Date().toISOString(),
status: 'healthy',
};
registry.set(entry.id, entry);
res.status(201).json({ id: entry.id });
});
// List all healthy servers (with optional tag filter)
app.get('/servers', (req, res) => {
const { tags, status = 'healthy' } = req.query;
let servers = [...registry.values()].filter(s => s.status === status);
if (tags) {
const filterTags = tags.split(',');
servers = servers.filter(s => filterTags.some(t => s.tags?.includes(t)));
}
res.json({ servers });
});
// Health check runner: poll all registered servers every 30 seconds
setInterval(async () => {
for (const [id, entry] of registry) {
try {
const res = await fetch(entry.healthUrl, { signal: AbortSignal.timeout(5000) });
entry.status = res.ok ? 'healthy' : 'degraded';
entry.lastSeen = new Date().toISOString();
} catch {
entry.status = 'down';
}
registry.set(id, entry);
}
}, 30_000);
app.listen(4000, () => console.log('Registry listening on :4000'));
Discovery Client for Agents
// discovery-client.js - Used by agent hosts to discover MCP servers
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamable-http.js';
class McpDiscoveryClient {
#registryUrl;
#connections = new Map();
constructor(registryUrl) {
this.#registryUrl = registryUrl;
}
// Discover servers by tags and establish connections
async connect(tags = []) {
const query = tags.length ? `?tags=${tags.join(',')}` : '';
const res = await fetch(`${this.#registryUrl}/servers${query}`);
const { servers } = await res.json();
const connected = [];
for (const server of servers) {
if (this.#connections.has(server.id)) {
connected.push(server);
continue;
}
try {
const transport = new StreamableHTTPClientTransport(new URL(`${server.url}/mcp`));
const client = new Client({ name: 'discovery-host', version: '1.0.0' });
await client.connect(transport);
this.#connections.set(server.id, { client, server });
connected.push(server);
console.log(`Connected to ${server.name} (${server.id})`);
} catch (err) {
console.error(`Failed to connect to ${server.name}: ${err.message}`);
}
}
return connected;
}
// Get all tools from all connected servers
async getAllTools() {
const allTools = [];
for (const [id, { client, server }] of this.#connections) {
try {
const { tools } = await client.listTools();
allTools.push(...tools.map(t => ({ ...t, serverId: id })));
} catch (err) {
console.error(`Failed to list tools from ${id}: ${err.message}`);
}
}
return allTools;
}
// Route a tool call to the correct server
async callTool(toolName, args) {
for (const [, { client }] of this.#connections) {
const { tools } = await client.listTools();
if (tools.some(t => t.name === toolName)) {
return client.callTool({ name: toolName, arguments: args });
}
}
throw new Error(`Tool '${toolName}' not found in any connected server`);
}
}
// Usage
const discovery = new McpDiscoveryClient('https://registry.internal');
await discovery.connect(['products', 'analytics']);
const allTools = await discovery.getAllTools();
console.log(`Discovered ${allTools.length} tools across all servers`);

Service Mesh Integration (Istio / Linkerd)
In Kubernetes environments, a service mesh handles mutual TLS, traffic routing, and observability for all service-to-service communication, including MCP connections:
# With Istio, MCP server-to-server communication is automatically mTLS
# No code changes required - the sidecar proxy handles it
# Example: VirtualService for traffic splitting during MCP server rollout
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: mcp-product-server
spec:
hosts:
- mcp-product-server
http:
- route:
- destination:
host: mcp-product-server
subset: v2
weight: 10 # 10% to new version
- destination:
host: mcp-product-server
subset: v1
weight: 90 # 90% to stable version
Server Health Aggregation
// Aggregate health status across all registered servers for a status page
app.get('/status', async (req, res) => {
const servers = [...registry.values()];
const healthy = servers.filter(s => s.status === 'healthy').length;
const degraded = servers.filter(s => s.status === 'degraded').length;
const down = servers.filter(s => s.status === 'down').length;
const overall = down > 0 ? 'degraded' : (degraded > 0 ? 'degraded' : 'operational');
res.json({
status: overall,
summary: { total: servers.length, healthy, degraded, down },
servers: servers.map(s => ({
id: s.id, name: s.name, status: s.status, lastSeen: s.lastSeen,
})),
});
});
What to Build Next
- Deploy the registry server alongside your existing MCP servers. Register each server on startup using a POST to the registry.
- Build a simple status page that reads from
/statusand shows which MCP servers are healthy.
nJoy 😉
