Free lesson · GenAI Solutions Architecture

Build MCP server registry with capability discovery and health checks

You will build an MCPServerRegistry that provides centralized registration, discovery, and health monitoring for all MCP servers in the enterprise tool ecosystem, aligned with the latest MCP specification (2025-11-25). MCP uses **Streamable HTTP** as the standard remote transport -- a single HTTP endpoint that supports bidirectional JSON-RPC 2.0 message framing, replacing the now-deprecated SSE (Server-Sent Events) dual-endpoint transport. Every MCP message (requests, responses, and notifications) is a JSON-RPC 2.0 envelope, giving the protocol a language-agnostic wire format. Note that MCP was donated to the **Agentic AI Foundation (AAIF)** under the Linux Foundation in December 2025, establishing vendor-neutral governance for the protocol's evolution. Define a ServerRegistration Pydantic model with fields server_id: str, endpoint_url: HttpUrl, transport_type: TransportType (enum: streamable_http, stdio, defaulting to streamable_http), capabilities: list[ToolCapability], sla_contract: SLAContract, version: str, owner_team: str, tags: list[str], and health_status: HealthStatus. The ToolCapability model captures tool_name: str, input_schema: dict, output_schema: dict, avg_latency_ms: float, rate_limit_rpm: int, and description: str. The SLAContract model defines max_latency_p99_ms: int, availability_target: float, max_error_rate: float, and support_tier: str. Implement register_server(registration: ServerRegistration) -> RegistrationResult that validates the server's MCP manifest against the MCPManifestSchema, sends a JSON-RPC 2.0 tools/list request over the server's Streamable HTTP endpoint to verify capability claims match the declared capabilities, runs a connectivity test by invoking one tool with a synthetic input to confirm end-to-end functionality, and persists registration in the PostgreSQL mcp_servers table with columns server_id, endpoint_url, transport_type, capabilities_json, sla_json, registered_at, last_health_check, status, owner_team, version. Build discover_servers(capability_filter: CapabilityQuery) -> list[ServerRegistration] that queries the registry to find servers matching required tool names, input types, or SLA requirements using PostgreSQL JSONB queries with the @> containment operator on the capabilities_json column. Support fuzzy matching by embedding tool descriptions in pgvector and querying with 1 - (embedding <=> query_embedding) > 0.8 for semantic discovery. Implement a HealthCheckProber that runs every 30 seconds via an asyncio background task, sending a JSON-RPC 2.0 tools/list request to each server's Streamable HTTP endpoint to verify capability availability, tracking consecutive failures in Redis key health:{server_id}:failures and marking servers as degraded after 3 failures or offline after 10 consecutive failures. Emit Prometheus metrics mcp_server_health_status{server_id,status}, mcp_server_discovery_latency_seconds{query_type}, mcp_registry_servers_total{status}, mcp_health_check_duration_seconds{server_id}, and mcp_server_capabilities_count{server_id}. Build FastAPI endpoints GET /api/v1/mcp/servers returning all registered servers with filtering by capability, status, and team, GET /api/v1/mcp/servers/{server_id}/capabilities returning the detailed capability manifest, and POST /api/v1/mcp/servers for new registrations. Implement deprecate_server(server_id: str, sunset_days: int = 30) that marks a server as deprecated, updates the deprecated_at and sunset_date timestamps, notifies dependent consumers via webhook, and enforces the sunset period before removal. Build server lifecycle state machine: registered -> active -> deprecated -> deregistered with transition validation and audit logging of every state change in the server_lifecycle_events table with columns event_id, server_id, from_state, to_state, triggered_by, timestamp.

Course: GenAI Architecture & Design Patterns · Chapter 11 · MCP Tool Mesh

Free to read — no subscription required.

Introduction

When you run a dozen MCP servers deployed by different teams — each versioned independently, scaled horizontally, and occasionally swapped out — agents lose track of which tools exist and which servers are still reachable. Teams that ship MCP without a centralized registry end up with hardcoded server lists scattered across agent configs, silent failures when a server crashes, and no audit trail of capability changes. By the end of this lesson you will be able to design a centralized MCP server registry that tracks identity, connection, capabilities, and health for every server, accepts dynamic registration at runtime, and exposes a query interface agents use for capability discovery across the mesh.

Key Terminology

  • ServerRecord: The registry's per-server entry, decomposed into Identity, Connection, Capabilities, and HealthStatus dimensions.
  • Capability discovery: The query interface agents use to locate tools, resources, or prompts by name, tag, or schema across all registered MCP servers.
  • Forced eviction: Automatic removal of a server whose health check has been UNREACHABLE beyond the configured TTL, without a graceful deregistration signal.

Concepts

This section covers why a registry beats static config, how lifecycle states (register, health-check, deregister, evict) are managed, and the discovery patterns agents use to find capabilities across the mesh.

Why a Registry, Not a Configuration File

A static configuration file works when you have two or three MCP servers that rarely change. In an enterprise context, servers are deployed by different teams, versioned independently, scaled horizontally, and occasionally deprecated. A registry differs from a config file in three critical ways: it accepts dynamic registration at runtime, it continuously validates server health, and it exposes a query interface for capability discovery. Think of it as a service registry (like Consul or Eureka) but purpose-built for the MCP protocol's tool, resource, and prompt primitives.

The MCP specification defines a server as an entity that exposes tools (callable functions), resources (readable data), and prompts (reusable templates). Your registry must track all three capability types per server, because an agent performing tool routing needs to know not just "which server has a query_database tool" but also "which server exposes a schema:// resource that lets me inspect the database before querying it."

Lifecycle Management

Registration and health checks cover two of the three lifecycle phases. The third is deregistration — the controlled removal of a server from the mesh. A well-designed registry supports two deregistration modes:

  • Graceful deregistration: The server (or its deployment pipeline) calls a deregister_server API, which marks the server as DEREGISTERED, removes its tools from the discovery index, but retains the ServerRecord for audit history. This is the normal path during rolling deployments or planned decommissioning.
  • Forced eviction: The health check loop detects that a server has been UNREACHABLE for longer than a configurable TTL (for example, 10 minutes). The registry automatically evicts the server, removes its tools from the index, and emits an alert to the governance dashboard. This handles crash scenarios where no graceful deregistration signal is sent.

Both modes must emit structured log events that the governance layer can ingest. Every registration, deregistration, health state transition, and capability change should produce a log entry with the server ID, timestamp, old state, new state, and the triggering event. These logs feed into the ecosystem governance dashboards covered later in this chapter, enabling operators to answer questions like "How many servers registered in the last week?" or "Which servers had more than three degraded episodes this month?"

Capability Discovery Patterns

The discover_tools method shown earlier supports simple keyword search, but enterprise tool meshes need richer discovery patterns:

  • Tag-based discovery: Each tool is annotated with tags like database, read-only, admin, or experimental. Agents filter by tags to narrow results before executing keyword search.
  • Schema-compatible discovery: Given a desired input schema (e.g., "I need a tool that accepts a sql_query string parameter"), the registry matches tools whose input_schema contains compatible fields. This enables automated tool selection without relying on name conventions.
  • Capability aggregation: When multiple servers expose the same tool name (common in horizontally scaled deployments), the registry returns a single logical tool entry with a list of backing server IDs. The tool routing layer (covered in another goal) then selects among replicas using load balancing strategies.

The combination of a centralized registry, continuous health monitoring, and structured capability discovery creates the foundation that every other component in the tool mesh depends on. Tool authorization queries the registry to resolve tool names to server IDs. Composition validation uses the registry's schema metadata to type-check tool chains. Load-balanced routing reads health status and latency data from the registry to make placement decisions. And governance dashboards aggregate registry events to visualize the health and evolution of the entire tool ecosystem.

Without this foundation, each of those layers would need its own server tracking mechanism, leading to inconsistent views of the mesh state, duplicated health-check traffic, and configuration drift between components. The registry is not optional infrastructure — it is the single source of truth for the tool mesh, and its reliability directly determines the reliability of every agent that depends on it.

Loading diagram...

Code Walkthrough

Building on the registry-versus-config distinction from the Concepts section, this walkthrough turns those lifecycle and discovery ideas into a working MCPServerRegistry. The data model groups each ServerRecord into the four dimensions named in the Key Terminology — Identity, Connection, Capabilities, and HealthStatus — so the registry can route requests only to healthy servers. The register_server method assigns a unique ID, ingests the server's advertised tools, and indexes them for fast lookup; discover_tools is the query interface agents call to find capabilities by keyword, filtered to servers currently reporting HEALTHY.

Code snippetpython
1import uuid 2import time 3from dataclasses import dataclass, field 4from enum import Enum 5 6class ServerStatus(Enum): 7 HEALTHY = "healthy" 8 DEGRADED = "degraded" 9 UNREACHABLE = "unreachable" 10 DEREGISTERED = "deregistered" 11 12@dataclass 13class ToolInfo: 14 name: str 15 description: str 16 server_id: str 17 18@dataclass 19class HealthStatus: 20 status: ServerStatus = ServerStatus.UNREACHABLE 21 last_check: float = 0.0 22 failure_count: int = 0 23 avg_latency_ms: float = 0.0 24 25@dataclass 26class ServerRecord: 27 server_id: str 28 name: str 29 version: str 30 owner: str 31 endpoint: str 32 transport: str 33 tools: list[ToolInfo] = field(default_factory=list) 34 health: HealthStatus = field(default_factory=HealthStatus) 35 registered_at: float = field(default_factory=time.time) 36 37class MCPServerRegistry: 38 def __init__(self): 39 self._servers: dict[str, ServerRecord] = {} 40 self._tool_index: dict[str, list[str]] = {} 41 42 def register_server(self, name, endpoint, tools, 43 transport="http", version="1.0.0", owner="platform"): 44 server_id = str(uuid.uuid4()) 45 record = ServerRecord(server_id, name, version, owner, endpoint, transport) 46 for spec in tools: 47 info = ToolInfo(spec["name"], spec["description"], server_id) 48 record.tools.append(info) 49 self._tool_index.setdefault(info.name, []).append(server_id) 50 record.health.status = ServerStatus.HEALTHY 51 record.health.last_check = time.time() 52 self._servers[server_id] = record 53 return record 54 55 def discover_tools(self, keyword): 56 return [t for r in self._servers.values() for t in r.tools 57 if keyword in t.name and r.health.status == ServerStatus.HEALTHY] 58 59registry = MCPServerRegistry() 60registry.register_server( 61 name="db-tools", endpoint="https://db.internal/mcp", 62 tools=[{"name": "query_database", "description": "Run a read query"}], 63) 64matches = registry.discover_tools("query") 65print([t.name for t in matches])

register_server stamps each ServerRecord with a generated server_id, copies advertised tools into ToolInfo entries, and updates _tool_index so discovery avoids scanning every server. Because discover_tools filters on ServerStatus.HEALTHY, a server marked UNREACHABLE by the health-check loop disappears from results without being explicitly removed — the same gating that later enables forced eviction. You'll know it works when discover_tools("query") returns ['query_database'] while the server is healthy and an empty list once its status is flipped away from HEALTHY.

Do's and Don'ts

Do's

  1. Do build _tool_index at register_server time using setdefault(...).append(...) — Pre-indexing tool names to server IDs lets discover_tools route directly to candidate servers instead of scanning every ServerRecord and its tool list; skipping this step degrades discovery to O(servers × tools) as the mesh grows.
  2. Do filter discover_tools on ServerStatus.HEALTHY — Gating the query at the registry layer means a server the health-check loop marks UNREACHABLE drops out of capability results automatically, without an explicit deregistration call and without pushing that safety check into every agent caller.
  3. Do assign server_id via uuid.uuid4() inside register_server — A generated ID decouples a server's stable registry identity from its mutable endpoint, version, and name, so the registry can track lifecycle events and capability changes accurately across redeployments and address rotations.

Don'ts

  1. Don't hardcode MCP server endpoints in agent configurations — Scattered per-agent config lists create silent failures when a server crashes or rotates its address, and leave no shared view of which tools exist across the mesh; that fragmentation is the exact problem MCPServerRegistry replaces with dynamic registration and a single query interface.
  2. Don't omit the ServerStatus.HEALTHY guard inside discover_tools — Returning tools from all _servers values regardless of health lets agents route requests to UNREACHABLE or DEGRADED servers at call time, converting what should be a clean registry-layer exclusion into unpredictable downstream failures.
  3. Don't overwrite _tool_index[tool_name] with a single-element list on each registration — Using self._tool_index[name] = [server_id] instead of setdefault(name, []).append(server_id) silently drops every earlier server that advertises the same tool name, destroying the multi-server redundancy the registry is built to track.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Solutions Architecture subscription.

From · cancel anytime

More free lessons in GenAI Architecture & Design Patterns

All free lessons in GenAI Solutions Architecture