MCP Server Discovery and Registry: Finding and Connecting to Available Tools
Learn how MCP clients discover available servers, understand server manifests and tool catalogs, and build a lightweight server registry that lets agents dynamically connect to the right tools.
The Discovery Problem
In a simple setup, an agent connects to a hardcoded list of MCP servers. But as organizations deploy dozens of MCP servers — one for databases, one for Slack, one for file systems, one for monitoring — the question becomes: how does an agent know which servers exist and which tools they offer?
This is the server discovery problem. It parallels service discovery in microservices architectures: you need a registry that catalogs available servers, their capabilities, and their connection details so that agents (or humans configuring agents) can find the right tools without manual configuration.
Static Configuration: The Starting Point
The simplest discovery mechanism is a JSON configuration file that lists available servers. Both Claude Desktop and the OpenAI Agents SDK support this pattern:
# mcp_servers.json — static server configuration
{
"servers": {
"database": {
"transport": "stdio",
"command": "python",
"args": ["servers/db_server.py"],
"description": "Query and manage application databases"
},
"filesystem": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
"description": "Read and write files in the data directory"
},
"monitoring": {
"transport": "http",
"url": "https://mcp.internal.company.com/monitoring",
"description": "Access application metrics and alerts"
}
}
}
This works for small teams but breaks down as the number of servers grows. Every time someone deploys a new MCP server, every agent configuration file needs to be updated.
Building a Server Registry
A server registry is a centralized catalog that MCP servers register with. Agents query the registry to discover available servers dynamically:
# registry_server.py — a simple MCP server registry
from mcp.server.fastmcp import FastMCP
import json
from datetime import datetime, timedelta
mcp_registry = FastMCP(name="MCPRegistry")
# In-memory registry (use a database in production)
_registry: dict[str, dict] = {}
@mcp_registry.tool()
async def register_server(
name: str,
description: str,
transport: str,
url: str | None = None,
command: str | None = None,
args: list[str] | None = None,
tags: list[str] | None = None,
) -> str:
"""Register an MCP server in the discovery registry.
Args:
name: Unique server name.
description: What this server does.
transport: Transport type - 'stdio' or 'http'.
url: Server URL (required for http transport).
command: Command to launch (required for stdio transport).
args: Command arguments (for stdio transport).
tags: Searchable tags describing server capabilities.
"""
_registry[name] = {
"name": name,
"description": description,
"transport": transport,
"url": url,
"command": command,
"args": args or [],
"tags": tags or [],
"registered_at": datetime.utcnow().isoformat(),
"last_heartbeat": datetime.utcnow().isoformat(),
}
return json.dumps({"registered": name})
@mcp_registry.tool()
async def discover_servers(
tag: str | None = None,
transport: str | None = None,
) -> str:
"""Discover available MCP servers, optionally filtered by tag or transport.
Args:
tag: Filter servers by this tag.
transport: Filter by transport type ('stdio' or 'http').
"""
cutoff = datetime.utcnow() - timedelta(minutes=5)
results = []
for server in _registry.values():
heartbeat = datetime.fromisoformat(server["last_heartbeat"])
if heartbeat < cutoff:
continue # Skip stale servers
if tag and tag not in server["tags"]:
continue
if transport and server["transport"] != transport:
continue
results.append(server)
return json.dumps({
"count": len(results),
"servers": results,
}, indent=2)
Server Manifests
Each MCP server can expose a manifest resource that describes itself in detail. This goes beyond what initialize returns — it includes documentation, examples, and dependency information:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
# In your MCP server, expose a manifest resource
@mcp_server.resource("manifest://server/info")
async def get_manifest() -> str:
"""Return the server manifest with capability details."""
import json
return json.dumps({
"name": "DatabaseServer",
"version": "2.1.0",
"description": "SQL query and management tools for PostgreSQL",
"author": "platform-team@company.com",
"documentation_url": "https://docs.internal/mcp/database",
"capabilities": {
"tools": [
{
"name": "query_db",
"category": "read",
"description": "Execute read-only SQL queries",
},
{
"name": "insert_record",
"category": "write",
"description": "Insert records into tables",
},
],
"resources": ["schema://tables", "metrics://db/stats"],
"prompts": ["analyze_table", "debug_slow_query"],
},
"dependencies": {
"requires_auth": True,
"auth_method": "api_key",
"rate_limit": "100 requests per minute",
},
}, indent=2)
Auto-Registration Pattern
Production MCP servers can register themselves with the registry on startup:
import httpx
async def auto_register(registry_url: str, server_info: dict):
"""Register this server with the central MCP registry."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{registry_url}/mcp",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "register_server",
"arguments": server_info,
},
},
)
print(f"Registration result: {response.json()}")
Combine auto-registration with periodic heartbeats, and the registry always reflects the current state of available servers. When a server goes down, its heartbeat expires and it disappears from discovery results.
Agent-Side Dynamic Connection
On the agent side, query the registry before building the agent's server list:
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHTTP
async def build_agent_with_discovery(task_tags: list[str]):
"""Build an agent that connects to servers matching the task."""
# Query the registry for relevant servers
registry = MCPServerStreamableHTTP(
name="Registry",
params={"url": "http://registry:8000/mcp"},
)
async with registry:
# Use the registry to find servers tagged for our task
# Then connect the agent to those servers
pass # Implementation depends on your agent framework
FAQ
Is there a standard MCP server registry protocol?
As of early 2026, there is no official MCP registry specification. The patterns described here are community-developed approaches. The MCP specification focuses on the client-server protocol, leaving discovery as an implementation concern. Expect a standardized registry protocol to emerge as the ecosystem matures.
How do I handle version conflicts between servers?
Include version information in server manifests and registry entries. When two servers expose tools with the same name, use the server name as a namespace prefix. Your agent configuration should specify which server version to prefer when conflicts arise.
Should agents discover servers at runtime or at configuration time?
For most production deployments, discover at configuration time and cache the server list. Runtime discovery adds latency and introduces a dependency on the registry being available. Reserve runtime discovery for long-running agents that need to adapt to new servers appearing in the ecosystem.
#MCP #ServiceDiscovery #ToolRegistry #AIAgents #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.