<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Todd Fearn</title>
    <description>The latest articles on DEV Community by Todd Fearn (@tfearn).</description>
    <link>https://dev.to/tfearn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3882580%2F08e20ce6-226c-459b-a785-9f3a059eae3e.png</url>
      <title>DEV Community: Todd Fearn</title>
      <link>https://dev.to/tfearn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tfearn"/>
    <language>en</language>
    <item>
      <title>What is a Data Control Plane and why you need it. https://youtu.be/TfyeVkDOT0A</title>
      <dc:creator>Todd Fearn</dc:creator>
      <pubDate>Mon, 28 Sep 2026 16:27:12 +0000</pubDate>
      <link>https://dev.to/tfearn/what-is-a-data-control-plane-and-why-you-need-it-httpsyoutubetfyevkdot0a-243g</link>
      <guid>https://dev.to/tfearn/what-is-a-data-control-plane-and-why-you-need-it-httpsyoutubetfyevkdot0a-243g</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://youtu.be/TfyeVkDOT0A" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;youtu.be&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>When an Agent Touches Your Data, Lineage Is the Alibi</title>
      <dc:creator>Todd Fearn</dc:creator>
      <pubDate>Thu, 17 Sep 2026 22:21:23 +0000</pubDate>
      <link>https://dev.to/tfearn/when-an-agent-touches-your-data-lineage-is-the-alibi-khj</link>
      <guid>https://dev.to/tfearn/when-an-agent-touches-your-data-lineage-is-the-alibi-khj</guid>
      <description>&lt;p&gt;A risk manager once walked over to my desk holding a printout. One number on it was circled. Not wrong, exactly. Just different from the number on the same report the day before, and nobody had changed anything.&lt;/p&gt;

&lt;p&gt;I spent most of that afternoon working backwards. The report read a view, the view read a table, the table was loaded by a job, the job pulled from a vendor file, and the vendor had quietly resent the file at 11am with two rows corrected. Fine. Annoying, but fine. The thing I remember is how much of the afternoon was spent proving what &lt;em&gt;hadn't&lt;/em&gt; happened. Nobody edited the loader. Nobody touched the table. Once I could show that, the corrected file was the only suspect left.&lt;/p&gt;

&lt;p&gt;In practice, that's what I use lineage for. It's an alibi.&lt;/p&gt;

&lt;h2&gt;
  
  
  The suspect list just got longer
&lt;/h2&gt;

&lt;p&gt;In that story, every step in the chain was built by a person and left alone until a person changed it. The list of things that could have moved was short, and the humans who could have moved them all sat within shouting distance.&lt;/p&gt;

&lt;p&gt;Now picture the same report, except the loader was written by an agent. It was told "pull the daily positions file from this vendor into this table," wrote a Python script to do it, ran it, noticed a column had shifted last Thursday, and rewrote its own script to cope.&lt;/p&gt;

&lt;p&gt;Every one of those is a legitimate action. But walk back to the circled number and try to build the alibi. You need the run that landed the row and the version of the script it ran under, the one patched Thursday or the one before. Did the patch change what the column means? And which key did the patching, a person or a scheduled job with nobody watching?&lt;/p&gt;

&lt;p&gt;"It came from the positions table" answers none of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four questions, in order
&lt;/h2&gt;

&lt;p&gt;There are four questions, and they have to be answerable in this order, because each one narrows the next.&lt;/p&gt;

&lt;p&gt;Start with the run. A row needs to carry the identity of the run that wrote it. On the row, as a column, because the log somewhere else rotates after a week. Then when the risk manager circles a number, you can select the row and read off the run id and when it landed.&lt;/p&gt;

&lt;p&gt;Then the script. For agent-written feeds this is the one that keeps me up. The script that ran is a commit, and the row should carry the commit. I wrote back in July that agent code belongs in git and every run should be pinned to a SHA. With a SHA on the row, "the script that ran Thursday" is a diff you can open.&lt;/p&gt;

&lt;p&gt;Then the source, which is not the same thing as the tap. When a script edit switches the vendor, rows already landed should keep the source they were stamped with. Otherwise the graph tells you where the data comes from &lt;em&gt;now&lt;/em&gt;, which is a different question from where the circled number came from.&lt;/p&gt;

&lt;p&gt;Last, whose key ran it. That could be a person logged in, an agent holding an issued key, or an assistant acting on behalf of a person. Record the denials too. An agent that was refused permission to change something is evidence, because it tells you what &lt;em&gt;almost&lt;/em&gt; happened. In an audit, who moved it is the half they ask about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claims versus evidence
&lt;/h2&gt;

&lt;p&gt;One thing I got wrong early, and I think most lineage tools still get wrong: drawing the graph from configuration and calling it done.&lt;/p&gt;

&lt;p&gt;Configuration is a claim. It says pipeline A &lt;em&gt;is supposed to&lt;/em&gt; land into table B. Whether it did, when, how many rows, and whether it failed halfway through on Tuesday, none of that is in the config. So the graph should carry what actually traversed each hop, and an edge with nothing behind it should be drawn faint, because it's still a hypothesis.&lt;/p&gt;

&lt;p&gt;The same discipline applies inside the row. You can ask a model to read a transformation script and tell you which output column came from which input field. That's inference, and it has to say so. Anything it can't point to evidence for should come back as "unresolved." A lineage tool that fills gaps with plausible answers is worse than one with visible gaps. A gap at least sends you looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent needs the alibi too
&lt;/h2&gt;

&lt;p&gt;I've been writing this as though the reader of the lineage is a person. More and more often it's an agent.&lt;/p&gt;

&lt;p&gt;An agent about to change a tap should ask what's downstream of it first. Before it answers a question from a dataset, it should check whether anything has landed there lately and which run did it. None of that works if lineage is only a screen. It has to be something an agent can call as a tool, returning the records a person would read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I landed
&lt;/h2&gt;

&lt;p&gt;This is the ground I've been covering in &lt;a href="https://datris.ai" rel="noopener noreferrer"&gt;Datris&lt;/a&gt; over the last few weeks: an opt-in provenance stamp that puts run id, ingest time, definition version, script commit and source on every row a pipeline lands; run-level lineage recorded for every run; evidence on every edge of the graph; column lineage in two labeled tiers, exact and inferred; and an audit log that names the actor on every write. An agent can call all of it as tools.&lt;/p&gt;

&lt;p&gt;But you don't need my product to take the test. Go find a number on a dashboard that an agent-built feed produced. Try to build the alibi. Which run, which script, which source, who. Count how many of those you can answer from the row itself, and how many need a person who remembers.&lt;/p&gt;

&lt;p&gt;The risk manager with the printout is coming back. This time, the loader has no one to shout across the room to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Todd Fearn is the founder of &lt;a href="https://datris.ai" rel="noopener noreferrer"&gt;Datris.ai&lt;/a&gt;, an open-source, agent-native data platform built on the Model Context Protocol, and he runs IData Corporation, a data engineering consultancy for financial services firms. He has spent about thirty years building production data infrastructure inside institutions like Goldman Sachs, Bridgewater Associates, Deutsche Bank, and Freddie Mac.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Market Intelligence Agent That Operates a Data Platform Autonomously via MCP</title>
      <dc:creator>Todd Fearn</dc:creator>
      <pubDate>Tue, 21 Apr 2026 16:01:52 +0000</pubDate>
      <link>https://dev.to/tfearn/i-built-a-market-intelligence-agent-that-operates-a-data-platform-autonomously-via-mcp-5g2h</link>
      <guid>https://dev.to/tfearn/i-built-a-market-intelligence-agent-that-operates-a-data-platform-autonomously-via-mcp-5g2h</guid>
      <description>&lt;p&gt;Most AI agent demos follow the same pattern: pre-loaded dataset, clever prompt, nice answer. The infrastructure is invisible and static.&lt;/p&gt;

&lt;p&gt;I wanted to show something different — an agent that &lt;em&gt;operates&lt;/em&gt; the data infrastructure itself. Creates pipelines, ingests live data, manages the lifecycle, then answers questions against what it just built. No pre-loaded anything.&lt;/p&gt;

&lt;p&gt;This is a working example on top of &lt;a href="https://datris.ai" rel="noopener noreferrer"&gt;Datris&lt;/a&gt;, an agent-native data platform I've been building (with Claude helping, full transparency). The platform exposes data pipeline operations as MCP tools. The agent discovers what's available and figures out the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video walkthrough:&lt;/strong&gt; &lt;a href="https://datris.ai/videos/market-intelligence-agent-mcp" rel="noopener noreferrer"&gt;https://datris.ai/videos/market-intelligence-agent-mcp&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;The Market Intelligence Agent is a FastAPI + vanilla JS app (no Node) that connects to the Datris MCP server and handles a full financial data workflow end-to-end — live, in the browser.&lt;/p&gt;

&lt;p&gt;Two example queries that show the pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What's the current macro picture?"&lt;/strong&gt;&lt;br&gt;
The agent doesn't query a database. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Discovers available MCP tools via &lt;code&gt;tools/list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Decides it needs FRED macro data (yields, VIX, CPI, credit spreads) + equity OHLCV&lt;/li&gt;
&lt;li&gt;Creates pipelines for each source&lt;/li&gt;
&lt;li&gt;Ingests live data from the public APIs&lt;/li&gt;
&lt;li&gt;Queries the results and returns a cited answer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;"Is crypto confirming the risk-on trade in equities?"&lt;/strong&gt;&lt;br&gt;
Cross-asset reasoning across BTC/ETH/SOL (CoinGecko) vs SPY/QQQ/TLT/GLD/XLE/IWM — pipelines created and ingested on the fly, same pattern.&lt;/p&gt;

&lt;p&gt;All data sources are free: FRED, yfinance, CoinGecko, SEC EDGAR (10-K/10-Q).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (vanilla JS)
    │
    ├── Chat panel → FastAPI → Anthropic API
    │                              │
    │                         MCP Client → Datris MCP Server
    │                                           │
    │                                    Pipeline Lifecycle
    │                                    (create, ingest, query)
    │
    └── Pipeline status panel ← SSE stream (live tiles + activity feed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent connects to the Datris MCP server and calls &lt;code&gt;tools/list&lt;/code&gt; at startup to discover what operations are available — it's not hardcoded to any specific tool names. Background refresh keeps active pipelines current via SSE.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why MCP for This
&lt;/h2&gt;

&lt;p&gt;The interesting design choice here is using MCP as the control plane between the agent and the data platform rather than building a custom API layer.&lt;/p&gt;

&lt;p&gt;The agent doesn't need to know ahead of time what pipelines exist or what data sources are configured. It discovers capabilities at runtime, decides what it needs, and operates accordingly. Add a new tool to the MCP server and the agent picks it up automatically.&lt;/p&gt;

&lt;p&gt;This is the pattern I think is underexplored: agents that manage infrastructure, not just agents that query infrastructure someone else built.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run It Yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the platform&lt;/span&gt;
git clone https://github.com/datris/datris-platform-oss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example code:&lt;/strong&gt; &lt;a href="https://github.com/datris/datris-platform-oss/tree/main/examples/market-macro-agent" rel="noopener noreferrer"&gt;https://github.com/datris/datris-platform-oss/tree/main/examples/market-macro-agent&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Platform (AGPL, open core):&lt;/strong&gt; &lt;a href="https://github.com/datris/datris-platform-oss" rel="noopener noreferrer"&gt;https://github.com/datris/datris-platform-oss&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://docs.datris.ai" rel="noopener noreferrer"&gt;https://docs.datris.ai&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://datris.ai" rel="noopener noreferrer"&gt;https://datris.ai&lt;/a&gt;  &lt;/p&gt;




&lt;p&gt;Happy to answer questions on the MCP server design, SSE streaming approach, or pipeline architecture in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>From Demo to Production: Building an MCP Control Plane Inside Your Agent Architecture</title>
      <dc:creator>Todd Fearn</dc:creator>
      <pubDate>Thu, 16 Apr 2026 13:43:29 +0000</pubDate>
      <link>https://dev.to/tfearn/from-demo-to-production-building-an-mcp-control-plane-inside-your-agent-architecture-34d7</link>
      <guid>https://dev.to/tfearn/from-demo-to-production-building-an-mcp-control-plane-inside-your-agent-architecture-34d7</guid>
      <description>&lt;p&gt;Most AI agent demos are impressive for about five minutes.&lt;/p&gt;

&lt;p&gt;Then someone asks: &lt;em&gt;Who authorized that tool call? What data did it access? Can we audit that?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer is usually silence.&lt;/p&gt;

&lt;p&gt;That's the gap between a demo and a production system. And the thing that closes it isn't a better prompt — it's a control plane inserted between your agent and every MCP tool it can reach.&lt;/p&gt;

&lt;p&gt;Here's how to build it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;The agent has one address: the control plane. That's it.&lt;/p&gt;

&lt;p&gt;It doesn't know where MCP servers live. It doesn't hold credentials. It can't route around policy. The control plane is the only entity in the system with network access to the MCP layer — and the agent talks to nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent  →  Control Plane  →  MCP Server A (market data)
              ↑          →  MCP Server B (trade execution)
         Tool Registry   →  MCP Server C (research/docs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The control plane is a router. The agent calls two endpoints — &lt;code&gt;/tools&lt;/code&gt; at startup, &lt;code&gt;/invoke&lt;/code&gt; at runtime — and has no idea how many MCP servers sit behind them. That topology is entirely the control plane's concern.&lt;/p&gt;

&lt;p&gt;Four pillars enforce this. All code. No prompt engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Discovery: The Registry Layer
&lt;/h2&gt;

&lt;p&gt;This is where most teams get it wrong. They hard-code tool definitions into the agent's system prompt and call it done. The agent sees everything. That's not access control — it's a flat namespace with a label on it.&lt;/p&gt;

&lt;p&gt;The correct model: the agent discovers tools through the control plane, not from MCP servers directly.&lt;/p&gt;

&lt;p&gt;On startup, the agent calls &lt;code&gt;/tools&lt;/code&gt;. The control plane queries a Service Catalog — a DynamoDB table or a GitOps-managed JSON file — and returns only the tool names that agent's role is permitted to see. Crucially, the response contains names only. No server URLs. No routing information. The agent gets a menu, not a map.&lt;/p&gt;

&lt;p&gt;That filtered list is what populates the agent's tool definitions.&lt;/p&gt;

&lt;p&gt;A "Research Agent" is never told that "Execute Trade" exists. It can't attempt to call it. It can't be jailbroken into calling it. The tool is simply absent from its world.&lt;/p&gt;

&lt;p&gt;That's least privilege at discovery time — before a single request is ever made.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Identity: Scoped, Short-Lived Credentials
&lt;/h2&gt;

&lt;p&gt;Agents should never own long-lived credentials. If they do, a compromised agent is a compromised system.&lt;/p&gt;

&lt;p&gt;The pattern: agents operate on Service Tokens or OIDC delegation. When an agent wants to invoke a tool, the control plane validates its Service Identity first. If valid, it mints a short-lived, scoped token from your IAM provider — AWS STS or HashiCorp Vault — specific to that one tool call, scoped to the specific MCP server being called.&lt;/p&gt;

&lt;p&gt;The token expires. The blast radius of a compromised agent is bounded to a single, already-expiring credential against one server.&lt;/p&gt;

&lt;p&gt;This is standard infrastructure security. Apply it to agents exactly the same way.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Policy: Hard Guardrails in Code
&lt;/h2&gt;

&lt;p&gt;This is the most important pillar — and the one most teams skip.&lt;/p&gt;

&lt;p&gt;The instinct is to put guardrails in the prompt. &lt;em&gt;"Never move funds in production."&lt;/em&gt; That's not a control. That's a suggestion. LLMs hallucinate. They get jailbroken. Prompts drift.&lt;/p&gt;

&lt;p&gt;Move the logic into code. Use Open Policy Agent (OPA) and write Rego policies that the control plane evaluates before any request touches an MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"read_positions"&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"analyst"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"move_funds"&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the policy evaluates to false, the request is dropped. Not logged. Not flagged for review. Dropped.&lt;/p&gt;

&lt;p&gt;The LLM doesn't get a vote on this. It never reaches the tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Observability: The Audit Wrapper
&lt;/h2&gt;

&lt;p&gt;Every interaction gets wrapped in a logging decorator. Structured JSON. Four fields that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; — Agent ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; — MCP tool invoked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where&lt;/strong&gt; — Data source targeted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was&lt;/strong&gt; — Result or failure status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ship those logs to Kafka or an ELK stack. More importantly, propagate a &lt;code&gt;trace_id&lt;/code&gt; from the initial user prompt all the way through the control plane to the MCP server and back. Every hop in the chain carries the same ID.&lt;/p&gt;

&lt;p&gt;When a compliance audit lands on your desk, that trace ID lets you reconstruct the entire chain of thought and data access path for any agent, any session, any point in time.&lt;/p&gt;

&lt;p&gt;That's not a nice-to-have. In regulated environments, it's the requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;Here's a minimal Python control plane. Two endpoints: one for discovery, one for invocation. The agent calls both. It calls nothing else.&lt;/p&gt;

&lt;p&gt;The tool registry maps each role's actions directly to the MCP server that handles them. The agent never sees this mapping — it only sees the tool names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_plane&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Tool registry: maps role → action → MCP server URL ---
# The agent sees tool names only. Server URLs stay here.
&lt;/span&gt;
&lt;span class="n"&gt;TOOL_REGISTRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_positions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://mcp-marketdata:8001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_report&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://mcp-research:8003&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_positions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://mcp-marketdata:8001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_trade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://mcp-execution:8002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;POLICY_RULES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_trade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# hard block
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_scoped_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mcp_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# In production: call Vault or AWS STS, scoped to this server
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tok_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# --- Request models ---
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DiscoveryRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;agent_role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToolRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;agent_role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;# --- 1. Discovery endpoint ---
# Returns tool names only. No URLs. No routing info.
# The agent builds its tool definitions from this list.
&lt;/span&gt;
&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DiscoveryRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;role_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;tool_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role_tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools_returned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_names&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_names&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# --- 2. Invocation endpoint ---
# Resolves the MCP server, enforces policy, mints a token,
# and calls the MCP server. The agent has no direct route to any of this.
&lt;/span&gt;
&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/invoke&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;trace_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;

    &lt;span class="n"&gt;role_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

    &lt;span class="c1"&gt;# Discovery check — is this tool in this agent's allowed list?
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;role_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denied_discovery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tool not available for this role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Policy check — hard rules, no exceptions
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;POLICY_RULES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denied_policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Action blocked by policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Resolve the MCP server for this action
&lt;/span&gt;    &lt;span class="n"&gt;mcp_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;role_tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Mint a short-lived token scoped to this server and action
&lt;/span&gt;    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_scoped_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mcp_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Control plane calls the MCP server — not the agent
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mcp_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/invoke&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Trace-Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Structured audit log — includes which MCP server was called
&lt;/span&gt;    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp_server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mcp_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trace_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's startup sequence is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Agent startup — discover available tools through the control plane
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://control-plane/tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;available_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# → ["read_positions", "run_report"]
# No URLs. No server topology. Just the tool names it's allowed to use.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, every tool call goes to &lt;code&gt;/invoke&lt;/code&gt;. The control plane resolves the server, mints the token, and makes the call. The agent is completely isolated from the MCP layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Router Gives You
&lt;/h2&gt;

&lt;p&gt;Routing through the control plane isn't just indirection for its own sake. It unlocks capabilities that a direct agent-to-MCP connection can never have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out&lt;/strong&gt;: A single action can call multiple MCP servers and aggregate results — useful for cross-system reads where the agent shouldn't care about the seams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning&lt;/strong&gt;: Route &lt;code&gt;read_positions_v2&lt;/code&gt; to a new MCP server without the agent knowing anything changed. Zero agent-side deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaking&lt;/strong&gt;: If &lt;code&gt;mcp-execution&lt;/code&gt; is down, the control plane fails fast and returns a clean error rather than hanging the agent mid-task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-server token scoping&lt;/strong&gt;: Each MCP server gets a credential scoped specifically to it — not a shared token that opens the entire layer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;If you're building this today, the control plane looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: Python or Go&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol&lt;/strong&gt;: FastMCP or the official MCP SDK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy engine&lt;/strong&gt;: Open Policy Agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt;: Redis for tool schemas and server topology&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit store&lt;/strong&gt;: PostgreSQL for the trail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. It's the same infrastructure patterns that have governed data pipelines and API gateways for years. The only thing new is the surface being controlled.&lt;/p&gt;

&lt;p&gt;The full working source code for this control plane is available at &lt;a href="https://github.com/datris/sample-code" rel="noopener noreferrer"&gt;github.com/datris/sample-code&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;The LLM is not the system. The LLM is a component of the system.&lt;/p&gt;

&lt;p&gt;An MCP control plane is what enforces that distinction. The agent discovers tools through it, invokes tools through it, and never holds a credential or a server address. Add as many MCP servers as your architecture demands — the agent doesn't care, because it was never exposed to the topology in the first place.&lt;/p&gt;

&lt;p&gt;Discovery, identity, policy, and observability — implemented as code, not instructions — is what takes an agent from a demo that works most of the time to infrastructure you can actually run in production.&lt;/p&gt;

&lt;p&gt;Build the control plane. Then let the agents loose.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>agents</category>
      <category>agentskills</category>
    </item>
  </channel>
</rss>
