Free lesson · GenAI Solutions Architecture
Create MCP ecosystem governance dashboard
You will build an MCPEcosystemDashboard that provides comprehensive visibility into the health, adoption, and maturity of the enterprise MCP tool mesh. The dashboard integrates with the public **MCP Registry** at registry.modelcontextprotocol.io -- a centralized directory for discovering and auditing available MCP servers across the ecosystem. By syncing with the MCP Registry, the dashboard can cross-reference internal servers against the public catalog, identify when newer versions of community MCP servers are available, and flag servers that have been delisted or flagged for security issues upstream. This registry integration provides an external trust signal that complements the internal health and adoption metrics. Implement compute_ecosystem_health() -> EcosystemHealthReport that aggregates metrics across all registered MCP servers: overall availability (percentage of servers passing health checks), average tool latency across the mesh, authorization denial rate, composition safety score, circuit breaker trip frequency, and pre-filter cache hit rate. Define EcosystemHealthReport Pydantic model with fields overall_availability: float, avg_tool_latency_ms: float, auth_denial_rate: float, safety_score: float, circuit_breaker_trips_24h: int, server_count_by_status: dict[str, int], top_tools_by_usage: list[ToolUsageSummary], slowest_tools: list[ToolLatencySummary], most_denied_tools: list[ToolDenialSummary], report_generated_at: datetime. Build a Grafana dashboard with panels: (1) MCP Server Fleet Status showing server count by health state with mcp_registry_servers_total{status} as a pie chart with green/yellow/red segments, (2) Tool Invocation Heatmap displaying mcp_tool_usage_total{tool,outcome} across time with color intensity by volume, (3) Authorization Decision Breakdown using mcp_auth_decisions_total{decision,role} as stacked bar chart per hour, (4) Circuit Breaker State Map showing mcp_circuit_breaker_state{server_id,state} per server as a real-time status grid, (5) Composition Safety Trend tracking mcp_composition_safety_findings_total{severity} over rolling 7 days with annotation markers for new safety rules. Implement ToolAdoptionTracker with method compute_adoption_metrics(team: str | None = None) -> AdoptionReport that queries the tool_usage_stats table to compute per-team adoption metrics: which teams use which tools, tool discovery-to-first-use latency (time between server registration and first invocation from server_lifecycle_events and tool_usage_stats join), tool abandonment rate (tools not invoked in 30 days), and new tool adoption velocity (tools adopted per team per month). Store adoption snapshots in PostgreSQL tool_adoption_snapshots table with columns snapshot_id, team, tools_active, tools_discovered, tools_abandoned, adoption_velocity, snapshot_date. Build a FastAPI endpoint GET /api/v1/mcp/ecosystem/report generating the full ecosystem maturity report scored across five dimensions: server coverage (percentage of internal APIs exposed as MCP tools), authorization policy coverage (percentage of tools with explicit policies), composition test coverage (percentage of multi-tool workflows with validated compositions), documentation completeness (percentage of tools with descriptions over 100 characters), and SLA compliance rate (percentage of tools meeting their declared SLA contracts). Emit Prometheus gauges mcp_ecosystem_maturity_score{dimension}, mcp_ecosystem_tool_count, mcp_ecosystem_adoption_rate{team}, mcp_ecosystem_abandonment_rate, and mcp_ecosystem_overall_health. Build an Alertmanager rule that fires when mcp_ecosystem_maturity_score{dimension='authorization_coverage'} drops below 0.8 or when mcp_ecosystem_overall_health drops below 0.9.
Course: GenAI Architecture & Design Patterns · Chapter 11 · MCP Tool Mesh
Free to read — no subscription required.
Introduction
When your MCP tool mesh grows from a handful of tools to dozens across multiple teams, leadership stops asking "does it work?" and starts asking "what do we own, who runs it, and what is rotting?" Without a governance dashboard, schema drift goes unnoticed until callers crash in production, orphan tools accumulate review debt, and elevated-permission surfaces silently widen. By the end of this lesson you'll be able to design the four panel groups (inventory, quality, adoption, risk), aggregate registry and telemetry state on a schedule, and wire the dashboard into a deprecation workflow that turns signals into action.
Key Terminology
- MCP registry: the declarative store of every published tool —
tool_id, owner team, semver, sandbox profile, elevated permissions, and the schema hash the version was published with. - Telemetry projection: the observed-state view of each tool over a rolling window — call volume, success rate, P95 latency, error-class breakdown, observed schema hash, and the set of calling agents.
- Schema drift: the condition where a tool's
published_schema_hashin the registry differs from theobserved_schema_hashseen on the wire; the canonical risk-panel trigger. - Orphan tool: a registered tool whose caller set is empty for longer than
ORPHAN_GRACE_DAYS; the canonical signal that fans into the deprecation workflow. - Elevated permission: a sandbox capability outside the default profile (network egress, filesystem write, secret read); tracked as a closed enum so risk-panel counts aren't fractured by free-form strings.
- Materialized snapshot: the cached aggregate (
DashboardSnapshot) refreshed on a 5–15 minute schedule so panels render from one consistent join instead of live registry + telemetry queries per page load.
Concepts
What the dashboard must show
The dashboard's job is to compress every signal a platform owner cares about into four panel groups: ecosystem inventory, tool-quality metrics, adoption analytics, and risk panels. Each panel pulls from at least two sources — the registry (declarative state) and the telemetry pipeline (observed state) — and the value of governance comes from the join, not from either side alone.
Panel taxonomy
- Ecosystem inventory — every registered tool, owner team, semver, last health check, sandbox profile, and current request rate.
- Tool quality — success rate, P95 latency, error-class breakdown (timeout / 4xx / 5xx / schema-violation), and schema-violation rate over the trailing 7 days.
- Adoption analytics — caller-to-tool matrix, top tools by call volume, orphaned tools (zero callers in N days), and version-skew across consumers.
- Risk panels — stale sandbox profiles, tools holding elevated permissions (network egress, filesystem write, secret reads), and schema drift between the version published in the registry and the version observed on the wire.
Pitfalls
- Joining on stale registry snapshots. If your registry cache is older than the telemetry window, schema drift will fire false positives every time a tool publishes a new version. Pin the registry read to a transaction id or always read fresher than the telemetry window.
- Treating "low call volume" as "orphan." A nightly batch tool might fire eight times a month and still be load-bearing. Orphan detection must be
callers == empty, nevercalls < threshold, or you will retire critical tooling. - Letting elevated permissions live in free-form strings. If
elevated_permissionsis["net", "network", "egress"]across three teams, your risk panel undercounts. Enforce a closed enum at registration time. - Schema-drift alarms without a quarantine path. Detecting drift is easy; deciding what to do is hard. Wire the risk panel to an automatic version-pin rollback or a routing freeze, not just a Slack ping.
- EOL announcements without a forced migration deadline. "Deprecated" with no date is noise. Every EOL announcement must carry a
removal_attimestamp and the dashboard must show countdown days, not vague status text. - Sandbox profiles aging silently.
sandbox_profile_age_days > 90is a config smell, not a security incident — but ignored long enough it becomes one. Make stale-sandbox a blocker on the next version publish, not just a panel row. - Conflating tool owners with on-call. The owner team in the registry is for governance correspondence; paging during an incident needs a separate
on_call_rotationfield. Don't make the dashboard the source of truth for both. - Rendering the dashboard from live queries. A governance dashboard that costs a registry scan and a telemetry rollup per page load will be turned off the first time someone refreshes it during an incident. Materialize the snapshot every 5-15 minutes and serve from cache.
Code Walkthrough
Now that you have the panel taxonomy in hand, the walkthrough turns it into working code. The governance value comes from the join: the registry carries declarative state (owner, version, published schema hash, sandbox age, elevated permissions) while telemetry carries observed state (call volume, callers, observed schema hash). We model each side as a frozen dataclass keyed on tool_id, then aggregate them into a single DashboardSnapshot so panels render from one consistent view.
Code snippetpython
1from dataclasses import dataclass 2from datetime import datetime, timedelta, timezone 3 4@dataclass(frozen=True) 5class ToolRecord: 6 tool_id: str 7 owner_team: str 8 version: str 9 sandbox_profile_age_days: int 10 elevated_permissions: tuple[str, ...] 11 published_schema_hash: str 12 13@dataclass(frozen=True) 14class ToolTelemetry: 15 tool_id: str 16 calls_24h: int 17 observed_schema_hash: str 18 callers: frozenset[str] 19 last_called_at: datetime | None 20 21@dataclass(frozen=True) 22class DashboardSnapshot: 23 risk: list[dict] 24 adoption: dict 25 generated_at: datetime 26 27def _zero_telemetry(tool_id: str) -> ToolTelemetry: 28 return ToolTelemetry(tool_id, 0, "", frozenset(), None)
The aggregator joins the two streams on tool_id, then projects panel views. The risk panel computes schema drift as published != observed (guarded so a tool with no telemetry never false-positives), flags stale sandboxes, and surfaces elevated permissions. The adoption panel detects orphans — zero callers past ORPHAN_GRACE_DAYS — which later fan into the deprecation workflow.
Code snippetpython
1class MCPGovernanceDashboard: 2 ORPHAN_GRACE_DAYS = 14 3 SANDBOX_STALE_DAYS = 90 4 5 def __init__(self, registry, telemetry, clock=None): 6 self._registry = registry 7 self._telemetry = telemetry 8 self._now = clock or (lambda: datetime.now(timezone.utc)) 9 10 def snapshot(self) -> DashboardSnapshot: 11 records = {r.tool_id: r for r in self._registry.list_tools()} 12 telemetry = {t.tool_id: t for t in self._telemetry.window(hours=24)} 13 joined = [ 14 (rec, telemetry.get(tid, _zero_telemetry(tid))) 15 for tid, rec in records.items() 16 ] 17 return DashboardSnapshot( 18 risk=self._risk(joined), 19 adoption=self._adoption(joined), 20 generated_at=self._now(), 21 ) 22 23 def _risk(self, joined) -> list[dict]: 24 rows = [] 25 for rec, tel in joined: 26 flags = [] 27 if rec.sandbox_profile_age_days > self.SANDBOX_STALE_DAYS: 28 flags.append("stale_sandbox") 29 if rec.elevated_permissions: 30 flags.append("elevated_permissions") 31 if tel.observed_schema_hash and rec.published_schema_hash != tel.observed_schema_hash: 32 flags.append("schema_drift") 33 if flags: 34 rows.append({"tool_id": rec.tool_id, "owner": rec.owner_team, "flags": flags}) 35 return rows 36 37 def _adoption(self, joined) -> dict: 38 cutoff = self._now() - timedelta(days=self.ORPHAN_GRACE_DAYS) 39 orphans = [ 40 rec.tool_id 41 for rec, tel in joined 42 if not tel.callers and (tel.last_called_at is None or tel.last_called_at < cutoff) 43 ] 44 top = sorted(joined, key=lambda jt: jt[1].calls_24h, reverse=True)[:20] 45 return {"orphans": orphans, "top_by_volume": [r.tool_id for r, _ in top]}
You'll know it works when calling snapshot() against a registry holding one tool whose observed hash differs from its published hash and one tool with no callers past the grace window returns exactly one schema_drift entry under risk and that second tool_id in adoption["orphans"].
Do's and Don'ts
Do's
- ✓Do join
_registry.list_tools()and_telemetry.window(hours=24)into a singleDashboardSnapshotbefore projecting any panel — if_risk()and_adoption()each query the registry and telemetry independently, a schema push or new registration that arrives between calls creates cross-panel inconsistencies where the same tool appears in the risk panel but not the adoption panel within the same render cycle. - ✓Do fall back to
_zero_telemetry()for tools absent from the telemetry window, then guard the drift check withif tel.observed_schema_hashbefore comparing againstpublished_schema_hash— without both, every newly registered tool whose first call hasn't landed yet carries an empty observed hash that spuriously triggersschema_driftin the risk panel before any real drift has occurred. - ✓Do define orphan status with the compound condition
not tel.callers and (last_called_at is None or last_called_at < cutoff)usingORPHAN_GRACE_DAYSas the cutoff, not a call-volume floor — a nightly batch tool can showcalls_24h == 0in any given 24-hour window yet have alast_called_atinside the 14-day grace period; a volume threshold misclassifies it as orphaned and fans it into the deprecation workflow incorrectly.
Don'ts
- ✗Don't omit the
if tel.observed_schema_hashguard before thepublished_schema_hash != observed_schema_hashcomparison —_zero_telemetry()initializesobserved_schema_hashas an empty string, so an unguarded!=firesschema_driftfor every tool that has never been called in the window, drowning real drift signals in false positives and making the risk panel unactionable. - ✗Don't collapse
elevated_permissions,stale_sandbox, andschema_driftinto a single aggregated risk score without preserving the per-flag list — each flag in_risk()maps to a distinct remediation path (schema_drifttriggers a routing freeze or version-pin rollback,stale_sandboxtriggers a reconfiguration gate,elevated_permissionstriggers a permission-scope audit), and a scalar score discards exactly the routing information that determines which team acts and how. - ✗Don't use
calls_24hfrom the telemetry window as a proxy for the orphan check — the_adoption()method compareslast_called_atagainst theORPHAN_GRACE_DAYS(14-day) cutoff, not the 24-hour telemetry window; a tool on a weekly batch schedule will showcalls_24h == 0on most days but is not an orphan, and substituting a volume threshold enqueues load-bearing tools for deprecation.
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
- Ch 11Validate MCP tool composition correctness and safety
- Ch 11Build MCP tool routing with load balancing and failover
- Ch 11Create MCP ecosystem governance dashboardYou are here
- Ch 12Build A2A agent card registry with capability advertisement
- Ch 12Implement A2A task delegation with streaming artifact exchange
- Ch 12Validate A2A communication reliability with failure injection
- Ch 12Build A2A agent trust and authorization framework