<?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: Hasnain Arif</title>
    <description>The latest articles on DEV Community by Hasnain Arif (@justhsnn).</description>
    <link>https://dev.to/justhsnn</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%2F3919727%2Fb2de8405-98cb-4746-b2fe-a51cd167cade.png</url>
      <title>DEV Community: Hasnain Arif</title>
      <link>https://dev.to/justhsnn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/justhsnn"/>
    <language>en</language>
    <item>
      <title>Why Your MCP Server Breaks Silently When the SDK Renames Things Underneath You</title>
      <dc:creator>Hasnain Arif</dc:creator>
      <pubDate>Tue, 15 Sep 2026 16:41:34 +0000</pubDate>
      <link>https://dev.to/justhsnn/why-your-mcp-server-breaks-silently-when-the-sdk-renames-things-underneath-you-5b8a</link>
      <guid>https://dev.to/justhsnn/why-your-mcp-server-breaks-silently-when-the-sdk-renames-things-underneath-you-5b8a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmtfhdjh16ycwru6awh2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdmtfhdjh16ycwru6awh2.jpg" alt="Dark terminal window showing lines of code and command output, representing a developer debugging a broken build." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On July 28, 2026, the Model Context Protocol Python SDK shipped v2.0.0 and renamed its built-in &lt;code&gt;FastMCP&lt;/code&gt; class to &lt;code&gt;MCPServer&lt;/code&gt;. No deprecation warning window, no shim left behind for the old import path: &lt;code&gt;mcp.server.fastmcp&lt;/code&gt; moved wholesale to &lt;code&gt;mcp.server.mcpserver&lt;/code&gt;. For any project that pinned &lt;code&gt;mcp&lt;/code&gt; loosely in &lt;code&gt;requirements.txt&lt;/code&gt;, the next &lt;code&gt;pip install&lt;/code&gt; didn't fail loudly. It just quietly resolved to a version where half the imports no longer existed.&lt;/p&gt;

&lt;p&gt;I ran into a version of this problem while building an open-source MCP server that exposes 70 Highcharts chart types to AI agents (more on it below). The rename itself didn't break my server, but the pattern it exposed, an SDK vendor moving a name without warning, and a dependency graph with no version floor to catch it, is exactly the failure mode a two-tier tool architecture is built to survive. This is the story of that architecture, why I landed on it before I knew about the rename, and why it turned out to matter more than I expected.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP Python SDK v2.0.0 (July 28, 2026) renamed the built-in &lt;code&gt;FastMCP&lt;/code&gt; class to &lt;code&gt;MCPServer&lt;/code&gt; and moved every &lt;code&gt;mcp.server.fastmcp.*&lt;/code&gt; submodule to &lt;code&gt;mcp.server.mcpserver.*&lt;/code&gt;, with no deprecation shim.&lt;/li&gt;
&lt;li&gt;The break is silent by design: unpinned &lt;code&gt;mcp&lt;/code&gt; dependencies resolve to 2.x on a fresh install, and nothing in the affected repo has to change for it to happen.&lt;/li&gt;
&lt;li&gt;A subtler companion break: the server's default &lt;code&gt;serverInfo.name&lt;/code&gt; changed from &lt;code&gt;"FastMCP"&lt;/code&gt; to &lt;code&gt;"mcp-server"&lt;/code&gt;, silently altering client-visible identity with no exception raised.&lt;/li&gt;
&lt;li&gt;This is not a freak event: general-ecosystem data shows 20-28% of "safe" minor/patch releases introduce breaking API changes, and renames are a recognized, common category of breaking change, not an edge case.&lt;/li&gt;
&lt;li&gt;A two-tier tool design, one guided/validated path plus one raw/passthrough path, contains this kind of breakage because the guided tier can absorb an SDK migration internally without changing its contract to callers.&lt;/li&gt;
&lt;li&gt;Don't confuse this with the standalone third-party FastMCP project (Jeremiah Lowin, now v4.0): that's a different, unaffected codebase that predates and inspired the SDK's built-in class.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Rename Nobody Announced Loudly Enough
&lt;/h2&gt;

&lt;p&gt;The MCP Python SDK's &lt;code&gt;FastMCP&lt;/code&gt; class was the decorator-based, batteries-included way to stand up an MCP server: &lt;code&gt;@mcp.tool()&lt;/code&gt;, run it, done. In v2.0.0, the SDK team renamed it to &lt;code&gt;MCPServer&lt;/code&gt; and moved the supporting submodules from &lt;code&gt;mcp.server.fastmcp&lt;/code&gt; to &lt;code&gt;mcp.server.mcpserver&lt;/code&gt;. The official &lt;a href="https://py.sdk.modelcontextprotocol.io/migration/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; confirms the scope: "FastMCP is now MCPServer, and there is a first-class Client." The decorator API itself didn't change, and the low-level &lt;code&gt;Server&lt;/code&gt; was rebuilt around a shared dispatcher engine, but the import path every existing project depended on simply moved, a change also confirmed in the &lt;a href="https://github.com/modelcontextprotocol/python-sdk/releases" rel="noopener noreferrer"&gt;v2.0.0 GitHub release notes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The rename didn't fail loudly because Python doesn't require you to declare an upper version bound. A &lt;code&gt;requirements.txt&lt;/code&gt; line like &lt;code&gt;mcp&lt;/code&gt; or &lt;code&gt;mcp&amp;gt;=1.0&lt;/code&gt; has no ceiling, so pip has no reason to stop at 1.x. The break only becomes visible at import time, in whichever environment happens to run the next fresh install, which is often a CI pipeline or a new contributor's machine, not the original author's.&lt;/p&gt;

&lt;p&gt;The real-world confirmation of this is a &lt;a href="https://github.com/datalayer/jupyter-mcp-server/issues/325" rel="noopener noreferrer"&gt;GitHub issue from the &lt;code&gt;jupyter-mcp-server&lt;/code&gt; project&lt;/a&gt;: "mcp 2.0.0 breaks imports: FastMCP renamed to MCPServer, module moved." Nothing in that repository had changed. Only the dependency resolution had. That's the pattern worth internalizing: the codebase was correct yesterday and broken today, and the diff that caused it lives in someone else's repository, not yours.&lt;/p&gt;

&lt;p&gt;There's a second, quieter change in the same release that's arguably worse for debugging. The server's default &lt;code&gt;serverInfo.name&lt;/code&gt;, the identity string a client sees when it connects, changed from &lt;code&gt;"FastMCP"&lt;/code&gt; to &lt;code&gt;"mcp-server"&lt;/code&gt;. This doesn't raise an exception. It doesn't show up in a stack trace. It just silently changes what your server reports itself as to every connected client, which is the kind of thing that surfaces three weeks later as "wait, why does our logging dashboard show a different server name than last month."&lt;/p&gt;

&lt;p&gt;Before going further: there are two different things named "FastMCP," and conflating them is a common mistake. The class that got renamed is the MCP Python SDK's &lt;em&gt;built-in&lt;/em&gt; &lt;code&gt;FastMCP&lt;/code&gt;, maintained by the &lt;code&gt;modelcontextprotocol/python-sdk&lt;/code&gt; project. The standalone third-party FastMCP framework, built independently by Jeremiah Lowin and now at v4.0 with its own background tasks, stateless interactivity, and enterprise auth features, is a separate project. It predates and inspired the SDK's built-in class, and it is not being renamed or discontinued. If you're using the standalone framework, this migration doesn't touch you. If you're importing &lt;code&gt;mcp.server.fastmcp&lt;/code&gt;, it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is a Pattern, Not a One-Off
&lt;/h2&gt;

&lt;p&gt;It's tempting to read the FastMCP rename as an unusually careless move by one SDK team. The broader dependency-management literature says otherwise: this is what healthy, actively maintained ecosystems do on a predictable cadence, and semantic versioning alone doesn't stop it.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://arxiv.org/html/2605.24397v1" rel="noopener noreferrer"&gt;2026 systematic literature review&lt;/a&gt; covering 97 primary studies on breaking changes in software ecosystems found that roughly 20% of &lt;em&gt;non-major&lt;/em&gt; Maven releases, the ones semver promises are safe to auto-upgrade, introduce a public API break anyway. Across the full release history of Maven artifacts, 67% violate semver at least once. The Go ecosystem, often held up as stricter about compatibility, still shows 28.6% of non-major upgrades introducing breaking changes, against an 86.3% overall semver-adherence rate, and 33.3% of downstream client programs in &lt;a href="https://arxiv.org/pdf/2309.02894" rel="noopener noreferrer"&gt;that study&lt;/a&gt; were affected by at least one breaking change from an upgrade they had reason to trust.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ecosystem&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maven&lt;/td&gt;
&lt;td&gt;Non-major releases that break the API&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maven&lt;/td&gt;
&lt;td&gt;Artifacts that violate semver at least once&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Non-major upgrades that introduce breaking changes&lt;/td&gt;
&lt;td&gt;28.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Overall semver adherence&lt;/td&gt;
&lt;td&gt;86.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Source: arXiv 2605.24397v1 (May 2026), arXiv 2309.02894 (2023)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The same literature review breaks down &lt;em&gt;what kind&lt;/em&gt; of breaking change hits consumers most often. Behavioral changes dominate at 68.1% (1,034 of 1,519 studied breaking changes), followed by removal at 13.9%, signature changes at 8.8%, and renames at 7.3%. A rename isn't a freak occurrence sitting outside the normal taxonomy. It's a recognized, regularly occurring category, and from an importer's perspective it behaves exactly like removal-plus-addition: the old symbol is gone, a new one exists somewhere else, and your code doesn't know to look for it. In Python specifically, the same research found removals account for 96.4% of breaking changes in that ecosystem, which is the closest verified proxy for what FastMCP → MCPServer actually did to every &lt;code&gt;from mcp.server.fastmcp import FastMCP&lt;/code&gt; line in the wild.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Breaking change category&lt;/th&gt;
&lt;th&gt;Share of 1,519 studied cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Behavioral change&lt;/td&gt;
&lt;td&gt;68.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removal&lt;/td&gt;
&lt;td&gt;13.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature change&lt;/td&gt;
&lt;td&gt;8.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename&lt;/td&gt;
&lt;td&gt;7.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Source: arXiv 2605.24397v1 (May 2026)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to be precise about what I'm not claiming here. There is no verified, MCP-specific statistic on what percentage of MCP servers carry unpinned dependencies. That number doesn't exist yet in any published study I could find. What does exist is the scale of what's exposed if the general pattern holds: the MCP ecosystem now counts more than 10,000 active public servers and over 97 million monthly SDK downloads across Python and TypeScript, with the four Tier-1 SDKs (TypeScript, Python, Go, C#) approaching roughly 500 million combined monthly downloads, and the TypeScript and Python SDKs individually past 1 billion downloads all-time, per &lt;a href="https://www.anthropic.com/news/donating-the-model-context-protocol-and-establishing-of-the-agentic-ai-foundation" rel="noopener noreferrer"&gt;Anthropic's December 2025 announcement&lt;/a&gt; and the &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;MCP Blog's July 2026 specification post&lt;/a&gt;. Governance moved to the Agentic AI Foundation, a Linux Foundation-directed fund co-founded by Anthropic, Block, and OpenAI, with Google, Microsoft, AWS, Cloudflare, and Bloomberg as supporting organizations. That's a large, fast-growing surface area for a well-documented, ecosystem-wide failure pattern to land on. The specific MCP number isn't measured yet; the general-ecosystem rate is the closest honest proxy, and it's not small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Two-Tier MCP Server: The Tool Design I Landed On Before I Knew Why It Mattered
&lt;/h2&gt;

&lt;p&gt;I built an open-source MCP server, &lt;code&gt;highchart-mcp&lt;/code&gt;, that exposes all 70 Highcharts chart types to AI agents like Claude, published as &lt;a href="https://www.npmjs.com/package/@highchart-mcp/server" rel="noopener noreferrer"&gt;&lt;code&gt;@highchart-mcp/server&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;@highchart-mcp/sdk&lt;/code&gt; on npm, mirrored on PyPI, with a Docker image included.&lt;/p&gt;

&lt;p&gt;Most MCP servers I looked at before starting this project gave the model exactly one rigid way to accomplish a task. That works fine until the model needs something the tool's author didn't anticipate, and then it's a dead end: the model can't express the request, the tool rejects it, and the conversation stalls on a limitation nobody documented because nobody expected it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.pexels.com%2Fphotos%2F34803985%2Fpexels-photo-34803985.jpeg%3Fcs%3Dsrgb%26fm%3Djpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.pexels.com%2Fphotos%2F34803985%2Fpexels-photo-34803985.jpeg%3Fcs%3Dsrgb%26fm%3Djpg" alt="Laptop screen showing a code editor with programming code in a dimly lit workspace." width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix I landed on was splitting the tool surface into two tiers. &lt;code&gt;create_chart&lt;/code&gt; is the guided, validated path: the model sends structured input describing what it wants (chart type, series, labels), and gets a working chart back with safe defaults filled in. There's no way to hand it a broken config, because the tool itself won't accept one. &lt;code&gt;render_chart&lt;/code&gt; and &lt;code&gt;export_chart&lt;/code&gt; are the raw passthrough path: the model sends a full Highcharts options object directly, with zero guardrails, for the cases where the guided tool's structured input can't express what's needed.&lt;/p&gt;

&lt;p&gt;The guided tool handles the large majority of real calls. The raw one exists so the remainder don't dead-end. In practice, that means a model can ask for "a stacked bar chart of quarterly revenue by region" through &lt;code&gt;create_chart&lt;/code&gt; and get something correct on the first try, no Highcharts knowledge required. But if it needs a chart type or a configuration option that &lt;code&gt;create_chart&lt;/code&gt;'s schema doesn't cover, &lt;code&gt;render_chart&lt;/code&gt; is still there, accepting the same options object a human developer would hand Highcharts directly.&lt;/p&gt;

&lt;p&gt;This felt obvious once it was built, but it wasn't obvious while building it. The natural instinct when designing a tool is to model the cases you can picture: a handful of chart types, a handful of configuration patterns, ship it. That instinct produces a tool that's clean and well-documented and works great for the cases the author thought of, and silently fails for everything else. The raw passthrough tier exists specifically to undercut that instinct: it's an admission, built into the architecture, that the guided tier's schema will never cover every case, so there has to be an escape hatch that doesn't require waiting for the next release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Same Instinct That Breaks SDKs Also Breaks Tools
&lt;/h2&gt;

&lt;p&gt;Here's the connection that took longer to see than the tool design itself: the instinct that produces a rigid, single-path tool is the same instinct that lets an SDK migration quietly break every caller at once. Both come from the same assumption, that you know, in advance, every way the thing will be used and every path the dependency graph will take. A tool author who assumes they've anticipated every model request builds one rigid path. An SDK maintainer who assumes an import path will never move ships a rename with no shim. Neither assumption survives contact with a large enough user base for long.&lt;/p&gt;

&lt;p&gt;A two-tier surface is naturally more resilient to exactly this kind of upstream breakage, and not by accident. The guided tier, &lt;code&gt;create_chart&lt;/code&gt; in my case, sits between the caller and whatever internals it depends on. If the MCP Python SDK renames &lt;code&gt;FastMCP&lt;/code&gt; to &lt;code&gt;MCPServer&lt;/code&gt; tomorrow, or renames something else the year after, the guided tool's &lt;em&gt;internals&lt;/em&gt; can absorb that migration: update the import, update the internal call, ship a patch. The tool's &lt;em&gt;contract&lt;/em&gt; to callers, the structured input schema, the guaranteed valid output, doesn't have to change at all. The caller never sees the churn.&lt;/p&gt;

&lt;p&gt;The raw passthrough tier is different by design, and that difference is a feature, not an oversight. &lt;code&gt;render_chart&lt;/code&gt; and &lt;code&gt;export_chart&lt;/code&gt; expose the underlying options object directly, which means their contract is inherently less stable: if Highcharts changes an option name, or if the SDK underneath changes how it accepts raw payloads, that instability passes straight through to the caller. But that's an acceptable trade, because a caller reaching for the raw tier already knows they're trading guardrails for capability. They opted into that risk the moment they skipped the guided path. The guided tier is where stability lives; the raw tier is where power lives, and mixing those two concerns into a single tool is what makes a rename three layers down into a surprise outage instead of a routine internal patch.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;create_chart&lt;/code&gt; (guided)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;render_chart&lt;/code&gt; / &lt;code&gt;export_chart&lt;/code&gt; (raw)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured, schema-validated fields&lt;/td&gt;
&lt;td&gt;Full Highcharts options object, unchecked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failure mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Can't accept a broken config&lt;/td&gt;
&lt;td&gt;Can fail exactly like raw Highcharts fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDK-migration exposure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Absorbed internally, contract unchanged&lt;/td&gt;
&lt;td&gt;Passes through, caller owns the risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When to reach for it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default path, most calls&lt;/td&gt;
&lt;td&gt;Guided schema can't express the request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Author's own framework, built for a two-tier chart-rendering MCP server. Not sourced external data.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Actually Check in Your Own MCP Server
&lt;/h2&gt;

&lt;p&gt;None of this requires waiting for the next rename to matter. Three things are worth checking regardless of which SDK you're on.&lt;/p&gt;

&lt;p&gt;Pin your MCP SDK dependency with an explicit upper bound, not just a floor. &lt;code&gt;mcp&amp;gt;=1.0,&amp;lt;2.0&lt;/code&gt; fails loudly at install time if a major bump lands; &lt;code&gt;mcp&lt;/code&gt; or &lt;code&gt;mcp&amp;gt;=1.0&lt;/code&gt; alone will resolve silently to whatever's newest, including a version that renamed the class you're importing. This is the single highest-leverage fix for the exact failure this article describes, and it costs one line in a dependency file.&lt;/p&gt;

&lt;p&gt;Audit whether any single tool in your server is a dead end. If a tool has a fixed schema and no fallback, ask what happens when a caller's request falls outside that schema. If the honest answer is "the call fails and there's no other path," that's the same rigidity that makes an upstream rename catastrophic instead of routine, just pointed at your own API surface instead of the SDK's.&lt;/p&gt;

&lt;p&gt;Separate what's stable from what's inherently unstable, and say so in your tool descriptions. A guided tool's promise to callers should be a promise you can keep even after an internal migration. A raw passthrough tool's promise should be explicit that the caller is trading stability for capability, so nobody's surprised when that tier changes underneath them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The FastMCP-to-MCPServer rename wasn't a bug in the MCP Python SDK. It was a normal, well-documented major-version change that happened to land on an ecosystem where a lot of dependency declarations don't have upper bounds. The literature on breaking changes says this happens across every ecosystem studied, at rates far above what semver's promise of "safe minor upgrades" would suggest. The fix isn't a smarter SDK team; it's dependency pinning plus a tool architecture that doesn't put all its stability eggs in one rigid basket.&lt;/p&gt;

&lt;p&gt;Building that two-tier surface, one tool guided and validated, one raw and unguarded, wasn't originally a defense against SDK churn. It came from a much more mundane problem: a single rigid tool hits a wall the moment a model needs something you didn't anticipate. But the same design that solves that problem turns out to solve the SDK-rename problem too, because both problems come from the same root assumption: that you can predict every future need in advance. You can't. Build the tier that absorbs change internally, and the tier that admits it can't, and label which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the Pattern
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;highchart-mcp&lt;/code&gt; is the working example of the two-tier design described here: a guided &lt;code&gt;create_chart&lt;/code&gt; tool for the common case, and &lt;code&gt;render_chart&lt;/code&gt;/&lt;code&gt;export_chart&lt;/code&gt; for when the guided path isn't enough, covering all 70 Highcharts chart types. It's published as &lt;a href="https://www.npmjs.com/package/@highchart-mcp/server" rel="noopener noreferrer"&gt;&lt;code&gt;@highchart-mcp/server&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@highchart-mcp/sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@highchart-mcp/sdk&lt;/code&gt;&lt;/a&gt; on npm, mirrored on PyPI, with a Docker image included for anyone who wants to run it without a local install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the FastMCP rename affect the standalone third-party FastMCP framework too?
&lt;/h3&gt;

&lt;p&gt;No. The third-party FastMCP framework built by Jeremiah Lowin (currently v4.0) is a separate, independently maintained project that predates and inspired the SDK's built-in class. It is not being renamed and is unaffected by MCP Python SDK v2.0.0. Only code importing &lt;code&gt;mcp.server.fastmcp&lt;/code&gt; from the official &lt;code&gt;modelcontextprotocol/python-sdk&lt;/code&gt; package is affected.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know if my project is exposed to this specific break?
&lt;/h3&gt;

&lt;p&gt;Check your dependency file for an unbounded or loosely bounded &lt;code&gt;mcp&lt;/code&gt; version (for example, plain &lt;code&gt;mcp&lt;/code&gt; or &lt;code&gt;mcp&amp;gt;=1.0&lt;/code&gt; with no upper limit), and search your codebase for &lt;code&gt;from mcp.server.fastmcp import&lt;/code&gt; or &lt;code&gt;mcp.server.fastmcp.*&lt;/code&gt; references. If both are present, a fresh install after July 28, 2026 can resolve to v2.0.0 and break those imports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a two-tier tool design overkill for a small or simple MCP server?
&lt;/h3&gt;

&lt;p&gt;Not necessarily overkill, but it's a judgment call tied to how predictable the tool's use cases really are. A server with one narrow, well-bounded task (checking a single API status, for instance) may never need a raw escape hatch. The pattern earns its cost once a tool's input space is genuinely open-ended, the way chart configuration, query building, or file transformation tend to be, where a single rigid schema is unlikely to cover every real request.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;hasnaintypes&lt;/strong&gt; builds open-source developer tooling, including the &lt;code&gt;highchart-mcp&lt;/code&gt; MCP server referenced in this post. See more of their work on &lt;a href="https://github.com/hasnaintypes" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>architecture</category>
      <category>api</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Stop Putting API Keys in Your Agent's .env File</title>
      <dc:creator>Hasnain Arif</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:09:02 +0000</pubDate>
      <link>https://dev.to/justhsnn/stop-putting-api-keys-in-your-agents-env-file-27kk</link>
      <guid>https://dev.to/justhsnn/stop-putting-api-keys-in-your-agents-env-file-27kk</guid>
      <description>&lt;p&gt;Open any repo with an AI agent in it and you'll find the same thing nine times out of ten: a &lt;code&gt;.env&lt;/code&gt; file with an OpenAI key, a GitHub token, maybe a database URL, all long-lived, all scoped to basically everything, all sitting in plaintext on disk.&lt;/p&gt;

&lt;p&gt;This works fine in a demo. It's also exactly the pattern that turns a single leaked file, a compromised dependency, or a prompt-injected tool call into total account takeover, because the credential the agent is holding doesn't expire, doesn't know what task it's for, and can usually do a lot more than the task actually needs.&lt;/p&gt;

&lt;p&gt;The fix isn't "better secrets management." It's not treating the agent as something that holds credentials at all. Below is the pattern that's actually converging across MCP, Auth0, WorkOS, and a few other places building this right now, plus a minimal implementation you can steal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your current setup is the problem, not a workaround away from it
&lt;/h2&gt;

&lt;p&gt;A static API key answers one question: &lt;em&gt;is the bearer allowed to do this category of thing, forever, until someone remembers to rotate it.&lt;/em&gt; That's a bad shape for an agent, because an agent isn't one actor doing one job. The same agent process might read a customer record for one user, then modify a deployment config for a completely different user, thirty seconds later, using the same credential the whole time.&lt;/p&gt;

&lt;p&gt;Give that credential broad scope once, and every one of those very different actions is now authorized by the same blanket permission. If the agent gets manipulated into doing something it shouldn't — a prompt injection, a bad tool call, a hallucinated plan — the credential doesn't know the difference. It was never scoped to the task in the first place.&lt;/p&gt;

&lt;p&gt;The fix is to stop giving the agent a credential at all, and instead give it a way to &lt;em&gt;ask for one, just before it needs it, scoped to exactly that action.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision tree
&lt;/h2&gt;

&lt;p&gt;Before you touch any code, figure out which of these your agent actually is, because it changes the whole flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does the agent act on behalf of a specific logged-in user?
├── YES → Authorization Code + PKCE
│         (agent inherits a narrow, user-consented scope)
│
└── NO → Is it a background job / daemon / pipeline with no user in the loop?
          ├── YES → Client Credentials Grant
          │         (agent authenticates as itself, gets a short-lived token)
          │
          └── UNCERTAIN → Does it ever touch a specific user's data mid-task?
                    ├── YES → treat as user-delegated, use token exchange
                    └── NO  → Client Credentials, narrowest scope you can define
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most agent architectures actually need both at different points — client credentials for the agent's own background identity, and token exchange whenever it needs to act &lt;em&gt;as&lt;/em&gt; a specific user for one call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Client credentials, done right
&lt;/h2&gt;

&lt;p&gt;This is the baseline for an autonomous agent with no user in the loop. The agent authenticates as itself and gets a token that dies fast.&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;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentTokenClient&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_url&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_secret&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# (token, expires_at, scope)
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cached&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cached&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cached&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cached&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&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;grant_type&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;client_credentials&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;client_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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_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;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth doing that people skip: request the &lt;em&gt;narrowest&lt;/em&gt; scope string that covers the actual call you're about to make (not "crm:*", but "crm:read:invoices"), and set the TTL as low as your workflow tolerates. Five to fifteen minutes for anything sensitive, an hour at most for read-only calls. There's no refresh token in this flow on purpose — if the token expires, the agent just re-authenticates. That's a feature, not friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Token exchange for "acting as this specific user, right now"
&lt;/h2&gt;

&lt;p&gt;This is the one most agent codebases skip entirely, and it's the one that actually matters once your agent does anything on behalf of different users.&lt;/p&gt;

&lt;p&gt;The idea, from RFC 8693: your agent holds a base token that proves &lt;em&gt;who the agent is&lt;/em&gt;, and exchanges it for a narrower, audience-restricted token that proves &lt;em&gt;what this specific call is allowed to do, for this specific user, right now.&lt;/em&gt;&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exchange_for_scoped_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_token&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;target_resource&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="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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&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="n"&gt;TOKEN_EXCHANGE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&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;grant_type&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;urn:ietf:params:oauth:grant-type:token-exchange&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;subject_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject_token_type&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;urn:ietf:params:oauth:token-type:access_token&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;resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target_resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&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;# e.g. "invoices:refund"
&lt;/span&gt;    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&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;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff: if this narrow token leaks, the blast radius is one action, on one resource, for a window measured in minutes — not the agent's entire standing access to everything it's ever touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: The vault broker, so the agent never sees a raw secret
&lt;/h2&gt;

&lt;p&gt;The cleanest version of this removes the agent from the credential-handling business entirely. The agent doesn't request a token from an auth server directly — it asks a broker for permission to do something, and the broker injects the credential on the way out to the actual API call.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CredentialBroker&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vault_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vault_client&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorized_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent_identity&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;api_call&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# 1. Policy check — is this agent allowed to do this, right now?
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;policy_allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_identity&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&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;agent_identity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not authorized for &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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 2. Mint a scoped, short-lived token for exactly this action
&lt;/span&gt;        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&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;ttl_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Make the call, inject the credential here, never hand it to the agent
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;api_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# belt and suspenders
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's code just calls &lt;code&gt;broker.authorized_call("refund-agent", "invoices:refund", do_refund)&lt;/code&gt; and never touches a credential at all. If the agent's process gets compromised — prompt injection, malicious tool output, whatever — there's nothing to steal, because nothing long-lived was ever there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP already gets right here
&lt;/h2&gt;

&lt;p&gt;If you're building on Model Context Protocol, some of this is handled for you by design. MCP's authorization spec deliberately treats the MCP server as an OAuth &lt;strong&gt;resource server&lt;/strong&gt;, not an authorization server — meaning the server that handles your tool calls isn't the one deciding who's allowed to call them. That job belongs to a dedicated identity provider, and the MCP server's only responsibility is validating the token it's handed.&lt;/p&gt;

&lt;p&gt;That split matters more than it sounds. It means you can swap out or upgrade your auth provider without touching every MCP server you run, and it means tool-level authorization decisions live in one place instead of being reimplemented inconsistently across every integration you build.&lt;/p&gt;

&lt;p&gt;If you're rolling your own agent-to-tool protocol instead of MCP, this is worth copying even without the rest of MCP: keep "can this token do this" completely separate from "what does this tool do."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this still breaks
&lt;/h2&gt;

&lt;p&gt;Being honest about the gaps, because this pattern isn't a finished solution yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant isolation.&lt;/strong&gt; If your agent serves multiple organizations, every token needs the tenant ID baked in and checked at every hop — an agent from Org A presenting a token that happens to also work against Org B's data is a real, recurring failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation lag.&lt;/strong&gt; Short TTLs help, but if you revoke an agent's standing access mid-task, in-flight short-lived tokens it already grabbed can still be valid until they expire on their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Who approved this scope" governance.&lt;/strong&gt; The mechanism above handles issuance cleanly. It says nothing about who decided this agent should be allowed to request &lt;code&gt;invoices:refund&lt;/code&gt; in the first place, or whether that decision gets reviewed six months later. That's policy work, not code, and most teams skip it because it's the boring part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK friction.&lt;/strong&gt; A lot of LLM provider SDKs still assume one static key at initialization with no clean hook for mid-session credential rotation. You'll end up wrapping the client yourself more often than you'd like.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;An agent is not a user and it's not a static service account — it's a new kind of actor that needs its own identity, its own narrowly scoped credentials, and a credential lifetime measured in minutes, not months. If your agent's &lt;code&gt;.env&lt;/code&gt; file has anything in it that doesn't expire on its own, that's the thing to fix this week, before it's the thing in next month's incident report.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>oauth</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
