<?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: Shanahan</title>
    <description>The latest articles on DEV Community by Shanahan (@shanahansuresh).</description>
    <link>https://dev.to/shanahansuresh</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%2F4006597%2F7bbe07b7-3d77-47b4-954d-b562859a9bec.png</url>
      <title>DEV Community: Shanahan</title>
      <link>https://dev.to/shanahansuresh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shanahansuresh"/>
    <language>en</language>
    <item>
      <title>Wiring Django to Claude: Generating an MCP server from OpenAPI</title>
      <dc:creator>Shanahan</dc:creator>
      <pubDate>Sun, 28 Jun 2026 17:19:07 +0000</pubDate>
      <link>https://dev.to/shanahansuresh/wiring-django-to-claude-generating-an-mcp-server-from-openapi-1e8a</link>
      <guid>https://dev.to/shanahansuresh/wiring-django-to-claude-generating-an-mcp-server-from-openapi-1e8a</guid>
      <description>&lt;p&gt;If you build Django APIs, you probably use &lt;a href="https://github.com/tfranzel/drf-spectacular" rel="noopener noreferrer"&gt;drf-spectacular&lt;/a&gt; to generate your OpenAPI schema. That schema already describes every single endpoint you have. Here is how to automatically transform it into a Model Context Protocol (MCP) server. As well as how to survive the harder engineering problems that appear once real AI agents actually starts calling it.&lt;/p&gt;




&lt;p&gt;If you want an LLM to &lt;em&gt;operate&lt;/em&gt; your Django API, not just talk about it, you have to give it tools. The naïve way is to hand-write one MCP tool per endpoint: name, description, input schema, the call itself. It works on day one, and it rots by day thirty. Every new endpoint, every renamed field, every changed query param becomes a second place you have to remember to update. Maintenance scales with your API surface, which is exactly the thing you were hoping the LLM would help you tame.&lt;/p&gt;

&lt;p&gt;But you've already described your entire API once. If you use &lt;a href="https://github.com/tfranzel/drf-spectacular" rel="noopener noreferrer"&gt;drf-spectacular&lt;/a&gt;, and if you're doing DRF in 2026 you almost certainly do, that's an OpenAPI 3 schema sitting in your project that already knows every path, parameter, and type. That schema can &lt;em&gt;be&lt;/em&gt; your tool definitions. You just have to teach something to read it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DRF views (drf-spectacular) ──▶ OpenAPI schema ──▶ MCP tools ──▶ Claude (or any other LLMs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post comes in two halves. First, the wiring: how you turn that schema into a working MCP server, with real code, so you understand what off-the-shelf tools are doing under the hood. Then the harder half: the problems that have nothing to do with OpenAPI and everything to do with putting a &lt;em&gt;real&lt;/em&gt; agent in front of those tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  First, the honest part: this isn't new
&lt;/h3&gt;

&lt;p&gt;Generating LLM tools from an OpenAPI spec is a solved problem. On the Django side, packages like &lt;a href="https://github.com/zacharypodbela/django-rest-framework-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;django-rest-framework-mcp&lt;/code&gt;&lt;/a&gt; do almost exactly this: they hook directly into your DRF configuration and expose your Views and ViewSets as MCP tools with minimal setup. There are also generic &lt;code&gt;openapi → mcp&lt;/code&gt; converters (&lt;a href="https://pypi.org/project/openapi-to-mcp/" rel="noopener noreferrer"&gt;&lt;code&gt;openapi-to-mcp&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pypi.org/project/openapi-mcp-generator/" rel="noopener noreferrer"&gt;&lt;code&gt;openapi-mcp-generator&lt;/code&gt;&lt;/a&gt;), and &lt;a href="https://github.com/PrefectHQ/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; ships native OpenAPI support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So why read on?&lt;/strong&gt; Because if you only ever &lt;code&gt;pip install&lt;/code&gt; one of those, you never learn what it's doing. The day it doesn't quite fit your API, you're stuck debugging a black box. I built a small, readable &lt;a href="https://github.com/Shanahan-Suresh/django-openapi-mcp" rel="noopener noreferrer"&gt;reference implementation&lt;/a&gt; so you can see every piece. If you want a maintained dependency, use FastMCP. If you want to understand how it works, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The wiring, in five pieces
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Introspect (in-process, no HTTP self-calling).&lt;/strong&gt; The reflex is to fetch &lt;code&gt;/api/schema/&lt;/code&gt; over HTTP. Don't. drf-spectacular will hand you the same schema in-process, no running server required:&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;from&lt;/span&gt; &lt;span class="n"&gt;drf_spectacular.generators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SchemaGenerator&lt;/span&gt;

&lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SchemaGenerator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it: a plain dict describing every endpoint. (If your schema lives somewhere else, a remote service or a non-DRF API, you fall back to fetching a URL and resolving its &lt;code&gt;$ref&lt;/code&gt; pointers yourself.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generate tool specs.&lt;/strong&gt; Walk the schema's &lt;code&gt;paths&lt;/code&gt; and turn each operation into a tool. The &lt;code&gt;operationId&lt;/code&gt; becomes the name, the parameters become a JSON Schema. Path params are always required, query params honour their own flag:&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;for&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;operation&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                         &lt;span class="c1"&gt;# params can be references
&lt;/span&gt;        &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;param&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# "path" or "query"
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;string&lt;/span&gt;&lt;span class="sh"&gt;"&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;location&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;param&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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 gotchas the happy-path tutorials skip. &lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;&lt;code&gt;$ref&lt;/code&gt; resolution&lt;/strong&gt;: a parameter can arrive as &lt;code&gt;{"$ref": "#/components/parameters/Foo"}&lt;/code&gt; instead of an inline object. Resolve the pointer or you'll generate broken tools. &lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;name collisions and charset&lt;/strong&gt;: MCP tool names allow only a limited character set, and two operations can share an &lt;code&gt;operationId&lt;/code&gt;. Sanitize and de-duplicate, or the client rejects your entire tool list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Serve: the one MCP decision that matters.&lt;/strong&gt; The official &lt;a href="https://github.com/modelcontextprotocol/python-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;mcp&lt;/code&gt;&lt;/a&gt; Python SDK gives you two ways to define tools. The high-level &lt;code&gt;FastMCP&lt;/code&gt; &lt;code&gt;@tool&lt;/code&gt; decorator is nice for tools you &lt;em&gt;know at write time&lt;/em&gt;. It's the wrong tool for this job. We're generating tools at &lt;em&gt;runtime&lt;/em&gt;, each with a JSON Schema we can't know in advance. For that, you drop down to the low-level &lt;code&gt;Server&lt;/code&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.lowlevel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mcp.types&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;

&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-django-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@server.list_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_tools&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="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;inputSchema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nd"&gt;@server.call_tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by_name&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&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="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you take one thing from this half of the post, take this: &lt;strong&gt;&lt;code&gt;@tool&lt;/code&gt; is for static tools. The low-level &lt;code&gt;Server&lt;/code&gt; is for tools you generate.&lt;/strong&gt; Reaching for the decorator first and then fighting it is the most common wrong turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Execute.&lt;/strong&gt; A tool spec is really two things: a schema to advertise, and the routing info to make the call. At call time you split the arguments back apart. Path params get substituted into the URL template, query params get attached as a query string. Then fire the request:&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;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&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="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Two transports, one server.&lt;/strong&gt; The same &lt;code&gt;Server&lt;/code&gt; object serves over &lt;strong&gt;stdio&lt;/strong&gt; (how Claude Desktop and Claude Code launch a local server) and &lt;strong&gt;Streamable HTTP&lt;/strong&gt; (for a deployed server). &lt;/p&gt;

&lt;p&gt;One gotcha that cost me an hour the first time: &lt;strong&gt;in stdio mode, stdout &lt;em&gt;is&lt;/em&gt; the protocol channel.&lt;/strong&gt; One stray &lt;code&gt;print()&lt;/code&gt; corrupts the stream, and the client silently shows zero tools.🙃&lt;/p&gt;

&lt;p&gt;Always log your diagnostics to stderr instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two decisions worth making consciously
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Safe by default.&lt;/strong&gt; Only &lt;code&gt;GET&lt;/code&gt; endpoints become tools. Auto-exposing your whole CRUD surface to an LLM, &lt;code&gt;DELETE&lt;/code&gt; included, is how you end up explaining to your team why the agent dropped a production row. Writes are strictly opt-in (&lt;code&gt;INCLUDE_METHODS = ["GET", "POST"]&lt;/code&gt;). Check your package's default and override it on purpose, not by accident.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pro-tip: even within safe &lt;code&gt;GET&lt;/code&gt; requests, you've probably got views you never want an LLM anywhere near. Heavy reporting endpoints, billing aggregations, anything expensive. Because the schema is generated in-process, you can hide those with drf-spectacular's native &lt;code&gt;@extend_schema(exclude=True)&lt;/code&gt; decorator. It drops the endpoint from both your OpenAPI docs and the generated tool list in one move.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Auth: the part that bites.&lt;/strong&gt; A generated tool is useless if it can't authenticate, so credentials are applied to every outgoing request, configured once (token / bearer / custom header). But I'll be straight about the ceiling: this uses &lt;em&gt;static&lt;/em&gt; credentials. &lt;/p&gt;

&lt;p&gt;A better version, forwarding the &lt;strong&gt;calling user's&lt;/strong&gt; own credentials so each tool runs with &lt;em&gt;their&lt;/em&gt; permissions instead of a shared service account, is a line between demo and deployment. Don't let any tutorial, this one included, tell you auth is "done" while it's pointing at a single static token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;The repo ships a runnable &lt;code&gt;example/&lt;/code&gt; DRF project, a tiny shop (products + orders, with an &lt;code&gt;in_stock&lt;/code&gt; filter), so you can watch the whole loop work in about two minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Shanahan-Suresh/django-openapi-mcp
&lt;span class="nb"&gt;cd &lt;/span&gt;django-openapi-mcpnstall &lt;span class="s2"&gt;"git+https://github.com/Shanahan-Suresh/django-openapi-mcp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Django &lt;code&gt;settings.py&lt;/code&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="n"&gt;INSTALLED_APPS&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;rest_framework&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;drf_spectacular&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;django_openapi_mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;DJANGO_OPENAPI_MCP&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;BASE_URL&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://127.0.0.1:8000&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;On a new terminal run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python manage.py run_mcp_server &lt;span class="nt"&gt;--transport&lt;/span&gt; stdio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into Claude Desktop's &lt;code&gt;claude_desktop_config.json&lt;/code&gt;, restart, and all your endpoints are tools in your own MCP server:&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filygaprsx203bkf7klkz.png" 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%2Filygaprsx203bkf7klkz.png" alt="Claude Desktop connecting to the MCP server" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now for the fun part. Open a chat and ask Claude to interact with your Django app: &lt;em&gt;"List all products that are in stock."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude will automatically map your prompt to the &lt;code&gt;products_list&lt;/code&gt; tool, understand it needs to pass the &lt;code&gt;in_stock=true&lt;/code&gt; query parameter based on your schema, and ask for permission to run 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3l35x7htmepxmutmesvd.png" 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%2F3l35x7htmepxmutmesvd.png" alt="Claude asking for permission to use the Products list tool" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you allow it, the tool hits your local Django API, returns the JSON, and Claude formats it into a neat table. You just gave an LLM read-access to your database with zero hand-written glue code.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonquwuctxtutslrfdjoc.png" 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%2Fonquwuctxtutslrfdjoc.png" alt="Claude displaying the in-stock products table" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Generating tools is the easy part
&lt;/h2&gt;

&lt;p&gt;Everything above is an afternoon's work, and several packages will do it for you. Then you point a real agent at those tools and ask it something multi-step: &lt;em&gt;"find this customer's most recent order and tell me whether everything in it is still in stock"&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;You discover the tools were never the hard part. The hard part is everything between the model and the tools. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In real deployment that agent usually sits behind a chat interface, a thin web UI where someone types a question and watches the tool calls stream back. The surface doesn't change any of the problems below. They're the same whether the caller is a chat app, Claude Desktop, or a cron job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Generated tools aren't &lt;em&gt;choosable&lt;/em&gt; tools.&lt;/strong&gt; A raw schema gives you one tool per operation, named after its &lt;code&gt;operationId&lt;/code&gt;. Fine for a human reading docs, nearly useless for a model deciding &lt;em&gt;which&lt;/em&gt; tool to reach for. When the user asks "is their last order still fulfillable?", which of &lt;code&gt;orders_list&lt;/code&gt;, &lt;code&gt;orders_retrieve&lt;/code&gt;, and &lt;code&gt;products_retrieve&lt;/code&gt; fire, and in what order? &lt;/p&gt;

&lt;p&gt;You end up adding a thin layer of &lt;em&gt;semantic enrichment&lt;/em&gt;, an index of what each tool needs and yields, so the agent can reason over capabilities ("I need something that returns an order id") instead of pattern-matching tool names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reasoning loop.&lt;/strong&gt; A single tool call rarely answers a real question. "Is their last order in stock?" is at least three steps, and step 2 depends on step 1's output. &lt;/p&gt;

&lt;p&gt;That's a ReAct-style loop: reason → call a tool → append the result to the conversation → reason again, over a &lt;em&gt;single persistent message history&lt;/em&gt; so the model can carry an id from one step to the next. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors → classify → recover → retry.&lt;/strong&gt; This is the layer that surprised me most. &lt;em&gt;Generic retries don't fix semantic errors.&lt;/em&gt; If a call fails because a required field is missing or an id is malformed, calling it again unchanged fails identically. &lt;/p&gt;

&lt;p&gt;So instead of "retry N times," you classify the failure and pick a recovery strategy matched to it: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing fields&lt;/strong&gt;: Derive them from context (e.g., extract a username from an email).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong identifiers&lt;/strong&gt;: If the model tries to pass a name ("Bob's latest order") instead of a required ID field, intercept it, use a lookup tool to find the ID, and retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed data&lt;/strong&gt;: Alter wrong formats into the correct shape before hitting the API again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep these as small, ranked, pluggable strategies. A real fraction of first-attempt failures get fixed &lt;em&gt;without going back to the model at all&lt;/em&gt;: faster, cheaper, more reliable. The discipline that keeps it safe: &lt;strong&gt;derive, never invent.&lt;/strong&gt; Reformatting a value the user gave you is fine, fabricating one they didn't is how you get confidently wrong answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surviving the model layer.&lt;/strong&gt; Your loop is only as available as the model behind it, and quota exhaustion and rate limits aren't edge cases in production. They're Tuesdays. A &lt;code&gt;429&lt;/code&gt;/&lt;code&gt;402&lt;/code&gt; is a different animal from a transient blip, retrying the same over-quota provider just burns time. Put an abstraction over the provider, classify quota errors separately from retryable ones, and fall back primary → secondary.&lt;/p&gt;

&lt;h3&gt;
  
  
  The thread running through all of it
&lt;/h3&gt;

&lt;p&gt;None of those Part 2 problems are about OpenAPI or MCP. Tool generation is just &lt;em&gt;transport&lt;/em&gt;, it gets a list of capabilities in front of the model. But everything that makes an agent actually succeed (choosing tools, chaining them, recovering from failure) lives above that line.&lt;/p&gt;

&lt;p&gt;If there is one principle that ties both halves of this post together, it's this: &lt;br&gt;
&lt;strong&gt;Engineer defensively, rather than hoping the model behaves.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Block destructive operations at generation time.&lt;/li&gt;
&lt;li&gt;Derive missing data, never let the model invent it.&lt;/li&gt;
&lt;li&gt;Classify errors before you blindly retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An LLM is a brilliant, entirely unreliable component. Production engineering is the harness that makes it dependable despite all that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping up
&lt;/h3&gt;

&lt;p&gt;If you are building this for work tomorrow and need a quick reliable, maintained package, check out &lt;a href="https://github.com/gts360/django-mcp-server" rel="noopener noreferrer"&gt;&lt;code&gt;django-mcp-server&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://github.com/zacharypodbela/django-rest-framework-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;django-rest-framework-mcp&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But if you wanted to understand what's happening under the hood, you do now. Feel free to try out the &lt;a href="https://github.com/Shanahan-Suresh/django-openapi-mcp" rel="noopener noreferrer"&gt;reference repo&lt;/a&gt; by cloning it, breaking it down and seeing how the wiring works.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>django</category>
      <category>mcp</category>
      <category>python</category>
    </item>
  </channel>
</rss>
