<?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: MrOops</title>
    <description>The latest articles on DEV Community by MrOops (@mroops).</description>
    <link>https://dev.to/mroops</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%2F3914894%2F7dcf27d4-2e94-43f5-b7ee-91240cf591a0.png</url>
      <title>DEV Community: MrOops</title>
      <link>https://dev.to/mroops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mroops"/>
    <language>en</language>
    <item>
      <title>Turn a Codebase into a Domain Model Your PM and QA Can Read</title>
      <dc:creator>MrOops</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:05:29 +0000</pubDate>
      <link>https://dev.to/mroops/turn-a-codebase-into-a-domain-model-your-pm-and-qa-can-read-16d</link>
      <guid>https://dev.to/mroops/turn-a-codebase-into-a-domain-model-your-pm-and-qa-can-read-16d</guid>
      <description>&lt;p&gt;Every team I have worked on keeps two versions of the same product in its head.&lt;/p&gt;

&lt;p&gt;One is the code. It is honest about what actually runs, but it speaks in class names and package paths. A PM cannot read it, and even an engineer new to the service cannot tell from it why a rule exists or who asked for it.&lt;/p&gt;

&lt;p&gt;The other is the intent. It lives in PRDs, design docs, Notion, and an old Slack thread no one can find. It speaks the language of the business, but nobody keeps it in sync once the code lands. Before long, no one trusts it, and everyone ends up back at an engineer's desk to confirm what is actually true.&lt;/p&gt;

&lt;p&gt;These two drift apart every sprint, and the team ends up arguing about which one is right. &lt;a href="https://github.com/mroops0111/braid" rel="noopener noreferrer"&gt;Braid&lt;/a&gt; is my attempt to stop that argument by keeping a single model both sides can read. It is a framework. You bring the sources and pick an ontology, and it runs a loop where the AI drafts and asks, and people decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of the Idea
&lt;/h2&gt;

&lt;p&gt;Braid reads your sources, and an AI drafts a domain model from them. The model is typed by an ontology, so it reads as a domain rather than a call graph. The default ontology is Domain-Driven Design, so a node is a bounded context, an aggregate, a command, or a rule, named in the ubiquitous language of the product.&lt;/p&gt;

&lt;p&gt;A domain model is less an extraction than an agreement. The same code admits more than one honest reading, and the one that counts is the one the domain's owners will stand behind. Some of it is never in the source at all, like why a rule exists, and only a person can supply that. So the AI never has the final say. Every change arrives as a proposal a person approves before it lands, and when the AI is unsure how an expert sees the domain, it asks rather than guesses. As the code changes, Braid reacts and feeds the diff back as a fresh proposal, so the model keeps pace instead of going stale.&lt;/p&gt;

&lt;p&gt;The result is one artifact an engineer, a PM, and a QA can all point at, traced back to the source it was drawn from.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Run, From Code Alone
&lt;/h2&gt;

&lt;p&gt;The strongest version of the claim is the hardest one. Give Braid a codebase with no docs at all, and see whether it produces something a person would recognize as their domain.&lt;/p&gt;

&lt;p&gt;So in Studio I created a workspace, pointed it at &lt;a href="https://github.com/mroops0111/conciergent" rel="noopener noreferrer"&gt;conciergent&lt;/a&gt;, a service of mine that gives MCP tools a chat face on Slack, LINE, and Discord, and gave it no intent documents at all. Only the code.&lt;/p&gt;

&lt;p&gt;Braid splits the codebase into business units first, models each one, then reconciles the overlaps into a single graph. The scan found eight units on this repo, and the reconciled model has 120 nodes and 159 edges across five bounded contexts, with no failed unit.&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%2Fraw.githubusercontent.com%2Fmroops0111%2Fbraid%2Fmaster%2Fexamples%2Fconciergent%2Fgraph.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%2Fraw.githubusercontent.com%2Fmroops0111%2Fbraid%2Fmaster%2Fexamples%2Fconciergent%2Fgraph.png" alt="The conciergent model in Braid Studio, bounded contexts and aggregates" width="799" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full example is committed at &lt;a href="https://github.com/mroops0111/braid/tree/master/examples/conciergent" rel="noopener noreferrer"&gt;examples/conciergent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Braid Actually Does
&lt;/h2&gt;

&lt;p&gt;Three things, each of which showed up in that run.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A Model in the Language of the Domain
&lt;/h3&gt;

&lt;p&gt;The nodes are not classes. They are the concepts the product is about, described in plain language. Here is one aggregate from the run, its description quoted directly with the trailing state diagram left out:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pending Approval.&lt;/strong&gt; A sensitive action held back until the user says yes. When the assistant wants to use something the tool provider marked destructive, the action does not run: the work already in flight is set aside and the user is shown a confirmation with Confirm and Cancel.&lt;/p&gt;

&lt;p&gt;Rules that always hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At most one confirmation waits per conversation, and picking it up consumes it, so the same held work can never run twice.&lt;/li&gt;
&lt;li&gt;The confirmation ages out on its own, and held work that can no longer be read counts as expired.&lt;/li&gt;
&lt;li&gt;One answer decides every action held in that pause.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing in there is Domain-Driven Design jargon, and nothing is a code identifier. It reads like a domain expert explaining the rule, because that register is exactly what the ontology asks the model to write in. A PM can read it, a QA engineer can turn each rule that always holds into a test, and an engineer can check it against the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The AI Drafts, a Person Decides
&lt;/h3&gt;

&lt;p&gt;An AI-built model will not always match how each expert sees the domain. So Braid does not let the AI write to the model directly. Every change is a proposal, reviewed before it lands, and every disagreement is settled by a person rather than by the model's confidence.&lt;/p&gt;

&lt;p&gt;When the model runs into something genuinely ambiguous, it does not paper over it. It raises a question and waits for an answer, and that answer becomes part of the model. The loop is the point. The AI does the drafting, the people who own the domain do the deciding, and neither one is doing the other's job.&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%2Fk645rdb5s9fdv3vao8iu.gif" 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%2Fk645rdb5s9fdv3vao8iu.gif" alt="Answering a clarification in Studio, then reviewing the proposal it produces and applying it to the domain model" width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Answering a clarification, running clarify to turn it into a proposal, then reviewing the diff and applying it. The model gains the node the moment it lands.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Every Claim Points Back at Its Source
&lt;/h3&gt;

&lt;p&gt;Each node carries a reference to the source it was drawn from, down to the file and line for a code source. A claim in the model can be traced back to the exact place it came from, rather than taken on trust. When you disagree with a node, you can open the evidence and see what the AI was reading when it wrote it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A View You Can Hand a PM or QA, Projected From the Model
&lt;/h2&gt;

&lt;p&gt;The model is the one artifact everything else derives from. Everything else is a view of it, a read-only projection you regenerate rather than a second copy that drifts. A document is one such view, rendered a file per bounded context or just the one you point it at, in the same plain language as the model, for whoever has to understand it without reading code. Because it all comes from one model, a view can be cut to what a team needs, scoped wider or narrower, aimed at a different reader, or shaped as something other than prose.&lt;/p&gt;

&lt;p&gt;Here is how the doc it generated for the Conversation Turn Lifecycle context opens, unedited:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Conversation Turn Lifecycle
&lt;/h4&gt;

&lt;p&gt;A person sends one message in a chat app and expects one useful answer back. Answering can detour through a sign-in, a confirmation the user has to tap, or a conversation so long the assistant can no longer hold all of it. This subsystem owns that single round trip, from an arriving message to a sent reply and an up-to-date conversation memory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It goes on with a glossary of the terms the subsystem uses, the people it serves, each use case with the rules that always hold, and a lifecycle diagram. None of it is in code terms, and a draft banner sits up top because every node is still unreviewed. It closes with the exact nodes it was drawn from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source nodes: ctx.conversationTurn, actor.chatUser, agg.turn, cmd.runTurn, evt.replySent, rule.oneReplyPerTurn, and twenty-five more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Regenerate it after the model changes and it changes with the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Compares
&lt;/h2&gt;

&lt;p&gt;I did the homework so you do not have to. Braid sits next to three kinds of tool teams reach for today, each solving part of this problem.&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;Braid&lt;/th&gt;
&lt;th&gt;Team Docs&lt;/th&gt;
&lt;th&gt;AI Repo Wiki&lt;/th&gt;
&lt;th&gt;Self-Writing Wiki&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A Typed Graph Model&lt;/td&gt;
&lt;td&gt;Yes, Nodes and Edges&lt;/td&gt;
&lt;td&gt;No, Pages&lt;/td&gt;
&lt;td&gt;Partly, Diagrams&lt;/td&gt;
&lt;td&gt;Partly, Linked Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speaks the Domain&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, Code-Facing&lt;/td&gt;
&lt;td&gt;Partly, Freeform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keeps Pace With the Source&lt;/td&gt;
&lt;td&gt;Yes, Reacts to Source&lt;/td&gt;
&lt;td&gt;No, Drifts&lt;/td&gt;
&lt;td&gt;Yes, Regenerates&lt;/td&gt;
&lt;td&gt;Yes, Compounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A PM or QA Can Read It&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One Reviewed Source of Truth&lt;/td&gt;
&lt;td&gt;Yes, Human-Gated&lt;/td&gt;
&lt;td&gt;No, Hand-Maintained&lt;/td&gt;
&lt;td&gt;No, Auto-Generated&lt;/td&gt;
&lt;td&gt;No, Self-Written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traceable to the Source&lt;/td&gt;
&lt;td&gt;Yes, Per Node&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partly, Links to Code&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://notion.so" rel="noopener noreferrer"&gt;Notion&lt;/a&gt;, &lt;a href="https://www.atlassian.com/software/confluence" rel="noopener noreferrer"&gt;Confluence&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://deepwiki.com" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLMwiki&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team docs&lt;/strong&gt; in &lt;a href="https://notion.so" rel="noopener noreferrer"&gt;Notion&lt;/a&gt; or &lt;a href="https://www.atlassian.com/software/confluence" rel="noopener noreferrer"&gt;Confluence&lt;/a&gt; are the simplest option if you can live with prose that drifts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI repo wiki&lt;/strong&gt; like &lt;a href="https://deepwiki.com" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt; gives the quickest read on an unfamiliar repo, but it documents the code rather than the business, and nobody reviews what it writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-writing wiki&lt;/strong&gt; like &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLMwiki&lt;/a&gt; maintains itself and resolves contradictions on its own, but that is also the risk, the AI has the final say and you are out of the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Braid&lt;/strong&gt; is for one reviewed, traceable model of the domain that engineers, PMs, and QA can all read and that keeps pace as the code moves.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is and Is Not There Yet
&lt;/h2&gt;

&lt;p&gt;Braid is a young project, and I would rather be precise than oversell it. Everything above works today. What does not, in case it would block you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ontologies beyond DDD.&lt;/strong&gt; The ontology is a plugin and the framework does not assume DDD, but DDD is the only one shipped so far.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents beyond a coding agent.&lt;/strong&gt; The model is built by a coding agent today. A general chat surface over the model is on the roadmap, not in the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof at scale.&lt;/strong&gt; The runs so far are modest repos like the one above. Whether the model stays coherent on a large, sprawling codebase is unproven, and it is genuinely hard to validate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of these is what stands between you and trying it, an issue with the shape of your problem helps more than my guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This is a personal side project, built to settle an argument I have watched teams have for years. If the drift between what shipped and what the team meant sounds like your problem, give it a run and open an issue with what is missing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/mroops0111/braid" rel="noopener noreferrer"&gt;github.com/mroops0111/braid&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The example above&lt;/strong&gt;: &lt;a href="https://github.com/mroops0111/braid/tree/master/examples/conciergent" rel="noopener noreferrer"&gt;examples/conciergent&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>ddd</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Shape a Messy API into Clean MCP Tools with JSONata</title>
      <dc:creator>MrOops</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:35:20 +0000</pubDate>
      <link>https://dev.to/mroops/shape-a-messy-api-into-clean-mcp-tools-with-jsonata-2b1p</link>
      <guid>https://dev.to/mroops/shape-a-messy-api-into-clean-mcp-tools-with-jsonata-2b1p</guid>
      <description>&lt;p&gt;A raw OpenAPI operation rarely makes a good MCP tool. It exposes parameters the model should never set, speaks in cryptic API values, and hands back a bloated response envelope, so the model either misuses the call or drowns in the payload. &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;openapi-mcp-gateway&lt;/a&gt; fixes this with shaping. From one YAML block next to the operation, you declare the friendly input with &lt;code&gt;params&lt;/code&gt; and rewrite the request and response with JSONata, without forking the spec. It does four things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You decide the input.&lt;/strong&gt; Hide or fix the parameters the model should not choose, like a pinned project scope or pagination, expose only the friendly ones, and restrict values with &lt;code&gt;enum&lt;/code&gt;. It is enforced, so anything you did not expose cannot be set, and an out-of-range value is rejected before the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It speaks the model's language.&lt;/strong&gt; Friendly names and enums map onto the raw API values (&lt;code&gt;popular&lt;/code&gt; becomes &lt;code&gt;popularity.desc&lt;/code&gt;), so the model never invents a &lt;code&gt;sort_by&lt;/code&gt; string or a filter DSL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You shape the output.&lt;/strong&gt; Trim the envelope down to the fields that matter (less context), and rename or flatten them (&lt;code&gt;vote_average&lt;/code&gt; becomes &lt;code&gt;rating&lt;/code&gt;) so the result is easy for the model to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It stays declarative.&lt;/strong&gt; No wrapper service to write and deploy, and the model-facing tool holds steady when the upstream spec shifts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add openapi-mcp-gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TMDB example behind everything below ships in the repo. From a checkout, point it at your token and it serves the two shaped tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TMDB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your v4 read token&amp;gt;"&lt;/span&gt;
uv run openapi-mcp-gateway &lt;span class="nt"&gt;--config&lt;/span&gt; examples/movie-shaping.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool shaping is available in openapi-mcp-gateway 0.6.0+, Python 3.11+.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of a Tool
&lt;/h2&gt;

&lt;p&gt;Three keys under &lt;code&gt;x-mcp-integration.tool&lt;/code&gt; do the work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;params&lt;/code&gt; and &lt;code&gt;strategy&lt;/code&gt;&lt;/strong&gt; declare what the model sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;request&lt;/code&gt;&lt;/strong&gt; is a JSONata expression that maps the friendly arguments onto the upstream request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response&lt;/code&gt;&lt;/strong&gt; is a JSONata expression that reshapes the upstream body before it reaches the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;params&lt;/code&gt; entries are plain JSON Schema fragments (&lt;code&gt;type&lt;/code&gt;, &lt;code&gt;enum&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;). &lt;code&gt;strategy&lt;/code&gt; says how they relate to the spec. &lt;code&gt;replace&lt;/code&gt; makes them the whole input schema and drops the spec's parameters, and &lt;code&gt;merge&lt;/code&gt; layers onto the spec and keeps the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Example: The Movie Database
&lt;/h2&gt;

&lt;p&gt;TMDB's &lt;code&gt;/discover/movie&lt;/code&gt; is a good punching bag. It takes &lt;code&gt;sort_by&lt;/code&gt; (values like &lt;code&gt;popularity.desc&lt;/code&gt;), &lt;code&gt;include_adult&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, and &lt;code&gt;page&lt;/code&gt;, and returns a large envelope. Here is the whole friendly tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;discover_movies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;popular&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;top_rated&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;newest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;popular&lt;/span&gt;
        &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"sort_by": $lookup(&lt;/span&gt;
            &lt;span class="s"&gt;{"popular": "popularity.desc", "top_rated": "vote_average.desc", "newest": "primary_release_date.desc"},&lt;/span&gt;
            &lt;span class="s"&gt;sort&lt;/span&gt;
          &lt;span class="s"&gt;),&lt;/span&gt;
          &lt;span class="s"&gt;"page": page,&lt;/span&gt;
          &lt;span class="s"&gt;"include_adult": false,&lt;/span&gt;
          &lt;span class="s"&gt;"language": "en-US"&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;[results.{&lt;/span&gt;
          &lt;span class="s"&gt;"title": title,&lt;/span&gt;
          &lt;span class="s"&gt;"overview": overview,&lt;/span&gt;
          &lt;span class="s"&gt;"release_date": release_date,&lt;/span&gt;
          &lt;span class="s"&gt;"rating": vote_average&lt;/span&gt;
        &lt;span class="s"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model sees exactly two inputs, &lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;page&lt;/code&gt;. Everything else is handled for it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$lookup&lt;/code&gt; translates the friendly &lt;code&gt;popular&lt;/code&gt; into the raw &lt;code&gt;popularity.desc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;include_adult&lt;/code&gt; and &lt;code&gt;language&lt;/code&gt; are injected as constants the model never sees.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;response&lt;/code&gt; keeps four fields per movie and renames &lt;code&gt;vote_average&lt;/code&gt; to &lt;code&gt;rating&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A call with no arguments goes out as &lt;code&gt;sort_by=popularity.desc&amp;amp;page=1&amp;amp;include_adult=false&amp;amp;language=en-US&lt;/code&gt;, and comes back as a short list of &lt;code&gt;{title, overview, release_date, rating}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSONata
&lt;/h2&gt;

&lt;p&gt;The response side looks like a query, and a query language could do it. The request side cannot. Building &lt;code&gt;sort_by&lt;/code&gt; from a lookup table, injecting constants, or fanning one friendly &lt;code&gt;query&lt;/code&gt; out into the repeated &lt;code&gt;f[]&lt;/code&gt; / &lt;code&gt;op[]&lt;/code&gt; / &lt;code&gt;v[]&lt;/code&gt; parameters of a filter DSL is construction, not selection. JMESPath and JSONPath query, they do not build. JSONata does both, so one engine covers request and response.&lt;/p&gt;

&lt;p&gt;Two idioms carry most of the weight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$lookup(table, key)&lt;/code&gt;&lt;/strong&gt; maps a friendly enum onto the raw API value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[ ... ]&lt;/code&gt;&lt;/strong&gt; forces a list. A projection like &lt;code&gt;results.{...}&lt;/code&gt; unwraps to a single object when only one item matches, so the array constructor keeps it a list every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To pass most arguments straight through and change only a few, merge them with &lt;code&gt;$merge([$, { ... }])&lt;/code&gt;, where &lt;code&gt;$&lt;/code&gt; is the whole input.&lt;/p&gt;

&lt;h2&gt;
  
  
  merge vs replace
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;replace&lt;/code&gt; is for a full reshape. You hide the raw API entirely and declare a clean interface, as above. &lt;code&gt;merge&lt;/code&gt; is for a light touch on an API that is already close.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;merge&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;internal_flag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;hidden&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;per_page&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the spec's other parameters stay visible, &lt;code&gt;internal_flag&lt;/code&gt; is hidden, and &lt;code&gt;per_page&lt;/code&gt; gains a default. Naming a parameter the spec does not define is a startup error, so a typo fails loudly instead of silently dropping a field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loud Failures
&lt;/h2&gt;

&lt;p&gt;Both expressions compile at startup, so a broken JSONata takes the server down on boot with a clear message rather than on the first call. A runtime evaluation failure comes back as an &lt;code&gt;isError&lt;/code&gt; result that names the side that broke, request or response.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does shaping work in dynamic exposure mode?
&lt;/h3&gt;

&lt;p&gt;Yes. A dynamically exposed operation gets the same shaped input schema, &lt;code&gt;request&lt;/code&gt;, and &lt;code&gt;response&lt;/code&gt; as the static path. The one thing dynamic mode drops is per-operation tool annotations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;One OpenAPI operation no longer has to mean one bad tool. &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;strategy&lt;/code&gt; shape what the model sees, and &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;response&lt;/code&gt; shape the bytes on the wire, all from one YAML block next to the operation. If you try it on a real API, I would love to hear which operations were the worst to tame.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;github.com/mroops0111/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/openapi-mcp-gateway/" rel="noopener noreferrer"&gt;pypi.org/project/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built Conciergent: Give Your MCP Tools a Slack, LINE, or Discord Chat Face, With Real In-Chat OAuth</title>
      <dc:creator>MrOops</dc:creator>
      <pubDate>Tue, 14 Jul 2026 10:36:20 +0000</pubDate>
      <link>https://dev.to/mroops/i-built-conciergent-give-your-mcp-tools-a-slack-line-or-discord-chat-face-with-real-in-chat-4o4l</link>
      <guid>https://dev.to/mroops/i-built-conciergent-give-your-mcp-tools-a-slack-line-or-discord-chat-face-with-real-in-chat-4o4l</guid>
      <description>&lt;p&gt;A while back I built &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;openapi-mcp-gateway&lt;/a&gt;, which turns any OpenAPI spec into an MCP server. That solved the supply side. But the people who most wanted those tools, product folks, support staff, ops, were never going to install Claude Desktop and paste a server URL into a config file. They live in Slack and LINE.&lt;/p&gt;

&lt;p&gt;The real work was putting a chat face on those tools, one a non-technical person can just talk to. Once you try it, three things stop being optional very fast.&lt;/p&gt;

&lt;p&gt;You need &lt;strong&gt;per-user auth that happens inside the chat&lt;/strong&gt;, because you cannot send someone to a JSON config to complete an OAuth flow, and you certainly cannot let the whole team act as one shared service account. You need &lt;strong&gt;a confirmation step before anything destructive runs&lt;/strong&gt;, because an LLM with write access to a real API will eventually try to do the wrong thing confidently. And you need &lt;strong&gt;replies that render natively on each platform&lt;/strong&gt;, because a wall of Markdown that looks fine in one client is broken in the next.&lt;/p&gt;

&lt;p&gt;By the time all three worked, I had &lt;strong&gt;&lt;a href="https://github.com/mroops0111/conciergent" rel="noopener noreferrer"&gt;Conciergent&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It connects to any Model Context Protocol server (or an OpenAPI spec directly) and turns it into a Slack, LINE, or Discord bot that can actually &lt;em&gt;do&lt;/em&gt; things. Per-user OAuth is handled inside the conversation, destructive tools pause behind an approval card, and the agent emits one structured reply that each surface renders on its own. You point it at a YAML and it runs.&lt;/p&gt;

&lt;p&gt;Who it is for matters as much as what it does. This is a bot you stand up for your users, each signing in as themselves, not a personal assistant you run for yourself.&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%2Fm5io7r84fg47bsshkqga.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%2Fm5io7r84fg47bsshkqga.png" alt="Conciergent architecture, layered top to bottom. Chat surfaces (Slack, LINE, Discord, and more) sit on top. An incoming message flows down into a surface- and agent-agnostic runtime that produces one structured reply (plain text, a Card, or a Carousel). The runtime hands each turn to an AI agent powered by Pydantic AI, which resolves to a normal reply, an in-chat OAuth authorization, or a human-in-the-loop confirmation. The agent calls MCP tools (an OpenAPI spec via the embedded openapi-mcp-gateway, or any MCP server) and stores messages in Redis and credentials in Postgres. The reply flows back up to each surface." width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="s2"&gt;"conciergent[gateway]"&lt;/span&gt;
uv run conciergent init      &lt;span class="c"&gt;# writes an annotated manifest.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scaffolded &lt;code&gt;manifest.yml&lt;/code&gt; is a full config with the surface and store already wired up. Point the agent at an MCP server, or drop in an OpenAPI spec, set your Slack, LINE, or Discord credentials as env vars, and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run conciergent run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fastest end-to-end path is the bundled Docker compose, which brings up Redis, Postgres, and the app together from one of the example configs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;examples/openapi-chat.yml manifest.yml
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.12+. Three model providers ship in the box (OpenAI, Google Gemini, Anthropic Claude). Set &lt;code&gt;agent.model&lt;/code&gt; to a &lt;code&gt;provider:model&lt;/code&gt; string and export that provider's key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Conciergent Actually Does
&lt;/h2&gt;

&lt;p&gt;Three things, each shown in config.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Any MCP Server, or an OpenAPI Spec Directly
&lt;/h3&gt;

&lt;p&gt;Most useful bots touch more than one thing. Point the agent at any MCP server URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# manifest.yml&lt;/span&gt;
&lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai:gpt-4o-mini&lt;/span&gt;
  &lt;span class="na"&gt;system_prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;You are a helpful assistant. Use your tools to answer the user's requests.&lt;/span&gt;
  &lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9000/mcp&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://another-server.example.com/mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if what you have is a REST API rather than an MCP server, you do not need to run a second process for it. Add the &lt;code&gt;gateway&lt;/code&gt; extra and Conciergent embeds openapi-mcp-gateway &lt;strong&gt;in-process&lt;/strong&gt;, so a spec becomes MCP tools right there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petstore&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore3.swagger.io/api/v3/openapi.json&lt;/span&gt;
      &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore3.swagger.io/api/v3&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
      &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./internal-api.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each spec is served at &lt;code&gt;/{name}/mcp&lt;/code&gt; and wired into the agent for you, alongside anything already in &lt;code&gt;agent.mcp_servers&lt;/code&gt;. A spec entry mirrors openapi-mcp-gateway's per-server config, so a large API can use &lt;code&gt;exposure: dynamic&lt;/code&gt; (the agent sees three meta-tools instead of one per endpoint), a &lt;code&gt;policy&lt;/code&gt; filter to keep endpoints away from the model, or &lt;code&gt;auth&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. In-Chat OAuth (The Constraint That Drove The Design)
&lt;/h3&gt;

&lt;p&gt;This is the part that makes a chat bot different from an MCP client on your laptop. When a tool needs authorization, there is no config file to edit and no desktop app to bounce through. The user is in a Slack, LINE, or Discord DM, and the authorization has to happen right there.&lt;/p&gt;

&lt;p&gt;So Conciergent runs the whole OAuth handoff inside the conversation. The first time a tool needs a token, the bot posts the authorization link into the chat. The user taps it, approves, and comes back to the same DM. Conciergent stores the token and refreshes it from then on, per user. Nobody leaves the conversation, and nobody shares one team-wide credential.&lt;/p&gt;

&lt;p&gt;The same handoff covers an &lt;code&gt;oauth2&lt;/code&gt; spec behind the embedded gateway, so each user authorizes their own upstream account before its tools run. And because the flow is time-bounded, a slow authorization expires cleanly instead of hanging the request.&lt;/p&gt;

&lt;p&gt;Tokens persist in Postgres, so a redeploy does not sign everyone out. Chat history lives only in Redis, kept just long enough for context before it expires on its own, along with the other transient state (approvals, the pending handoff). Nothing a user says is retained past that window.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. An Approval Gate, and One Reply That Renders Everywhere
&lt;/h3&gt;

&lt;p&gt;Two problems, one design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-in-the-loop.&lt;/strong&gt; Any tool the MCP server marks destructive pauses behind a Confirm / Cancel card before it runs. The model proposes the action, and the human commits it. A pending approval has its own TTL, so an ignored card does not sit forever holding a write open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surface-agnostic replies.&lt;/strong&gt; The agent never speaks Slack, LINE, or Discord. It emits one of three shapes, and each surface renders it natively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;str&lt;/code&gt;&lt;/strong&gt; for plain text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Card&lt;/code&gt;&lt;/strong&gt; for a header, text sections, an optional hero image, a footnote, link buttons, and suggestion quick-replies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Carousel&lt;/code&gt;&lt;/strong&gt; for a small set of option cards the user picks between.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A suggestion is the interactive primitive. Tapping one posts its prompt back to the agent as if the user had typed it. The field descriptions on these models &lt;em&gt;are&lt;/em&gt; the agent's structured-output schema, so the model fills them in directly and the same reply object renders correctly on Slack, LINE, and Discord.&lt;/p&gt;

&lt;p&gt;UI text (buttons, prompts, greetings) is not hardcoded either. It lives in a locale catalog, picked from each user's platform language, so the bot answers people in their own language with no per-message translation calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Compares
&lt;/h2&gt;

&lt;p&gt;The question that sorts this field is not which agent has the most features, but who the bot is for. Conciergent is built for a service provider who wants to give their own users a bot on the Slack, LINE, or Discord those users already have. Most of the projects it gets compared to are built for someone else, so they make different trade-offs. Two groups come up the most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personal assistants&lt;/strong&gt; like &lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;Hermes Agent&lt;/a&gt; and &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;. You run one for yourself and reach it from any chat app. They do far more than Conciergent (voice, memory, self-improving skills, dozens of channels), but each install serves a single owner with a single set of credentials, so there is no notion of separate end-users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted AI platforms&lt;/strong&gt; like &lt;a href="https://github.com/langgenius/dify" rel="noopener noreferrer"&gt;Dify&lt;/a&gt;, &lt;a href="https://github.com/open-webui/open-webui" rel="noopener noreferrer"&gt;Open WebUI&lt;/a&gt;, and &lt;a href="https://github.com/danny-avila/LibreChat" rel="noopener noreferrer"&gt;LibreChat&lt;/a&gt;. They overlap the most, with multi-tenant deployments, per-user OAuth, native MCP, and heavy maintenance. But your users reach them in the platform's own web app. Getting onto Slack or Discord means a relay that pipes that web-app chat into the DM, text-first, with no native cards and no per-user OAuth in the conversation.&lt;/li&gt;
&lt;/ul&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;Conciergent&lt;/th&gt;
&lt;th&gt;Personal assistants&lt;/th&gt;
&lt;th&gt;Self-hosted AI platforms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Built for&lt;/td&gt;
&lt;td&gt;Your users&lt;/td&gt;
&lt;td&gt;Just you&lt;/td&gt;
&lt;td&gt;Your users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surface&lt;/td&gt;
&lt;td&gt;Their Slack / LINE / Discord&lt;/td&gt;
&lt;td&gt;20+ chat apps&lt;/td&gt;
&lt;td&gt;Its own web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separate end-users&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;In web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-user OAuth&lt;/td&gt;
&lt;td&gt;Yes, in chat&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, in its web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native cards and carousels&lt;/td&gt;
&lt;td&gt;Yes, per surface&lt;/td&gt;
&lt;td&gt;Text-first&lt;/td&gt;
&lt;td&gt;Web app only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval gate&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP and OpenAPI to tools&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;MCP only&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The assistants that reach these surfaces run for a single owner, and the multi-tenant platforms stay in a web app. Neither puts a per-user bot with native replies in the chat itself.&lt;/p&gt;

&lt;p&gt;It fits three situations in particular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal tools for non-technical teammates.&lt;/strong&gt; Support, ops, finance. They will never open an MCP config, but they will DM a bot. Point Conciergent at your internal MCP server (or spec) and they talk to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything where actions must happen as the actual user.&lt;/strong&gt; Approving a request, moving a record, closing a ticket on someone's behalf, where the upstream audit log has to show the human. The in-chat per-user OAuth handles that end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A REST API you want to demo as a chatbot fast.&lt;/strong&gt; Enable the gateway, drop in the spec, and you have a Slack, LINE, or Discord bot over it with no second server to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is And Is Not There Yet
&lt;/h2&gt;

&lt;p&gt;Everything above ships today.&lt;/p&gt;

&lt;p&gt;From here, the main line of work is more surfaces. Each one is a single class behind the surface contract, with the runtime and agent unchanged, so the list keeps growing without a rewrite. Telegram and Teams are next. If your platform or use case is not there yet, open an issue with the shape of your problem. Concrete use cases drive the design more than my guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Conciergent is the companion to &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;openapi-mcp-gateway&lt;/a&gt;. The gateway turns a REST API into MCP tools, and Conciergent gives those tools a chat face. Together they take one or more OpenAPI specs all the way to a bot your users can talk to. Like the gateway, this is a personal open-source side project. Sharing it here in case the situations above sound like yours.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/mroops0111/conciergent" rel="noopener noreferrer"&gt;github.com/mroops0111/conciergent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Companion Project:&lt;/strong&gt; &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;github.com/mroops0111/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>messenger</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Expose a Huge OpenAPI Spec as MCP Tools and Resources</title>
      <dc:creator>MrOops</dc:creator>
      <pubDate>Tue, 09 Jun 2026 15:36:32 +0000</pubDate>
      <link>https://dev.to/mroops/openapi-mcp-gateway-resources-dynamic-exposure-and-spec-compliant-auth-1eob</link>
      <guid>https://dev.to/mroops/openapi-mcp-gateway-resources-dynamic-exposure-and-spec-compliant-auth-1eob</guid>
      <description>&lt;p&gt;A 1,000-endpoint OpenAPI spec should not ship 1,000 tool schemas at connect time, and a read-only GET is often a better MCP resource than a tool. &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;openapi-mcp-gateway&lt;/a&gt; fronts huge specs so they do not blow the context window, promotes eligible GETs to resources, and keeps the auth in between spec-compliant. Four pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dynamic exposure for huge specs like GitHub&lt;/li&gt;
&lt;li&gt;Auto-promote eligible GETs to MCP resources&lt;/li&gt;
&lt;li&gt;MCP spec compliance (audience-bound auth + protocol-native tool metadata)&lt;/li&gt;
&lt;li&gt;Tool name and description override&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;

&lt;p&gt;One config exercising every new surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# servers.yml&lt;/span&gt;
&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Eligible GETs become MCP resources; everything else stays a tool.&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petstore&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore3.swagger.io/api/v3/openapi.json&lt;/span&gt;
    &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore.swagger.io/v2&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;

  &lt;span class="c1"&gt;# ~1,200 ops behind three meta-tools instead of 1,200 schemas in tools/list.&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json&lt;/span&gt;
    &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bearer&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_TOKEN}&lt;/span&gt;

  &lt;span class="c1"&gt;# Per-user OAuth2, audience-bound tokens, no passthrough.&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;asana&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oauth2&lt;/span&gt;
      &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ASANA_CLIENT_ID}&lt;/span&gt;
      &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ASANA_CLIENT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;openid&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;profile&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx openapi-mcp-gateway &lt;span class="nt"&gt;--config&lt;/span&gt; servers.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you get at &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/petstore/mcp&lt;/code&gt;: 13 tools + 6 resources (3 concrete, 3 templates), partitioned by &lt;code&gt;mode: auto&lt;/code&gt; with no spec edits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/github/mcp&lt;/code&gt;: three meta-tools (&lt;code&gt;list_operations&lt;/code&gt;, &lt;code&gt;get_operation&lt;/code&gt;, &lt;code&gt;call_operation&lt;/code&gt;) fronting ~1,200 endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/asana/mcp&lt;/code&gt;: per-user OAuth2 against Asana's IdP.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Dynamic Exposure: Three Meta-Tools for Huge Specs
&lt;/h2&gt;

&lt;p&gt;GitHub's REST API spec carries 1,190 operations. Static registration ships every name, description, and JSON Schema at connect time. At ~300 tokens per tool that is 350K+ tokens in &lt;code&gt;tools/list&lt;/code&gt; alone, larger than any frontier model's context window, before the agent makes a call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json&lt;/span&gt;
    &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bearer&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_TOKEN}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client now sees three tools instead of 1,190:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_operations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&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;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# returns: {"operations": [{"name": str, "description": str}, ...]}
&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;get_operation&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&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;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# returns: {"name": str, "description": str, "input_schema": JSONSchema}
&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_operation&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="nb"&gt;str&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="nb"&gt;dict&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;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&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;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# returns: upstream response wrapped as CallToolResult
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent walks them in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;list_operations()&lt;/code&gt; to discover.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_operation("issues_create")&lt;/code&gt; to read the schema for the one it wants.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;call_operation(name, args)&lt;/code&gt; to invoke.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Auth, path templating, and per-operation request shape are identical to static mode. Only the surfacing changes.&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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDbGllbnQKICAgIHBhcnRpY2lwYW50IEdhdGV3YXkKICAgIHBhcnRpY2lwYW50IFVwc3RyZWFtCgogICAgQ2xpZW50LT4-R2F0ZXdheTogbGlzdF9vcGVyYXRpb25zKCkKICAgIEdhdGV3YXktLT4-Q2xpZW50OiBuYW1lICsgZGVzY3JpcHRpb24gbGlzdAogICAgQ2xpZW50LT4-R2F0ZXdheTogZ2V0X29wZXJhdGlvbihuYW1lKQogICAgR2F0ZXdheS0tPj5DbGllbnQ6IGlucHV0X3NjaGVtYQogICAgQ2xpZW50LT4-R2F0ZXdheTogY2FsbF9vcGVyYXRpb24obmFtZSwgYXJncykKICAgIEdhdGV3YXktPj5VcHN0cmVhbTogSFRUUAogICAgVXBzdHJlYW0tLT4-R2F0ZXdheTogcmVzcG9uc2UKICAgIEdhdGV3YXktLT4-Q2xpZW50OiBDYWxsVG9vbFJlc3VsdA%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDbGllbnQKICAgIHBhcnRpY2lwYW50IEdhdGV3YXkKICAgIHBhcnRpY2lwYW50IFVwc3RyZWFtCgogICAgQ2xpZW50LT4-R2F0ZXdheTogbGlzdF9vcGVyYXRpb25zKCkKICAgIEdhdGV3YXktLT4-Q2xpZW50OiBuYW1lICsgZGVzY3JpcHRpb24gbGlzdAogICAgQ2xpZW50LT4-R2F0ZXdheTogZ2V0X29wZXJhdGlvbihuYW1lKQogICAgR2F0ZXdheS0tPj5DbGllbnQ6IGlucHV0X3NjaGVtYQogICAgQ2xpZW50LT4-R2F0ZXdheTogY2FsbF9vcGVyYXRpb24obmFtZSwgYXJncykKICAgIEdhdGV3YXktPj5VcHN0cmVhbTogSFRUUAogICAgVXBzdHJlYW0tLT4-R2F0ZXdheTogcmVzcG9uc2UKICAgIEdhdGV3YXktLT4-Q2xpZW50OiBDYWxsVG9vbFJlc3VsdA%3Ftype%3Dpng" alt="Dynamic exposure walk: list_operations, get_operation, call_operation, then upstream HTTP" width="691" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  When NOT to Use It
&lt;/h3&gt;

&lt;p&gt;Dynamic exposure adds two round trips before the first real call. For small specs that fit comfortably in context, those round trips are pure latency tax.&lt;/p&gt;

&lt;p&gt;Rough rule of thumb (the numbers are guidance, not benchmarks):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation Count&lt;/th&gt;
&lt;th&gt;Recommended Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Under ~30&lt;/td&gt;
&lt;td&gt;&lt;code&gt;static&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 to ~100&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;static&lt;/code&gt; + &lt;code&gt;policy.allow&lt;/code&gt; narrowing to the hot subset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over ~100&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dynamic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is a per-server flag, so &lt;code&gt;/github/mcp&lt;/code&gt; can run &lt;code&gt;dynamic&lt;/code&gt; while &lt;code&gt;/petstore/mcp&lt;/code&gt; runs &lt;code&gt;static&lt;/code&gt; in the same gateway.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Resources Are a Primitive, and Most GETs Should Be One
&lt;/h2&gt;

&lt;p&gt;Tools are actions. The client picks one, fills in arguments, and the result enters the conversation. Most OpenAPI-to-MCP gateways register every operation that way. But MCP has a second primitive. Resources are addressable content returned as &lt;code&gt;text&lt;/code&gt; or &lt;code&gt;blob&lt;/code&gt;, and a &lt;code&gt;GET /pets/{petId}&lt;/code&gt; or &lt;code&gt;GET /store/inventory&lt;/code&gt; is a far better fit for the resource shape than the tool shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode: Auto
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petstore&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore3.swagger.io/api/v3/openapi.json&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At startup the gateway partitions every operation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation Shape&lt;/th&gt;
&lt;th&gt;Becomes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET, no required &lt;code&gt;query&lt;/code&gt; / &lt;code&gt;header&lt;/code&gt; / &lt;code&gt;body&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything else (POST, PUT, DELETE, GET with required args)&lt;/td&gt;
&lt;td&gt;Tool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vanilla Petstore3 produces 13 tools, 3 concrete resources, and 3 resource templates. The default &lt;code&gt;mode: tool_only&lt;/code&gt; keeps everything as tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Operation YAML Overrides
&lt;/h3&gt;

&lt;p&gt;When you want finer control without forking the spec, layer overrides in the same server YAML. Common reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rename a resource&lt;/li&gt;
&lt;li&gt;Set a custom URI template&lt;/li&gt;
&lt;li&gt;Set a non-JSON MIME type
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petstore&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore3.swagger.io/api/v3/openapi.json&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;
    &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;getPetById&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pet&lt;/span&gt;
          &lt;span class="na"&gt;uri_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petstore://v2/pet/{petId}&lt;/span&gt;
      &lt;span class="na"&gt;getInventory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inventory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keys match &lt;code&gt;operationId&lt;/code&gt;. Unknown ids raise at startup so typos do not silently no-op. Each entry fully replaces any spec-side &lt;code&gt;x-mcp-integration&lt;/code&gt; for that operation. This is the path when the upstream spec belongs to a vendor or a different team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spec-Side Opt-In
&lt;/h3&gt;

&lt;p&gt;If you own the spec, the same opt-in fits inline as &lt;code&gt;x-mcp-integration.resource&lt;/code&gt; on the operation. All three paths (&lt;code&gt;mode&lt;/code&gt;, YAML, spec) land in the same registration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spec Shape&lt;/th&gt;
&lt;th&gt;Registered As&lt;/th&gt;
&lt;th&gt;Example URI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET with path params&lt;/td&gt;
&lt;td&gt;Resource Template&lt;/td&gt;
&lt;td&gt;&lt;code&gt;petstore://pet/{petId}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET without path params&lt;/td&gt;
&lt;td&gt;Concrete Resource&lt;/td&gt;
&lt;td&gt;&lt;code&gt;petstore://store/inventory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Auth, base URL, and retries are shared with the tool path.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. MCP Spec Compliance
&lt;/h2&gt;

&lt;p&gt;The 2025-11-25 MCP spec tightened requirements on two fronts: how the server handles tokens it forwards upstream, and what shape it returns tool results in. The gateway handles both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audience-Bound Auth
&lt;/h3&gt;

&lt;p&gt;A naive gateway forwards the MCP client's token straight to whatever third-party upstream the tool eventually hits. That is the &lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#token-passthrough" rel="noopener noreferrer"&gt;token passthrough anti-pattern&lt;/a&gt;, and the spec explicitly forbids it.&lt;/p&gt;

&lt;p&gt;The new default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gateway is an OAuth client to the upstream and mints upstream tokens server-side for &lt;code&gt;authorization_code&lt;/code&gt; and &lt;code&gt;client_credentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;MCP client's token is never forwarded to a different audience.&lt;/li&gt;
&lt;li&gt;Tokens are bound to the gateway's canonical resource URI per &lt;a href="https://www.rfc-editor.org/rfc/rfc8707" rel="noopener noreferrer"&gt;RFC 8707&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Passthrough is opt-in (&lt;code&gt;auth.flow: passthrough&lt;/code&gt;) and only safe when gateway and upstream share the same audience. The FastAPI integration is the one place this opts in automatically.&lt;/li&gt;
&lt;/ul&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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDbGllbnQKICAgIHBhcnRpY2lwYW50IEdhdGV3YXkKICAgIHBhcnRpY2lwYW50IFVwc3RyZWFtCgogICAgTm90ZSBvdmVyIENsaWVudCxVcHN0cmVhbTogVG9rZW4gcGFzc3Rocm91Z2gKICAgIENsaWVudC0-PkdhdGV3YXk6IEJlYXJlciBtY3BfdG9rZW4KICAgIEdhdGV3YXktPj5VcHN0cmVhbTogQmVhcmVyIG1jcF90b2tlbgogICAgVXBzdHJlYW0tLT4-R2F0ZXdheTogMjAwCgogICAgTm90ZSBvdmVyIENsaWVudCxVcHN0cmVhbTogQXVkaWVuY2UtYm91bmQgKGRlZmF1bHQpCiAgICBDbGllbnQtPj5HYXRld2F5OiBCZWFyZXIgbWNwX3Rva2VuCiAgICBHYXRld2F5LT4-R2F0ZXdheTogbWludCB1cHN0cmVhbV90b2tlbgogICAgR2F0ZXdheS0-PlVwc3RyZWFtOiBCZWFyZXIgdXBzdHJlYW1fdG9rZW4KICAgIFVwc3RyZWFtLS0-PkdhdGV3YXk6IDIwMA%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDbGllbnQKICAgIHBhcnRpY2lwYW50IEdhdGV3YXkKICAgIHBhcnRpY2lwYW50IFVwc3RyZWFtCgogICAgTm90ZSBvdmVyIENsaWVudCxVcHN0cmVhbTogVG9rZW4gcGFzc3Rocm91Z2gKICAgIENsaWVudC0-PkdhdGV3YXk6IEJlYXJlciBtY3BfdG9rZW4KICAgIEdhdGV3YXktPj5VcHN0cmVhbTogQmVhcmVyIG1jcF90b2tlbgogICAgVXBzdHJlYW0tLT4-R2F0ZXdheTogMjAwCgogICAgTm90ZSBvdmVyIENsaWVudCxVcHN0cmVhbTogQXVkaWVuY2UtYm91bmQgKGRlZmF1bHQpCiAgICBDbGllbnQtPj5HYXRld2F5OiBCZWFyZXIgbWNwX3Rva2VuCiAgICBHYXRld2F5LT4-R2F0ZXdheTogbWludCB1cHN0cmVhbV90b2tlbgogICAgR2F0ZXdheS0-PlVwc3RyZWFtOiBCZWFyZXIgdXBzdHJlYW1fdG9rZW4KICAgIFVwc3RyZWFtLS0-PkdhdGV3YXk6IDIwMA%3Ftype%3Dpng" alt="Token flow: passthrough forwards the MCP client token; current default mints an upstream-audience token instead" width="669" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;authorization_code&lt;/code&gt; is configured without &lt;code&gt;client_id&lt;/code&gt; / &lt;code&gt;client_secret&lt;/code&gt; (i.e. you meant to mint upstream tokens but forgot credentials) the gateway logs an INFO message and falls back to passthrough. Set the credentials, or set &lt;code&gt;auth.flow&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protocol-Native Tool Metadata
&lt;/h3&gt;

&lt;p&gt;The other half is what comes back in &lt;code&gt;tools/call&lt;/code&gt;. Generated tools now emit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAPI &lt;code&gt;summary&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Clients show "Fetch one pet by id" instead of &lt;code&gt;getPetById&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;annotations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP method&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;readOnlyHint&lt;/code&gt; for GET, &lt;code&gt;destructiveHint&lt;/code&gt; for DELETE, &lt;code&gt;idempotentHint&lt;/code&gt; for GET/PUT/PATCH/DELETE.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;structuredContent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parsed JSON body (success and error)&lt;/td&gt;
&lt;td&gt;Agent reads error codes etc. without re-parsing the text body.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tool callables also return &lt;code&gt;CallToolResult&lt;/code&gt; directly. Upstream 4xx/5xx no longer raises out of the tool. It returns &lt;code&gt;isError=True&lt;/code&gt; with the parsed body in &lt;code&gt;structuredContent&lt;/code&gt;. The agent can read the error code, retry with backoff, or surface a useful message. Network failures get the same treatment.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Tool Name and Description Override
&lt;/h2&gt;

&lt;p&gt;Two recurring problems with OpenAPI specs in the wild:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ugly operationIds. The GitHub spec ships &lt;code&gt;actions/list-jobs-for-workflow-run-attempt&lt;/code&gt; and &lt;code&gt;packages/get-all-package-versions-for-package-owned-by-authenticated-user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Empty descriptions. Most of the GitHub &lt;code&gt;gists/*&lt;/code&gt; endpoints (&lt;code&gt;gists/delete&lt;/code&gt;, &lt;code&gt;gists/list-commits&lt;/code&gt;, &lt;code&gt;gists/fork&lt;/code&gt;, ...) ship with &lt;code&gt;description: ""&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM has to guess intent from the name alone. Override via &lt;code&gt;x-mcp-integration.tool&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;x-mcp-integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;list_gist_commits&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;List commits for a Gist. Returns up to 30 commits per page with&lt;/span&gt;
      &lt;span class="s"&gt;commit SHA, author, and timestamp. Use ?page= for pagination.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap fix with outsized impact when the agent picks the wrong tool because two operations had near-identical default descriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use dynamic exposure and resources together?
&lt;/h3&gt;

&lt;p&gt;No. Dynamic exposure surfaces every operation uniformly behind the three meta-tools, so resource promotion and any &lt;code&gt;x-mcp-integration.resource&lt;/code&gt; opt-ins are skipped, with a warning at startup. Pick &lt;code&gt;exposure: dynamic&lt;/code&gt; for a huge spec, or &lt;code&gt;mode: auto&lt;/code&gt; for resource partitioning, per server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;A huge spec does not have to flood the context window, and a read-only GET does not have to be a tool. &lt;code&gt;exposure: dynamic&lt;/code&gt; and &lt;code&gt;mode: auto&lt;/code&gt; handle those, and the auth in between stays spec-compliant. If you try it, open an issue with what broke or felt awkward.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;github.com/mroops0111/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/openapi-mcp-gateway/" rel="noopener noreferrer"&gt;pypi.org/project/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Turn an OpenAPI Spec into an MCP Server in Python</title>
      <dc:creator>MrOops</dc:creator>
      <pubDate>Sun, 10 May 2026 15:21:31 +0000</pubDate>
      <link>https://dev.to/mroops/i-built-openapi-mcp-gateway-multi-spec-openapi-to-mcp-with-real-oauth2-in-python-46d2</link>
      <guid>https://dev.to/mroops/i-built-openapi-mcp-gateway-multi-spec-openapi-to-mcp-with-real-oauth2-in-python-46d2</guid>
      <description>&lt;p&gt;This gateway started as a demo. The job was to turn our company's OpenAPI spec into an MCP server so non-technical product folks could try the API through Claude Desktop. The naive version came together quickly.&lt;/p&gt;

&lt;p&gt;Then the demo grew. For a customer-support-style agent to be useful, it needed to chain our product API with the internal issue tracker, and both gate access on the user's identity, not a shared service token. The audience was non-technical, so streamable HTTP was the floor. But I did not know what the eventual production client would be, so I wanted stdio and SSE on the same binary as well. And because the spec was actively changing, I needed the MCP layer to track it automatically rather than be hand-maintained.&lt;/p&gt;

&lt;p&gt;By the time everything ran stably (multi-API, real per-user OAuth2, all three MCP transports, auto-regenerated from the spec), I had built &lt;strong&gt;&lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;openapi-mcp-gateway&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You point it at a YAML and it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx openapi-mcp-gateway &lt;span class="nt"&gt;--spec&lt;/span&gt; https://petstore3.swagger.io/api/v3/openapi.json &lt;span class="nt"&gt;--name&lt;/span&gt; petstore
&lt;span class="c"&gt;# MCP server live at http://127.0.0.1:8000/petstore/mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or add it as a dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add openapi-mcp-gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect from Claude Desktop, Cursor, Cline, or any other MCP client. Streamable HTTP, SSE, and stdio are all supported. Python 3.11+.&lt;/p&gt;

&lt;h2&gt;
  
  
  What The Gateway Actually Does
&lt;/h2&gt;

&lt;p&gt;Three things, each shown in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Multiple APIs In One Process
&lt;/h3&gt;

&lt;p&gt;Most agent tasks touch more than one API. GitHub for repo lookups, your internal product API for the actual work, an external CRM. Point a single YAML at all of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# servers.yml&lt;/span&gt;
&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8000&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petstore&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://petstore3.swagger.io/api/v3/openapi.json&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bearer&lt;/span&gt;
      &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_TOKEN}&lt;/span&gt;
    &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/repos/*"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/users/*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/repos/*/actions/secrets*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;asana&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oauth2&lt;/span&gt;
      &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ASANA_CLIENT_ID}&lt;/span&gt;
      &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ASANA_CLIENT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;openid&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;workspaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx openapi-mcp-gateway &lt;span class="nt"&gt;--config&lt;/span&gt; servers.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three MCP servers at &lt;code&gt;/petstore/mcp&lt;/code&gt;, &lt;code&gt;/github/mcp&lt;/code&gt;, &lt;code&gt;/asana/mcp&lt;/code&gt;. Each with its own auth. Each independently filterable with &lt;code&gt;policy.allow&lt;/code&gt; / &lt;code&gt;policy.deny&lt;/code&gt;, so you do not blast every endpoint of every spec at the model.&lt;/p&gt;

&lt;p&gt;Visually, this YAML produces:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptk29vmzAQxr-Kda82KSTBkH9oqkSp1E0aWrStqlTYCwcugAq2ZWx1WZTvPhNIFqb6lZ_z7-4eH-YImcgRAtjX4i0rmdLk6_eUE7tasysUkyWJ6gq5bpMU4mhLsl6l8KvHuhW5SVQzkyN5wPZVC3l7RpPIqFaoIYY8T_l_HR6fbXEhkTNZOU0mnYJpfGOHUZO8UpjpSnDy8_5fNHZt6kyibrVQOLPJn3ZqdscFYUaXowIx7dCi0qXZXcF7ZAoV0eIV-Zj2Opq1jLMr_C20NSmRqBzTorry793pSdr8J9lqhawh4fbLeGShm2wH07dRmjxW-rPZ3cY6I2Hn4-zhw6V77_njeyYilzjOnZ3NWNJB0l56vYz703CA4_40HODY66UHEyhUlUOglcEJNKga1kk4dmAKusQGUwjsNsc9M7VOIeUnmyYZfxGiuWQqYYoSgj2rW6uMzO23fqiYnVpzjSp7GVSRMFxD4M3PNSA4wm-rlqvpZuPPF75PN3TuLiZwgMBfTxdL6q_8zZquXW9Jl6cJ_Dl3nU_XKwvZ5yB-HHh2sYF5ZYcf96___BOc_gI6hegF%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNptk29vmzAQxr-Kda82KSTBkH9oqkSp1E0aWrStqlTYCwcugAq2ZWx1WZTvPhNIFqb6lZ_z7-4eH-YImcgRAtjX4i0rmdLk6_eUE7tasysUkyWJ6gq5bpMU4mhLsl6l8KvHuhW5SVQzkyN5wPZVC3l7RpPIqFaoIYY8T_l_HR6fbXEhkTNZOU0mnYJpfGOHUZO8UpjpSnDy8_5fNHZt6kyibrVQOLPJn3ZqdscFYUaXowIx7dCi0qXZXcF7ZAoV0eIV-Zj2Opq1jLMr_C20NSmRqBzTorry793pSdr8J9lqhawh4fbLeGShm2wH07dRmjxW-rPZ3cY6I2Hn4-zhw6V77_njeyYilzjOnZ3NWNJB0l56vYz703CA4_40HODY66UHEyhUlUOglcEJNKga1kk4dmAKusQGUwjsNsc9M7VOIeUnmyYZfxGiuWQqYYoSgj2rW6uMzO23fqiYnVpzjSp7GVSRMFxD4M3PNSA4wm-rlqvpZuPPF75PN3TuLiZwgMBfTxdL6q_8zZquXW9Jl6cJ_Dl3nU_XKwvZ5yB-HHh2sYF5ZYcf96___BOc_gI6hegF%3Ftype%3Dpng" alt="Multi-API gateway topology: one process, three mount paths, three different auth backends, three different upstream APIs" width="1221" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One process, three mount paths, three different auth backends, three different upstream identities. None of them leak into each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Real Per-User OAuth2 (Token Relay)
&lt;/h3&gt;

&lt;p&gt;This is the constraint that drove most of the design. When a user opens Claude Desktop and connects to one of the mounted servers:&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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDbGF1ZGUgYXMgTUNQIENsaWVudAogICAgcGFydGljaXBhbnQgR1cgYXMgR2F0ZXdheQogICAgcGFydGljaXBhbnQgVXBzdHJlYW0gYXMgVXBzdHJlYW0gT0F1dGggU2VydmVyCiAgICBwYXJ0aWNpcGFudCBBUEkgYXMgVXBzdHJlYW0gQVBJCgogICAgTm90ZSBvdmVyIENsYXVkZSxBUEk6IFBoYXNlIDEsIEF1dGhlbnRpY2F0aW9uCiAgICBDbGF1ZGUtPj5HVzogcmVnaXN0ZXIsIHRoZW4gYXV0aG9yaXplCiAgICBHVy0%2BPlVwc3RyZWFtOiByZWRpcmVjdCB1c2VyIHRvIGNvbnNlbnQKICAgIFVwc3RyZWFtLS0%2BPkdXOiBjYWxsYmFjayB3aXRoIGNvZGUKICAgIEdXLT4%2BVXBzdHJlYW06IGV4Y2hhbmdlIGNvZGUgZm9yIGFwaV90b2tlbgogICAgR1ctPj5HVzogbWludCBtY3BfdG9rZW4sIG1hcCBtY3BfdG9rZW4gdG8gYXBpX3Rva2VuCiAgICBHVy0tPj5DbGF1ZGU6IG1jcF90b2tlbgoKICAgIE5vdGUgb3ZlciBDbGF1ZGUsQVBJOiBQaGFzZSAyLCBUb29sIGNhbGwKICAgIENsYXVkZS0%2BPkdXOiBjYWxsIHRvb2wgd2l0aCBtY3BfdG9rZW4KICAgIEdXLT4%2BR1c6IHJlc29sdmUgbWNwX3Rva2VuIHRvIGFwaV90b2tlbgogICAgR1ctPj5BUEk6IGNhbGwgdXBzdHJlYW0gd2l0aCBhcGlfdG9rZW4KICAgIEFQSS0tPj5HVzogMjAwCiAgICBHVy0tPj5DbGF1ZGU6IHRvb2wgcmVzdWx0CgogICAgTm90ZSBvdmVyIENsYXVkZSxBUEk6IFBoYXNlIDMsIFJlZnJlc2gKICAgIENsYXVkZS0%2BPkdXOiByZWZyZXNoIG1jcF90b2tlbgogICAgR1ctPj5VcHN0cmVhbTogcmVmcmVzaCBhcGlfdG9rZW4gaWYgZXhwaXJlZAogICAgR1ctLT4%2BQ2xhdWRlOiBuZXcgbWNwX3Rva2VuIHBhaXI%3D" 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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDbGF1ZGUgYXMgTUNQIENsaWVudAogICAgcGFydGljaXBhbnQgR1cgYXMgR2F0ZXdheQogICAgcGFydGljaXBhbnQgVXBzdHJlYW0gYXMgVXBzdHJlYW0gT0F1dGggU2VydmVyCiAgICBwYXJ0aWNpcGFudCBBUEkgYXMgVXBzdHJlYW0gQVBJCgogICAgTm90ZSBvdmVyIENsYXVkZSxBUEk6IFBoYXNlIDEsIEF1dGhlbnRpY2F0aW9uCiAgICBDbGF1ZGUtPj5HVzogcmVnaXN0ZXIsIHRoZW4gYXV0aG9yaXplCiAgICBHVy0%2BPlVwc3RyZWFtOiByZWRpcmVjdCB1c2VyIHRvIGNvbnNlbnQKICAgIFVwc3RyZWFtLS0%2BPkdXOiBjYWxsYmFjayB3aXRoIGNvZGUKICAgIEdXLT4%2BVXBzdHJlYW06IGV4Y2hhbmdlIGNvZGUgZm9yIGFwaV90b2tlbgogICAgR1ctPj5HVzogbWludCBtY3BfdG9rZW4sIG1hcCBtY3BfdG9rZW4gdG8gYXBpX3Rva2VuCiAgICBHVy0tPj5DbGF1ZGU6IG1jcF90b2tlbgoKICAgIE5vdGUgb3ZlciBDbGF1ZGUsQVBJOiBQaGFzZSAyLCBUb29sIGNhbGwKICAgIENsYXVkZS0%2BPkdXOiBjYWxsIHRvb2wgd2l0aCBtY3BfdG9rZW4KICAgIEdXLT4%2BR1c6IHJlc29sdmUgbWNwX3Rva2VuIHRvIGFwaV90b2tlbgogICAgR1ctPj5BUEk6IGNhbGwgdXBzdHJlYW0gd2l0aCBhcGlfdG9rZW4KICAgIEFQSS0tPj5HVzogMjAwCiAgICBHVy0tPj5DbGF1ZGU6IHRvb2wgcmVzdWx0CgogICAgTm90ZSBvdmVyIENsYXVkZSxBUEk6IFBoYXNlIDMsIFJlZnJlc2gKICAgIENsYXVkZS0%2BPkdXOiByZWZyZXNoIG1jcF90b2tlbgogICAgR1ctPj5VcHN0cmVhbTogcmVmcmVzaCBhcGlfdG9rZW4gaWYgZXhwaXJlZAogICAgR1ctLT4%2BQ2xhdWRlOiBuZXcgbWNwX3Rva2VuIHBhaXI%3D" alt="OAuth2 authorization_code with token relay: gateway as both OAuth Server to MCP client and OAuth Client to upstream API" width="947" height="1008"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The gateway plays &lt;strong&gt;two roles at once&lt;/strong&gt;, acting as an OAuth Authorization Server to the MCP client and as an OAuth Client to the upstream API. It mints its own MCP-scoped tokens with their own TTLs and revocation, and keeps a private mapping from those tokens to the upstream tokens.&lt;/p&gt;

&lt;p&gt;The result is the part that matters. The MCP client never holds a credential for an API it does not know about, and the upstream API's audit log shows the actual end user.&lt;/p&gt;

&lt;p&gt;And because every mounted server has its own auth config, &lt;strong&gt;one gateway process can run several different OAuth setups in parallel&lt;/strong&gt;: Asana OAuth on &lt;code&gt;/asana/mcp&lt;/code&gt;, GitHub bearer on &lt;code&gt;/github/mcp&lt;/code&gt;, your internal OAuth on &lt;code&gt;/myapp/mcp&lt;/code&gt;, all at once. Three different upstream identities, three independent OAuth flows, no cross-talk in the token store, the auth resolver, or the request path. The Asana token issued to one user cannot be confused with the GitHub bearer used to look up a repo. Each &lt;code&gt;(server, user)&lt;/code&gt; pair lives in its own namespace.&lt;/p&gt;

&lt;p&gt;For service-to-service calls, &lt;code&gt;client_credentials&lt;/code&gt; is also supported, with lazy fetch and concurrent-refresh dedup under an &lt;code&gt;asyncio.Lock&lt;/code&gt; so N concurrent tool calls hitting the gateway at expiry produce exactly one IdP request. Different shape, different &lt;code&gt;OAuthFlowHandler&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. FastAPI-Native &lt;code&gt;@mcp_tool&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Beyond raw OpenAPI specs, the gateway also takes a live FastAPI app as input. If you already have one, decorate the routes you want exposed:&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;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openapi_mcp_gateway&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mcp_tool&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.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;/items/{item_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@mcp_tool&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;read_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;item_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.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;/internal/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# not decorated, not exposed
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_fastapi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool calls run &lt;strong&gt;in-process&lt;/strong&gt; through &lt;code&gt;httpx.ASGITransport&lt;/code&gt;. No localhost roundtrip, no parallel spec, no separate uvicorn. Auth is auto-detected from the app's &lt;code&gt;securitySchemes&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Compares To Existing Tools
&lt;/h2&gt;

&lt;p&gt;I did the homework, so you do not have to. As of May 2026, here is the OpenAPI-to-MCP &lt;strong&gt;converter&lt;/strong&gt; landscape (tools whose primary job is "take an OpenAPI spec, produce an MCP server"):&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;mroops0111/openapi-mcp-gateway&lt;/th&gt;
&lt;th&gt;&lt;a href="https://github.com/tadata-org/fastapi_mcp" rel="noopener noreferrer"&gt;fastapi_mcp&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://github.com/awslabs/mcp" rel="noopener noreferrer"&gt;awslabs/mcp&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://github.com/ivo-toby/mcp-openapi-server" rel="noopener noreferrer"&gt;ivo-toby&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://github.com/harsha-iiiv/openapi-mcp-generator" rel="noopener noreferrer"&gt;harsha-iiiv&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://github.com/ckanthony/openapi-mcp" rel="noopener noreferrer"&gt;ckanthony&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;TS&lt;/td&gt;
&lt;td&gt;TS (codegen)&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;OpenAPI &lt;strong&gt;or&lt;/strong&gt; FastAPI app&lt;/td&gt;
&lt;td&gt;FastAPI app only&lt;/td&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-spec in one process&lt;/td&gt;
&lt;td&gt;Yes (YAML)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth2 authorization_code w/ relay&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Cognito only&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth2 client_credentials&lt;/td&gt;
&lt;td&gt;Yes (lazy + lock)&lt;/td&gt;
&lt;td&gt;Not documented&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stdio + SSE + Streamable HTTP&lt;/td&gt;
&lt;td&gt;All three&lt;/td&gt;
&lt;td&gt;SSE + StrHTTP&lt;/td&gt;
&lt;td&gt;stdio&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;All three&lt;/td&gt;
&lt;td&gt;Single port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FastAPI-native decorator&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Whole-app only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pluggable Redis token store&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your only input is a FastAPI app, &lt;strong&gt;&lt;a href="https://github.com/tadata-org/fastapi_mcp" rel="noopener noreferrer"&gt;fastapi_mcp&lt;/a&gt;&lt;/strong&gt; is more focused than mine. The in-process ASGI trick is its native idea, and I borrowed it for &lt;code&gt;@mcp_tool&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you need multi-spec on AWS and Cognito is your auth, &lt;strong&gt;&lt;a href="https://github.com/awslabs/mcp" rel="noopener noreferrer"&gt;awslabs/mcp&lt;/a&gt;&lt;/strong&gt; is purpose-built for that.&lt;/li&gt;
&lt;li&gt;If you want a TypeScript runtime, &lt;strong&gt;&lt;a href="https://github.com/ivo-toby/mcp-openapi-server" rel="noopener noreferrer"&gt;ivo-toby/mcp-openapi-server&lt;/a&gt;&lt;/strong&gt; is the closest analogue.&lt;/li&gt;
&lt;li&gt;If you want a generated Node project you control end-to-end, &lt;strong&gt;&lt;a href="https://github.com/harsha-iiiv/openapi-mcp-generator" rel="noopener noreferrer"&gt;harsha-iiiv/openapi-mcp-generator&lt;/a&gt;&lt;/strong&gt; is the right shape.&lt;/li&gt;
&lt;li&gt;If you want a Python service you point at a YAML, that hosts several specs at once, that does both OAuth2 flows on real upstream providers (not just Cognito), and that has a decorator for the FastAPI app you probably already have, this gateway is for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concretely, this gateway tends to be the right fit in three situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal agents that have to touch several in-house APIs.&lt;/strong&gt; Customer support, finance ops, IT helpdesk. The agent needs your product API plus your issue tracker plus maybe your CRM, each with its own auth. One process, several mounts, no inter-service leakage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2B APIs where actions need to happen as the actual end user.&lt;/strong&gt; Signing a contract, moving money, closing a ticket on someone's behalf. Anything where the audit log on the upstream side has to show the human, not a shared service account. The token relay handles the per-user identity end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo or PoC stage with a moving spec.&lt;/strong&gt; The OpenAPI spec is still evolving, you do not yet know what the production MCP client will be, and you do not want to hand-maintain an MCP server in parallel. Point the gateway at the spec, get all three transports for free, regenerate every time the spec changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is And Is Not There Yet
&lt;/h2&gt;

&lt;p&gt;Shipping today: OAuth2 &lt;code&gt;authorization_code&lt;/code&gt; with token relay, OAuth2 &lt;code&gt;client_credentials&lt;/code&gt; with concurrency primitives, multi-server in one process (each with independent auth), FastAPI-native &lt;code&gt;@mcp_tool&lt;/code&gt;, operation filtering (&lt;code&gt;allow&lt;/code&gt; / &lt;code&gt;deny&lt;/code&gt; / &lt;code&gt;marked_only&lt;/code&gt;), Redis token store, all three MCP transports.&lt;/p&gt;

&lt;p&gt;On the roadmap, in case any of these would unblock you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flexible OAuth2 scope mapping.&lt;/strong&gt; Today, upstream scopes pass through to MCP one-to-one. Some setups need to expose only a subset, alias names, or group several upstream scopes under one MCP-side scope. Configurable in YAML, per server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merging multiple OpenAPI specs into one MCP server.&lt;/strong&gt; Today one spec produces one mounted server. Some upstream APIs split a single product's docs across several spec files (by tag, by version, by section), and forcing the agent to pick across N mount paths for what is logically one API is awkward. Want a &lt;code&gt;specs:&lt;/code&gt; list that the gateway combines into a single MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP primitives beyond tools, with per-operation YAML overrides.&lt;/strong&gt; Today every operation in your spec becomes an MCP tool. MCP also has resources, prompts, and sampling, and not every operation is best modelled as a tool (&lt;code&gt;GET /users/{id}&lt;/code&gt; is more naturally a resource than a tool). Want a YAML override per operation, which lets you choose the MCP primitive, rename it, hide it, or reshape its inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic tool exposure (skill-style discovery).&lt;/strong&gt; For large APIs with hundreds of operations, registering every operation as its own tool blows the agent's context window before the agent has done anything. Want an opt-in mode where the gateway exposes only three meta-tools (&lt;code&gt;list&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;call&lt;/code&gt;), and the agent fetches operation details on demand. Same pattern as agent skills.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these are blocking you, please open an issue with the shape of your problem. Concrete use cases drive the design more than my guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I run several mounted servers over stdio?
&lt;/h3&gt;

&lt;p&gt;No. stdio serves a single server. The multi-spec setup, where each API sits at its own mount path, needs streamable HTTP or SSE.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it pick up spec changes without a restart?
&lt;/h3&gt;

&lt;p&gt;No. The spec is read at startup and the tool list stays fixed while the process runs. Restart to regenerate from the changed spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This is a personal side project, shaped by the kind of agent flows I think about from my dayjob (a B2B e-signature API) but independent of it. If any of the situations above sound like yours, give it a run and open an issue with what is missing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;github.com/mroops0111/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/openapi-mcp-gateway/" rel="noopener noreferrer"&gt;pypi.org/project/openapi-mcp-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
