MCP (Model Context Protocol) turns your feature store, metric layer, and experiment tracker into governed tools any MCP-capable agent or assistant can call — one server, many clients, instead of bespoke tool code per agent.
Tier: Advanced · Workflow area: Agentic systems
At production scale this pattern carries serious weight (Uber's MCP gateway reports ~60k agent task executions weekly, with authorization and PII redaction enforced per tool call). This playbook builds a read-only MCP server over your own platform with those same three controls: scoped tools, auth, and redaction.
Skip MCP when exactly one agent needs the capability — a plain function tool in that agent's code is less moving parts.
pip install "mcp[cli]" (official Python SDK; FastMCP interface).mcp dev server.py).Choose 3–4 narrow, read-only tools. Good set: list_metrics() (metric layer catalog), query_metric(name, grain, filters) (parameterized — not free-form SQL), get_experiment_runs(experiment, max_results) (MLflow), describe_table(table) (schema only). Narrow tools with typed parameters are the governance surface — an agent cannot exfiltrate what no tool exposes.
Scaffold the server.
from mcp.server.fastmcp import FastMCP
import mlflow
mcp = FastMCP("data-platform")
@mcp.tool()
def query_metric(name: str, grain: str = "week", segment: str | None = None) -> str:
"""Return values for a governed metric at the given grain."""
sql = METRIC_TEMPLATES[name].render(grain=grain, segment=segment) # allowlist
return redact(run_readonly(sql))
@mcp.tool()
def get_experiment_runs(experiment: str, max_results: int = 10) -> str:
"""List recent MLflow runs with params and metrics."""
runs = mlflow.search_runs(experiment_names=[experiment], max_results=max_results)
return redact(runs.to_markdown())
if __name__ == "__main__":
mcp.run() # stdio for local clients; use streamable HTTP for remote
Enforce read-only at the database, not in the prompt. Connect with a warehouse role that has SELECT only, statement timeouts, and row limits. Template-render SQL from an allowlist of metric definitions; reject any tool design that passes raw SQL from the model.
Add PII redaction on every return path. A redact() function over all tool outputs: regex/deny-list for emails, phone numbers, and your org's ID formats, plus a column deny-list (drop email, address, etc. before serializing). Redaction lives in the server so every client inherits it.
Add auth appropriate to transport. Local stdio: the server runs as you — scope the database credential it holds. Remote (streamable HTTP): put it behind your gateway with per-client tokens, and log every tool call with caller, tool, args, and row count — that log is your audit trail.
Register with a real client and run an end-to-end task: "Which metric moved most WoW, and did any experiment ship that week?" — the agent should compose list_metrics → query_metric → get_experiment_runs.
Governance lives in the tool definitions: typed parameters, allowlisted queries, a read-only role, and server-side redaction bind every current and future agent at once — prompts guarantee none of this.
mcp_servers + mcp_toolset, beta mcp-client-2025-11-20) to call your remote server directly from the Messages API without client-side plumbing.DELETE FROM users") fails at the database layer, and the attempt is visible in the audit log.INSERT attempt through the server's connection.run_sql(query) tool. Free-form SQL from a model is the whole threat model in one tool. Parameterized templates or nothing.The whole stack self-hosts: MCP server on your infra, warehouse role-scoped, MLflow self-hosted, and the consuming agent on a vLLM-served open-weight model. For client platforms, run the server inside the client's network boundary and expose it to agents only via stdio or an internal endpoint — MCP's transport flexibility is exactly what makes the compliant topology possible.
An MCP server is an API product — version it, changelog it, and stage schema changes; silently renaming a tool breaks every downstream agent at once.