<?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: Latitude 10</title>
    <description>The latest articles on DEV Community by Latitude 10 (latitude10).</description>
    <link>https://dev.to/latitude10</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%2Forganization%2Fprofile_image%2F15712%2Fc80b9ca2-0797-4ef2-827c-c8bb684aebba.png</url>
      <title>DEV Community: Latitude 10</title>
      <link>https://dev.to/latitude10</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/latitude10"/>
    <language>en</language>
    <item>
      <title>Your API as an MCP server</title>
      <dc:creator>Jorge Ortega</dc:creator>
      <pubDate>Thu, 29 Oct 2026 20:50:22 +0000</pubDate>
      <link>https://dev.to/latitude10/your-api-as-an-mcp-server-in-process-what-broke-and-how-we-fixed-it-526a</link>
      <guid>https://dev.to/latitude10/your-api-as-an-mcp-server-in-process-what-broke-and-how-we-fixed-it-526a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesqo7ey8alex8tgnxa0v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesqo7ey8alex8tgnxa0v.webp" alt=" " width="480" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most ways to give an API an MCP server add a second service: a proxy that reads your OpenAPI spec and calls your API over HTTP. It works, but now there are two things to deploy, two places to check auth, and a hop that has to carry your users' credentials.&lt;/p&gt;

&lt;p&gt;We wanted something else: one middleware in the app you already run, where an agent's tool call goes through your own routes, with the auth middleware, validation and handlers you already test. That's &lt;a href="https://github.com/jorgeortega/openapi-agentify" rel="noopener noreferrer"&gt;openapi-agentify&lt;/a&gt;, MIT-licensed, for Express, Hono, Fastify and Next.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;agentify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Orders API&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./openapi.yaml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v1/orders/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// unchanged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/mcp&lt;/code&gt; then serves one tool per operation in your spec, reads on and writes off by default. Getting there took more than wiring a router to an MCP library. These are the three problems that took the longest.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Calling your own app without a port
&lt;/h2&gt;

&lt;p&gt;A tool call becomes the HTTP request the operation describes. The obvious way to run it is &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;localhost&lt;/code&gt;, but then the server needs to know its own port, and every call goes through the network stack.&lt;/p&gt;

&lt;p&gt;Fastify has &lt;code&gt;app.inject()&lt;/code&gt; and Hono has &lt;code&gt;app.fetch()&lt;/code&gt;, so those two were easy. Express has nothing built in. We tried &lt;code&gt;light-my-request&lt;/code&gt;, the library behind Fastify's inject. It works in a test, but when it detects an Express app it re-parents Express's shared request and response prototypes onto its own classes. In a test that's harmless. In a running server it breaks every later real request.&lt;/p&gt;

&lt;p&gt;So the Express adapter builds the request itself: a plain &lt;code&gt;IncomingMessage&lt;/code&gt; on an in-memory socket, a plain &lt;code&gt;ServerResponse&lt;/code&gt;, and the app called as a function. The injected request carries the agent's IP as its remote address, so your rate limiter and logs see the caller, not &lt;code&gt;127.0.0.1&lt;/code&gt;. (&lt;code&gt;src/adapters/node-inject.ts&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-entry.&lt;/strong&gt; That request goes through your whole middleware stack, which includes Agentify itself. The middleware has to recognise its own calls without letting a client pretend to be one. Each in-process call carries an &lt;code&gt;X-Agentify-Internal&lt;/code&gt; header whose value is a per-process random secret, valid only while that call runs. A forged header from outside doesn't match and is stripped. One more trap: if your spec is served by your own app, Agentify's first load calls the app, which calls Agentify, which is still loading. Waiting would wait for itself, so a marked call during the load just passes through. (&lt;code&gt;src/dispatch.ts&lt;/code&gt;, &lt;code&gt;src/core.ts&lt;/code&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Idempotency keys when the body can only be read once
&lt;/h2&gt;

&lt;p&gt;Agents retry. For writes, Agentify honours &lt;code&gt;Idempotency-Key&lt;/code&gt;: the same key with the same request replays the stored answer, and the same key with a different body is refused. That means hashing the body before your routes run.&lt;/p&gt;

&lt;p&gt;But a Node request is a stream, and it can be read once. If middleware reads it, your &lt;code&gt;express.json()&lt;/code&gt; finds nothing. Re-serialising a parsed body doesn't work either, because the hash has to match what the client sent, byte for byte, and your parser may have a &lt;code&gt;verify&lt;/code&gt; hook that needs the raw bytes too.&lt;/p&gt;

&lt;p&gt;What works: read the raw bytes, up to 1 MiB, then &lt;code&gt;unshift&lt;/code&gt; them back onto the front of the stream before it ends. Your own body parser then reads the request exactly as the client sent it, with its own limit, its own &lt;code&gt;verify&lt;/code&gt; hook and its own error handling. Agentify never parses the body and never sets &lt;code&gt;req.body&lt;/code&gt;. Past 1 MiB it stops, puts back what it read, and doesn't hash. The key store is pluggable (&lt;code&gt;beginIdempotent&lt;/code&gt;, &lt;code&gt;completeIdempotent&lt;/code&gt;, &lt;code&gt;releaseIdempotent&lt;/code&gt;), in memory by default and in your database when you run more than one instance. (&lt;code&gt;src/adapters/express.ts&lt;/code&gt; &lt;code&gt;prereadBody&lt;/code&gt;, &lt;code&gt;src/idempotency.ts&lt;/code&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Deciding which operation a request is
&lt;/h2&gt;

&lt;p&gt;Agentify has to match a request to a spec operation, both to label agent traffic and to know which tool a credential was handed out for. Your router decides which handler runs. If the two disagree, a call could be counted as one operation and run as another.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static segments win.&lt;/strong&gt; &lt;code&gt;/v1/orders/export&lt;/code&gt; and &lt;code&gt;/v1/orders/{id}&lt;/code&gt; both match &lt;code&gt;/v1/orders/export&lt;/code&gt;, so paths are ranked by their number of literal segments, as most routers do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case.&lt;/strong&gt; Matching ignores case, like Express's default router. But a credential is only handed over when the request is spelled the way the spec spells it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous paths are refused.&lt;/strong&gt; Dot segments (plain or &lt;code&gt;%2e&lt;/code&gt;), empty segments, and an encoded &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;\&lt;/code&gt; might be resolved differently by your router than by us, so agent requests like these are refused rather than guessed at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared templates.&lt;/strong&gt; Every in-process call carries &lt;code&gt;X-Agentify-Operation&lt;/code&gt;, so a route that shares a path template with another can check that the call was meant for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;code&gt;compilePath&lt;/code&gt; and &lt;code&gt;ambiguousPath&lt;/code&gt; in &lt;code&gt;src/core.ts&lt;/code&gt;, &lt;code&gt;src/mcp/operation-tool.ts&lt;/code&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  What else it does
&lt;/h2&gt;

&lt;p&gt;The same spec drives what agents read before they call: &lt;code&gt;llms.txt&lt;/code&gt;, an MCP server card, the AI Catalog, your spec with agent hints, and &lt;code&gt;/agent&lt;/code&gt;, an index of all of them. And it scores how well a model will understand your spec (operationIds, descriptions, examples, error schemas, pagination, auth, idempotency, tool count) with the same rules as &lt;a href="https://github.com/jorgeortega/openapi-agent-check" rel="noopener noreferrer"&gt;openapi-agent-check&lt;/a&gt;, whose &lt;code&gt;--fix&lt;/code&gt; writes the rules-only fixes into your spec for free.&lt;/p&gt;

&lt;p&gt;Try it on your API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx openapi-agentify init &lt;span class="nt"&gt;--write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or check your spec first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx openapi-agent-check openapi.yaml &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Issues and pull requests are welcome; there are good first issues in both repositories. If you'd rather not run it yourself, &lt;a href="https://latitude10.tech/platform/mcp" rel="noopener noreferrer"&gt;Latitude 10&lt;/a&gt; hosts the same thing from your OpenAPI document.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>node</category>
      <category>express</category>
      <category>openapi</category>
    </item>
  </channel>
</rss>
