<?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>openapi-mcp-gateway: Resources, Dynamic Exposure, and Spec-Compliant Auth</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;The first post on &lt;a href="https://github.com/mroops0111/openapi-mcp-gateway" rel="noopener noreferrer"&gt;openapi-mcp-gateway&lt;/a&gt; closed with a roadmap. The resource primitive and dynamic exposure both shipped, plus a few more worth writing down. All in v0.5.0:&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;
  
  
  Try It
&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;expose&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;expose&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.expose.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. Both shipped in v0.5.0.&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.expose.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;expose&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;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Still a side project. The recurring situations driving this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Huge specs eating context.&lt;/li&gt;
&lt;li&gt;GETs that want to be resources.&lt;/li&gt;
&lt;li&gt;B2B APIs that want real per-user auth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers turned out worth writing down. If any of this was useful, a star on the repo would mean a lot. It is the clearest signal back that this is solving real problems for other people, and it is what keeps the next release coming.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &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;PyPI: &lt;a href="https://pypi.org/project/openapi-mcp-gateway/" rel="noopener noreferrer"&gt;pypi.org/project/openapi-mcp-gateway&lt;/a&gt; (v0.5.0)&lt;/li&gt;
&lt;li&gt;First Post: &lt;a href="https://dev.to/mroops/i-built-openapi-mcp-gateway-multi-spec-openapi-to-mcp-with-real-oauth2-in-python-46d2"&gt;I Built 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>OpenAPI-to-MCP, Done Right: Multi-Spec APIs and Real OAuth2</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;It is a Python service that turns OpenAPI specs into MCP servers, mounts as many of them as you want in one process, and does real per-user OAuth2 with token relay. You point it at a YAML and it runs.&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;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="792" height="420"&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;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;This is a personal open-source side project, not anything I deploy at work. The constraints came from the kinds of agent flows I think about because of my dayjob (a B2B e-signature API), but the gateway itself is independent of that. 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/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>
