<?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: SmartGate</title>
    <description>The latest articles on DEV Community by SmartGate (smartgate).</description>
    <link>https://dev.to/smartgate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14908%2Ff15eb2a1-d092-46c7-95dc-8939707633b3.png</url>
      <title>DEV Community: SmartGate</title>
      <link>https://dev.to/smartgate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smartgate"/>
    <language>en</language>
    <item>
      <title>What 42,270 measured monthly searches for MCP actually contain</title>
      <dc:creator>lizer yang</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:17:51 +0000</pubDate>
      <link>https://dev.to/smartgate/what-42270-measured-monthly-searches-for-mcp-actually-contain-3hmk</link>
      <guid>https://dev.to/smartgate/what-42270-measured-monthly-searches-for-mcp-actually-contain-3hmk</guid>
      <description>&lt;p&gt;We build an MCP gateway. That means we have to answer "where is the demand for MCP tooling" with&lt;br&gt;
numbers instead of instinct — and when we finally measured it, the shape of the answer was not what&lt;br&gt;
the topic's noise suggests. This is that measurement, with the method attached so anyone can repeat&lt;br&gt;
it and disagree with our reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we measured instead of guessing
&lt;/h2&gt;

&lt;p&gt;The MCP ecosystem produces a lot of &lt;em&gt;content&lt;/em&gt; and very little &lt;em&gt;demand data&lt;/em&gt;. Every week brings another&lt;br&gt;
"awesome MCP servers" list, another protocol explainer, another launch post. What none of them carry is&lt;br&gt;
the number that decides what to write next: how many people search for a given phrase in a month, and&lt;br&gt;
how hard the results page for it is to enter.&lt;/p&gt;

&lt;p&gt;We had a second reason to care. Our own pages were indexed, our sitemap was clean, and our Search&lt;br&gt;
Console still showed almost no non-branded impressions. When that happens, the useful question is not&lt;br&gt;
"is our prose good" but "does anyone search for this at all, and with what words". A demand&lt;br&gt;
measurement answers both at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method, so the numbers can be checked
&lt;/h2&gt;

&lt;p&gt;The pool comes from paid keyword data, not estimate tables scraped from a free tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;73 phrases were screened: seed terms around the protocol itself, the client/server vocabulary, the
transports, the comparison phrasings ("mcp vs api"), and the adjacent infrastructure terms our
product lives among (agent memory, agentic RAG, streamable HTTP).&lt;/li&gt;
&lt;li&gt;Volumes and difficulty came from measured requests against the keyword APIs we already pay for,
with the location pinned to the United States (location code 2840) so the numbers do not mix
markets.&lt;/li&gt;
&lt;li&gt;48 of the 73 cleared 50 searches/month. Those 48 were then clustered by &lt;strong&gt;search-result overlap&lt;/strong&gt;:
fetch the top 10 results for each phrase, and group phrases whose result sets substantially coincide.
That is the only honest way to decide whether two phrases belong on one page or two.&lt;/li&gt;
&lt;li&gt;Snapshot date: 21 September 2026. Every number below is from that single day, which matters — see
the limits section at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding 1: the pool is small and top-heavy
&lt;/h2&gt;

&lt;p&gt;The 73 phrases sum to &lt;strong&gt;42,270 searches per month&lt;/strong&gt;. One phrasing carries more than a quarter of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phrase&lt;/th&gt;
&lt;th&gt;Monthly searches&lt;/th&gt;
&lt;th&gt;Share of pool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;model context protocol&lt;/td&gt;
&lt;td&gt;12,100&lt;/td&gt;
&lt;td&gt;28.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp protocol&lt;/td&gt;
&lt;td&gt;4,400&lt;/td&gt;
&lt;td&gt;10.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a2a protocol&lt;/td&gt;
&lt;td&gt;3,600&lt;/td&gt;
&lt;td&gt;8.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp tools&lt;/td&gt;
&lt;td&gt;2,400&lt;/td&gt;
&lt;td&gt;5.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agentic rag&lt;/td&gt;
&lt;td&gt;1,900&lt;/td&gt;
&lt;td&gt;4.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp vs api&lt;/td&gt;
&lt;td&gt;1,900&lt;/td&gt;
&lt;td&gt;4.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everything else (67 phrases)&lt;/td&gt;
&lt;td&gt;15,970&lt;/td&gt;
&lt;td&gt;37.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two readings follow, and they point in opposite directions. The optimistic one: there is a real&lt;br&gt;
audience — 42,000 searches a month is not a hobbyist niche. The pessimistic one: &lt;strong&gt;the top three&lt;br&gt;
phrases are 47.6% of it&lt;/strong&gt;, and those three are the &lt;em&gt;name of the thing&lt;/em&gt;. People searching "model&lt;br&gt;
context protocol" and "mcp protocol" are not looking for a vendor; they are looking for the definition,&lt;br&gt;
which is exactly the query an encyclopaedia and a documentation site answer for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: difficulty runs opposite to volume
&lt;/h2&gt;

&lt;p&gt;Difficulty here is a 0–100 estimate of how hard the first page is to enter. Sort the pool by volume&lt;br&gt;
and a pattern appears — the big phrases are defended, and the &lt;em&gt;usable&lt;/em&gt; ones sit in the middle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phrase&lt;/th&gt;
&lt;th&gt;Monthly searches&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;anthropic mcp&lt;/td&gt;
&lt;td&gt;1,600&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp protocol&lt;/td&gt;
&lt;td&gt;4,400&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model context protocol&lt;/td&gt;
&lt;td&gt;12,100&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp tools&lt;/td&gt;
&lt;td&gt;2,400&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp spec&lt;/td&gt;
&lt;td&gt;1,300&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp server list&lt;/td&gt;
&lt;td&gt;880&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp vs api&lt;/td&gt;
&lt;td&gt;1,900&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;streamable http&lt;/td&gt;
&lt;td&gt;880&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two phrases with the lowest difficulty in the whole table are also two of the most specific:&lt;br&gt;
"mcp vs api" compares two architectures, and "streamable http" names one transport that MCP&lt;br&gt;
standardised. Both are the kind of query a reader has &lt;em&gt;after&lt;/em&gt; learning what MCP is, and both are&lt;br&gt;
answerable by anyone who has actually shipped the thing rather than read the specification.&lt;/p&gt;

&lt;p&gt;That asymmetry is the practical finding: &lt;strong&gt;the demand that a new entrant can realistically win is&lt;br&gt;
not the demand that looks biggest.&lt;/strong&gt; Chasing the 12,100 phrase means competing with the protocol's&lt;br&gt;
own documentation; answering the 12-difficulty phrase means writing something only an implementer&lt;br&gt;
can write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: the demand is fragmented, not one topic
&lt;/h2&gt;

&lt;p&gt;This was the surprise. We expected the 48 phrases to collapse into a handful of topical clusters, the&lt;br&gt;
way "cheap flights" and "low cost flights" belong together. Instead, clustering the live result pages&lt;br&gt;
turned 48 phrases into &lt;strong&gt;43 clusters&lt;/strong&gt; — with only 4 merges and 11 adjacent pairs across the whole set.&lt;/p&gt;

&lt;p&gt;Translation: for MCP, &lt;em&gt;almost every phrase owns its own result page&lt;/em&gt;. "mcp spec" and "mcp&lt;br&gt;
specification" are not the same query if the results pages disagree; "mcp tools" and "mcp servers" sit&lt;br&gt;
next to each other without merging. The topic looks like one huge subject from the outside and&lt;br&gt;
behaves like a mosaic of small ones from the inside.&lt;/p&gt;

&lt;p&gt;This has a direct consequence for anyone planning content here, and it is the opposite of the usual&lt;br&gt;
advice to build one comprehensive pillar page. A single page cannot be the canonical answer to 43&lt;br&gt;
different result pages. What works is a &lt;strong&gt;small centre per phrase family&lt;/strong&gt; — the protocol overview, the&lt;br&gt;
transport comparison, the client-configuration walkthrough — each one narrow enough to actually match&lt;br&gt;
its result page, and each one linking to its neighbours so that a reader who lands on any of them can&lt;br&gt;
reach the rest. Our own overview of the protocol's actors, lifecycle and transports is written to that&lt;br&gt;
shape, rather than to a keyword count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: publishing is not the same as being found
&lt;/h2&gt;

&lt;p&gt;The last piece of the measurement is not about search demand at all; it is about what our own site&lt;br&gt;
saw while we were publishing into this pool. In the four weeks ending 22 September 2026, our Search&lt;br&gt;
Console recorded &lt;strong&gt;54 impressions and 7 clicks&lt;/strong&gt; — and every one of them was a branded query. Not one&lt;br&gt;
of the non-branded phrases in the table above produced an impression during that window.&lt;/p&gt;

&lt;p&gt;That is worth stating plainly, because it is the number that disciplines the rest: a pool of 42,270&lt;br&gt;
monthly searches, fifty-odd published articles, and a non-branded impression count of zero. Demand&lt;br&gt;
being real does not mean demand is reachable; the pages existed before the audience did, and Google has&lt;br&gt;
to decide to serve them before any of the arithmetic in this article matters.&lt;/p&gt;

&lt;p&gt;The same period's edge logs add a useful counterweight: of the page reads recorded at our CDN in one&lt;br&gt;
week, 282 carried AI-crawler user agents and 144 carried search-crawler user agents. Treat that as a&lt;br&gt;
signal rather than a measurement — a user agent is a claim, not an identity, and machine files like&lt;br&gt;
&lt;code&gt;robots.txt&lt;/code&gt; inflate both numbers unless they are filtered out. The direction is still informative:&lt;br&gt;
agents are reading this material earlier and more often than search engines are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changed in our own content pipeline
&lt;/h2&gt;

&lt;p&gt;Three things, all of them reversible if the next snapshot disagrees:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We stopped measuring success in pages and started measuring it in &lt;strong&gt;phrase families covered&lt;/strong&gt;, which
is the unit the clustering says actually exists.&lt;/li&gt;
&lt;li&gt;We publish a cluster as a batch — one centre page on the head phrasing, members on the long tails —
instead of one page at a time. A member page that links to siblings which do not exist yet is a
broken link, and a centre page with no members is a page nobody reaches from anywhere.&lt;/li&gt;
&lt;li&gt;We kept the measurement in the repository next to the content, so the next round can diff against
it instead of re-deriving it from memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this does not prove
&lt;/h2&gt;

&lt;p&gt;The honest list, because a demand table invites more confidence than it earns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is &lt;strong&gt;one market&lt;/strong&gt; (United States) on &lt;strong&gt;one day&lt;/strong&gt;. Volumes move; a phrase that was 1,900 in
September may be 600 by December, and the difficulty of a result page moves faster than its volume.&lt;/li&gt;
&lt;li&gt;The volumes are &lt;strong&gt;advertiser-side estimates&lt;/strong&gt;. A phrase with no advertiser interest reports nothing,
which is not the same as no demand — several protocol phrases returned no data at all and were
excluded rather than guessed.&lt;/li&gt;
&lt;li&gt;Two terms belong to a &lt;strong&gt;different protocol&lt;/strong&gt; (A2A) that shares the audience; we kept it in the pool
because the comparison queries are real, not because it is MCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;43 clusters from 48 phrases is a floor, not a truth.&lt;/strong&gt; Clustering depends on the result pages of a
given day; phrase pairs that did not overlap in September can merge in October.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our Search Console window is short and small.&lt;/strong&gt; 54 impressions is a sample that can only tell us
the order of magnitude, and it cannot distinguish "the pages are wrong" from "the pages are new".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The map, not the territory
&lt;/h2&gt;

&lt;p&gt;If the useful summary is one paragraph long, it is this: for MCP the measurable demand is real but&lt;br&gt;
concentrated in a handful of definitional queries, the entry price is lowest exactly where the&lt;br&gt;
audience is most specific, and the topic fragments into far more result pages than its vocabulary&lt;br&gt;
suggests. Plan accordingly — a few narrow pages that answer a phrase completely, linked into a&lt;br&gt;
neighbourhood, will outperform one broad page that answers nothing precisely.&lt;/p&gt;

&lt;p&gt;The pool, the clustering and the diffable snapshots stay where the content lives. If you want the&lt;br&gt;
starting point rather than the summary, the protocol's own mechanics are documented in our&lt;br&gt;
&lt;a href="https://smartgate.network/industry/model-context-protocol-explained" rel="noopener noreferrer"&gt;MCP overview&lt;/a&gt;, and the&lt;br&gt;
published-server side is collected in the&lt;br&gt;
&lt;a href="https://smartgate.network/industry/mcp-server-list" rel="noopener noreferrer"&gt;server list&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this measurement was assembled and written with AI assistance from our own paid keyword&lt;br&gt;
requests and Search Console exports; the numbers are quoted from those files rather than re-typed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Model Context Protocol (MCP) Message Format Explained</title>
      <dc:creator>lizer yang</dc:creator>
      <pubDate>Thu, 24 Sep 2026 11:01:51 +0000</pubDate>
      <link>https://dev.to/smartgate/model-context-protocol-mcp-message-format-explained-1hmo</link>
      <guid>https://dev.to/smartgate/model-context-protocol-mcp-message-format-explained-1hmo</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; The Model Context Protocol message format is JSON-RPC 2.0 — one request object&lt;br&gt;
with &lt;code&gt;jsonrpc&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;params&lt;/code&gt;, and an &lt;code&gt;id&lt;/code&gt;, one result or error object back, and notifications&lt;br&gt;
that carry no &lt;code&gt;id&lt;/code&gt; at all. Over HTTP the whole conversation is POSTed to a single endpoint:&lt;br&gt;
&lt;code&gt;initialize&lt;/code&gt;, then &lt;code&gt;tools/list&lt;/code&gt;, then &lt;code&gt;tools/call&lt;/code&gt;, with the session optional. The parts that break&lt;br&gt;
integrations are the small ones: an empty &lt;code&gt;params&lt;/code&gt; array, an &lt;code&gt;arguments&lt;/code&gt; field typed as a list, and a&lt;br&gt;
client that calls the tool name as the method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three shapes, one envelope.&lt;/strong&gt; Requests (&lt;code&gt;id&lt;/code&gt; + &lt;code&gt;method&lt;/code&gt; + &lt;code&gt;params&lt;/code&gt;), notifications (no &lt;code&gt;id&lt;/code&gt;), and
results (&lt;code&gt;id&lt;/code&gt; + &lt;code&gt;result&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;) — every MCP message is one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;initialize&lt;/code&gt; is a negotiation, not a formality.&lt;/strong&gt; The client proposes a protocol version, the
server answers with the version it will speak plus its capabilities and instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;tools/call&lt;/code&gt; are the two messages that matter for tool use.&lt;/strong&gt; One advertises
what exists (with annotations), the other names a tool and passes &lt;code&gt;arguments&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;params: []&lt;/code&gt; is legal JSON and invalid for most strict parsers&lt;/strong&gt; — Cursor sends exactly that for
&lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;notifications/initialized&lt;/code&gt;, which is why normalization sits in front of the
parser rather than inside each tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stateless HTTP server should not gate &lt;code&gt;tools/list&lt;/code&gt; behind a completed handshake&lt;/strong&gt;, because
clients connect, list, and disconnect in whatever order their transport allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;tools/list&lt;/code&gt; from a live server before writing a client.&lt;/strong&gt; Annotations in that response —
title and read-only hint — decide what your host will run without asking the user, so they are the
contract worth testing against first.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The short version for whoever signs the invoice
&lt;/h2&gt;

&lt;p&gt;"Model context protocol" carries roughly &lt;strong&gt;12,100 monthly US searches&lt;/strong&gt;, and the first page for&lt;br&gt;
&lt;code&gt;model context protocol message format&lt;/code&gt; is an AI Overview built from the specification itself&lt;br&gt;
(&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;MCP spec&lt;/a&gt;). That tells you two things&lt;br&gt;
about this topic: demand is real, and the audience is engineers who are comparing implementations,&lt;br&gt;
not shoppers. What they cannot get from the specification is what &lt;em&gt;holds up in production&lt;/em&gt; — which&lt;br&gt;
message shapes real hosts send, and where a gateway has to be lenient.&lt;/p&gt;

&lt;p&gt;SmartGate is an MCP-native algorithm gateway for token control, traffic shaping, and agent audit. It&lt;br&gt;
sits between a host (Cursor, Claude Desktop, Windsurf, OpenClaw, or your own client) and the open&lt;br&gt;
web, exposing seven tools: smart_fetch, smart_search, smart_context_gate, smart_dedup,&lt;br&gt;
smart_budget_guard, smart_memory, and smart_pipe. The message path described below is the layer that&lt;br&gt;
makes those seven tools work across hosts that disagree on details — and the same layer is where&lt;br&gt;
budget checks, rate limits, and audit rows are attached.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the protocol actually specifies
&lt;/h2&gt;

&lt;p&gt;MCP is JSON-RPC 2.0 over a transport. The specification covers two transports, stdio and Streamable&lt;br&gt;
HTTP, and explicitly allows a server to be stateless — session management is optional, and a server&lt;br&gt;
may answer each request independently&lt;br&gt;
(&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports" rel="noopener noreferrer"&gt;MCP transports&lt;/a&gt;). The&lt;br&gt;
server-side surface for tools is defined as two messages: &lt;code&gt;tools/list&lt;/code&gt; to advertise tools and&lt;br&gt;
&lt;code&gt;tools/call&lt;/code&gt; to invoke one&lt;br&gt;
(&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools" rel="noopener noreferrer"&gt;MCP tools&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A worked &lt;a href="https://smartgate.network/industry/model-context-protocol-example" rel="noopener noreferrer"&gt;MCP JSON-RPC round trip&lt;/a&gt; is easier to&lt;br&gt;
follow than the schema alone, because it shows the four fields and the two responses in&lt;br&gt;
the order a client actually sends them.&lt;/p&gt;

&lt;p&gt;The gap between the specification and a working integration is visible in the wild: there is a&lt;br&gt;
Stack Exchange question asking precisely what the MCP &lt;em&gt;message format&lt;/em&gt; is and how it differs from the&lt;br&gt;
communication architecture, with answers pointing at the same conclusion — the format is JSON-RPC,&lt;br&gt;
the architecture is transport plus session&lt;br&gt;
(&lt;a href="https://softwareengineering.stackexchange.com/questions/458556/clarification-on-model-context-protocol-message-format-vs-communication-archit" rel="noopener noreferrer"&gt;Stack Exchange&lt;/a&gt;).&lt;br&gt;
Everything below is that distinction, from the gateway's side of the wire.&lt;/p&gt;

&lt;p&gt;Which protocol versions a server will answer for, and which transport carries the messages, is the&lt;br&gt;
other half of that picture — &lt;a href="https://smartgate.network/industry/mcp-protocol-versions-and-transports" rel="noopener noreferrer"&gt;MCP Protocol Versions and Transports&lt;/a&gt;&lt;br&gt;
covers both.&lt;/p&gt;
&lt;h2&gt;
  
  
  mount_mcp_routes: one endpoint, no session state
&lt;/h2&gt;

&lt;p&gt;The whole HTTP surface is one mounted application on one path:&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="c1"&gt;# backend/smartgate/api/mcp.py — source lines 399–406 (mount_mcp_routes)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mount_mcp_routes&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;FastAPI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Expose POST /mcp (Streamable HTTP, stateless).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;apply_mcp_session_compat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;streamable_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamable_http_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;streamable_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lifespan_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_noop_starlette_lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streamable_app&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="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;streamable_app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP Streamable HTTP at POST /mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docstring is the specification in one line: &lt;strong&gt;POST /mcp, Streamable HTTP, stateless&lt;/strong&gt;. Session&lt;br&gt;
compatibility is applied &lt;em&gt;before&lt;/em&gt; mounting, because the patches have to be in place when the first&lt;br&gt;
request arrives rather than when the first session is created. The lifespan context is then replaced&lt;br&gt;
with a no-op so that mounting the MCP app inside a larger FastAPI application does not run a second&lt;br&gt;
startup lifecycle — a failure mode that looks like "the tools work locally but not in production".&lt;/p&gt;
&lt;h2&gt;
  
  
  normalize_jsonrpc_body: the leniency a real client needs
&lt;/h2&gt;

&lt;p&gt;Before any parsing happens, the body is normalized. This is the function that turns a&lt;br&gt;
non-conforming-but-legal message into something pydantic will accept:&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="c1"&gt;# backend/smartgate/api/mcp_sse_compat.py — source lines 67–99 (normalize_jsonrpc_body)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_jsonrpc_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Coerce non-object JSON-RPC params (e.g. []) to {} for pydantic validation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;

    &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_rewrite_direct_tool_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Cursor: tools/list, notifications/initialized with "params": []
&lt;/span&gt;        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_normalize_params_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Normalized JSON-RPC body: method=%s params_type=%s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three cases are handled. A message with &lt;strong&gt;no &lt;code&gt;params&lt;/code&gt;&lt;/strong&gt; gets &lt;code&gt;{}&lt;/code&gt;. A message whose &lt;code&gt;params&lt;/code&gt; is a&lt;br&gt;
&lt;strong&gt;list&lt;/strong&gt; — the &lt;code&gt;"params": []&lt;/code&gt; that Cursor sends for &lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;notifications/initialized&lt;/code&gt; —&lt;br&gt;
also gets &lt;code&gt;{}&lt;/code&gt;, with the comment naming the client. A message whose &lt;code&gt;params&lt;/code&gt; is an &lt;strong&gt;object&lt;/strong&gt; is&lt;br&gt;
passed to the nested-field fixer below. The rewrite is a no-op when nothing changed, and the log line&lt;br&gt;
records the method and the resulting &lt;code&gt;params&lt;/code&gt; type, which is the first thing worth grepping when a&lt;br&gt;
client's handshake fails.&lt;/p&gt;
&lt;h2&gt;
  
  
  NormalizeJsonRpcMiddleware: where in the stack it happens
&lt;/h2&gt;

&lt;p&gt;Normalization is applied as ASGI middleware, which is the only place in a Python server where you can&lt;br&gt;
rewrite a request body before the framework's own parser sees it:&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="c1"&gt;# backend/smartgate/api/mcp_sse_compat.py — source lines 102–138 (NormalizeJsonRpcMiddleware)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NormalizeJsonRpcMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;ASGI middleware: fix params: [] before MCP sse.handle_post_message parses body.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ASGIApp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&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__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;while&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;more_body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_jsonrpc_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&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;replay_receive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;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="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;sent&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sent&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.disconnect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;more_body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replay_receive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guards matter as much as the fix: only &lt;code&gt;POST&lt;/code&gt;, and only paths containing &lt;code&gt;messages&lt;/code&gt;. Everything&lt;br&gt;
else falls through untouched, so the gateway pays no cost on tool responses or on other routes. The&lt;br&gt;
middleware buffers the body until &lt;code&gt;more_body&lt;/code&gt; is false, normalizes the joined bytes, then hands the&lt;br&gt;
application a &lt;code&gt;receive&lt;/code&gt; callable that replays the rewritten body — the standard ASGI idiom for "the&lt;br&gt;
body is consumed, here is what the app should see instead".&lt;/p&gt;
&lt;h2&gt;
  
  
  _normalize_params_object: the nested quirk
&lt;/h2&gt;

&lt;p&gt;The object-level fix is deliberately tiny, because it fixes one observed shape rather than validating&lt;br&gt;
arbitrary input:&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="c1"&gt;# backend/smartgate/api/mcp_sse_compat.py — source lines 57–64 (_normalize_params_object)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_normalize_params_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fix nested params quirks from MCP hosts. Returns True if mutated.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&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;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP tool arguments belong in &lt;code&gt;params.arguments&lt;/code&gt; as an &lt;strong&gt;object&lt;/strong&gt;. Some hosts emit a list when the&lt;br&gt;
tool takes no parameters, which strict validation rejects. Coercing &lt;code&gt;[]&lt;/code&gt; to &lt;code&gt;{}&lt;/code&gt; keeps the call&lt;br&gt;
semantically identical — no arguments — while letting the same request pass validation on a server&lt;br&gt;
that would otherwise return an error the client cannot interpret. The &lt;code&gt;bool&lt;/code&gt; return value is what&lt;br&gt;
makes the normalizer idempotent: it only re-serializes the body when something actually changed.&lt;/p&gt;
&lt;h2&gt;
  
  
  _rewrite_direct_tool_method: when the method is the tool name
&lt;/h2&gt;

&lt;p&gt;The most pragmatic concession in the whole file is the direct-method rewrite, and it exists because&lt;br&gt;
users write what feels obvious:&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="c1"&gt;# backend/smartgate/api/mcp_sse_compat.py — source lines 27–54 (_rewrite_direct_tool_method)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_rewrite_direct_tool_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Rewrite {method: smart_fetch, params: {url: ...}} → standard tools/call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_SMART_TOOL_METHODS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&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="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rewrote legacy MCP tool method %s → tools/call (name=%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;original&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A standard call is &lt;code&gt;{method: "tools/call", params: {name: "smart_fetch", arguments: {...}}}&lt;/code&gt;. But a&lt;br&gt;
hand-written request often looks like &lt;code&gt;{method: "smart_fetch", params: {url: "…"}}&lt;/code&gt; — the tool name&lt;br&gt;
promoted to the method, the arguments flattened into &lt;code&gt;params&lt;/code&gt;. Rather than rejecting that with a&lt;br&gt;
protocol error, the gateway rewrites it into a conforming &lt;code&gt;tools/call&lt;/code&gt;, extracting inline keys as&lt;br&gt;
arguments and stripping &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;arguments&lt;/code&gt;, and &lt;code&gt;_meta&lt;/code&gt; so nothing is passed through twice. The&lt;br&gt;
&lt;code&gt;logger.info&lt;/code&gt; line records both the original method and the resolved tool name, which turns "my&lt;br&gt;
client says unknown method" into a one-line log search.&lt;/p&gt;
&lt;h2&gt;
  
  
  replay_receive: one body, exactly once
&lt;/h2&gt;

&lt;p&gt;Replaying the consumed body is small enough to hide, and wrong implementations fail in ways that look&lt;br&gt;
like networking problems:&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="c1"&gt;# backend/smartgate/api/mcp_sse_compat.py — source lines 131–136 (replay_receive)
&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;replay_receive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;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="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;sent&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sent&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.disconnect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;more_body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closure returns the rewritten body on the first call and &lt;code&gt;http.disconnect&lt;/code&gt; on every call after&lt;br&gt;
that. That is the ASGI contract for a request whose bytes have already been read: the application may&lt;br&gt;
ask again, but there is no more data, so it must be told the client went away rather than being&lt;br&gt;
handed an empty body. Getting this wrong produces a server that appears to hang on the second read —&lt;br&gt;
and a client-side timeout that points at the wrong layer.&lt;/p&gt;
&lt;h2&gt;
  
  
  _stateless_server_run: why tools/list must not wait for init
&lt;/h2&gt;

&lt;p&gt;Session-state handling is where stateless servers diverge from the textbook flow, and the gateway&lt;br&gt;
patches the run loop rather than the tools:&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="c1"&gt;# backend/smartgate/api/mcp_session_compat.py — source lines 70–86 (_stateless_server_run)
&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;_stateless_server_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lowlevel_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;read_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;write_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;initialization_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;raise_exceptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stateless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;SSE sessions start Initialized so tools/list is not rejected during init races.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_stateless_server_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_orig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[attr-defined]
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;read_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;write_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;initialization_options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;raise_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raise_exceptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stateless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stateless&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docstring states the production reality: SSE sessions start already &lt;code&gt;Initialized&lt;/code&gt;, so a&lt;br&gt;
&lt;code&gt;tools/list&lt;/code&gt; that arrives during an initialization race is answered instead of rejected. This is a&lt;br&gt;
deliberate trade — a stricter server would require the handshake to complete first and would be&lt;br&gt;
right by the book, while shedding exactly the clients that connect, list, and disconnect quickly.&lt;/p&gt;

&lt;p&gt;The lifecycle a fully negotiating client walks through instead, with the actors named, is the&lt;br&gt;
subject of &lt;a href="https://smartgate.network/industry/model-context-protocol-explained" rel="noopener noreferrer"&gt;Model Context Protocol Explained&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  apply_mcp_session_compat: idempotent monkey-patching
&lt;/h2&gt;

&lt;p&gt;The patches are applied in one guarded function, because applying them twice breaks the server:&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="c1"&gt;# backend/smartgate/api/mcp_session_compat.py — source lines 89–103 (apply_mcp_session_compat)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_mcp_session_compat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Idempotent patches applied before mounting MCP SSE.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_PATCHED&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_PATCHED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;ServerSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_received_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_compat_received_request&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[method-assign]
&lt;/span&gt;    &lt;span class="n"&gt;ServerSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_received_notification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_compat_received_notification&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[method-assign]
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_stateless_server_run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_orig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;_stateless_server_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_orig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lowlevel_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[attr-defined]
&lt;/span&gt;        &lt;span class="n"&gt;lowlevel_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_stateless_server_run&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[method-assign]
&lt;/span&gt;
    &lt;span class="n"&gt;_PATCHED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP session compat enabled (stateless SSE + relaxed init gate)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are being replaced: the session class's request and notification handlers (both point at&lt;br&gt;
the compatibility versions), and the low-level server's &lt;code&gt;run&lt;/code&gt; method — wrapped, with the original&lt;br&gt;
kept on &lt;code&gt;_orig&lt;/code&gt; so the wrapper can delegate instead of reimplementing. The &lt;code&gt;_PATCHED&lt;/code&gt; guard is what&lt;br&gt;
makes this safe to call from anywhere: &lt;code&gt;mount_mcp_routes&lt;/code&gt; calls it unconditionally, and a second call&lt;br&gt;
is a no-op rather than a double patch. Note the honest naming — this is a compatibility layer for&lt;br&gt;
real clients, not a claim that the specification is wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  _compat_received_request: the handshake answer
&lt;/h2&gt;

&lt;p&gt;The relaxed handler is where &lt;code&gt;initialize&lt;/code&gt; is answered, and where the version negotiation is visible:&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="c1"&gt;# backend/smartgate/api/mcp_session_compat.py — source lines 24–57 (_compat_received_request)
&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;_compat_received_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServerSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;responder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RequestResponder&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerResult&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Allow tools/* during Initializing; only block when session never started init.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;responder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InitializeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;requested_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;protocolVersion&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_initialization_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InitializationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Initializing&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;responder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;responder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServerResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InitializeResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;protocolVersion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;requested_version&lt;/span&gt;
                            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requested_version&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_PROTOCOL_VERSIONS&lt;/span&gt;
                            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LATEST_PROTOCOL_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_init_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;serverInfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_init_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_init_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;websiteUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_init_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;website_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;icons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_init_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;icons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_init_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_initialization_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InitializationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Initialized&lt;/span&gt;
        &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PingRequest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_initialization_state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;InitializationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotInitialized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received request before initialization was complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three behaviours are worth reading closely. &lt;code&gt;initialize&lt;/code&gt; records the requested protocol version,&lt;br&gt;
sets the session to &lt;code&gt;Initializing&lt;/code&gt;, and answers with &lt;strong&gt;the requested version when it is supported,&lt;br&gt;
otherwise the latest&lt;/strong&gt; — the standard MCP negotiation, and the reason a client speaking a newer&lt;br&gt;
minor version still gets a usable session. &lt;code&gt;ping&lt;/code&gt; is accepted silently. Any other request arriving&lt;br&gt;
before initialization raises, which is the one gate the compatibility layer deliberately keeps: the&lt;br&gt;
point is to allow &lt;code&gt;tools/*&lt;/code&gt; during the handshake race, not to remove initialization as a concept.&lt;/p&gt;
&lt;h2&gt;
  
  
  register_mcp_tools: seven tools, one registration path
&lt;/h2&gt;

&lt;p&gt;Tool advertisement is a plain function call per tool, which is what keeps &lt;code&gt;tools/list&lt;/code&gt; and the&lt;br&gt;
documentation from drifting apart:&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="c1"&gt;# backend/smartgate/api/mcp.py — source lines 106–126 (register_mcp_tools)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_mcp_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Register all 7 smart_* tools on a FastMCP instance.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nd"&gt;@server.tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smart_fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOL_DESCRIPTIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smart_fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tool_annotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smart_fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;smart_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Full HTTP or HTTPS URL to fetch.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP timeout in seconds.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_app_state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_tool_ctx&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_run_with_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch&lt;/span&gt;&lt;span class="sh"&gt;"&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&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;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool is declared once, with its description read from a shared table and its annotations&lt;br&gt;
derived from the tool name — so the model-facing metadata and the human-facing docs have a single&lt;br&gt;
source. The two tools in this window show the pattern end to end: &lt;code&gt;smart_fetch&lt;/code&gt; takes a URL and a&lt;br&gt;
timeout, &lt;code&gt;smart_search&lt;/code&gt; takes a query and a result cap, and both end in the same audited call path.&lt;br&gt;
That shared ending is what makes &lt;code&gt;tools/call&lt;/code&gt; predictable regardless of which of the seven tools is&lt;br&gt;
invoked.&lt;/p&gt;
&lt;h2&gt;
  
  
  _run_with_audit: one exit path for every call
&lt;/h2&gt;

&lt;p&gt;Every tool call — seven tools, any arguments — returns through the same function:&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="c1"&gt;# backend/smartgate/api/mcp.py — source lines 90–103 (_run_with_audit)
&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;_run_with_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tool&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;ToolContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;process_coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;_ensure_mcp_audit_context&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;_registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_app_state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;process_coro&lt;/span&gt;
    &lt;span class="k"&gt;await&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audit_hook&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;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It re-binds the audit context for in-stream calls, awaits the tool's coroutine, writes an audit row&lt;br&gt;
through the application's audit hook, and only then decides how to answer. A failed tool raises a&lt;br&gt;
&lt;code&gt;ToolError&lt;/code&gt; carrying the module's own message, which reaches the client as a JSON-RPC error instead&lt;br&gt;
of a success payload with an error field inside it; a successful call returns the data as compact&lt;br&gt;
JSON. Because every tool goes through this path, an audit log that is missing a call means the call&lt;br&gt;
never reached the gateway, not that a tool forgot to log.&lt;/p&gt;
&lt;h2&gt;
  
  
  tool_annotations: what &lt;code&gt;tools/list&lt;/code&gt; says about risk
&lt;/h2&gt;

&lt;p&gt;The last piece of the message format that clients actually consume is the annotation block, and it is&lt;br&gt;
derived, not hand-written:&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="c1"&gt;# backend/smartgate/api/mcp_tool_docs.py — source lines 63–67 (tool_annotations)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tool_annotations&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOL_TITLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;readOnlyHint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;READ_ONLY_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;title&lt;/code&gt; gives the tool a human-readable name in the host's UI, and &lt;code&gt;readOnlyHint&lt;/code&gt; tells the client&lt;br&gt;
whether invoking it can change state. Read-only hints matter more than they look: hosts use them to&lt;br&gt;
decide what may run without confirmation, so a wrong hint changes the approval experience for every&lt;br&gt;
user of that client. Deriving the hint from a &lt;code&gt;READ_ONLY_TOOLS&lt;/code&gt; set keeps it consistent with what the&lt;br&gt;
tool actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this differs from a local MCP server
&lt;/h2&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;Message path&lt;/th&gt;
&lt;th&gt;Session model&lt;/th&gt;
&lt;th&gt;What you get beyond the tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartGate (hosted, stateless)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON-RPC over Streamable HTTP at one POST endpoint (&lt;code&gt;/api/mcp&lt;/code&gt;), normalized for host quirks&lt;/td&gt;
&lt;td&gt;Stateless; sessions optional, no session id required&lt;/td&gt;
&lt;td&gt;Free: 2M tokens/mo, all 7 tools, 120 req/min/key. Pro from &lt;strong&gt;$18/mo&lt;/strong&gt;, share only after &lt;strong&gt;$15&lt;/strong&gt; saved (&lt;a href="https://smartgate.network/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local stdio MCP server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSON-RPC over stdin/stdout, no HTTP layer&lt;/td&gt;
&lt;td&gt;Process lifetime is the session&lt;/td&gt;
&lt;td&gt;Whatever the server implements; nothing at the gateway layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hand-rolled JSON-RPC shim&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your own parsing of the same envelopes&lt;/td&gt;
&lt;td&gt;Yours to invent&lt;/td&gt;
&lt;td&gt;Bugs proportional to how much of the protocol you re-implement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM proxy/router&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Different protocol entirely (model calls)&lt;/td&gt;
&lt;td&gt;Provider sessions&lt;/td&gt;
&lt;td&gt;Model routing, not tool governance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest reading: if you are writing a client or a single-purpose server, the specification is&lt;br&gt;
enough — JSON-RPC is small, and the two tool messages are small. The compatibility functions above&lt;br&gt;
exist because you are talking to &lt;em&gt;many&lt;/em&gt; hosts you do not control, and because "accept the message,&lt;br&gt;
then record it" is a gateway's job rather than a library's.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look at the messages you are already sending.&lt;/strong&gt; Point a client at
&lt;code&gt;https://smartgate.network/api/mcp&lt;/code&gt; (POST) with &lt;code&gt;Authorization: Bearer &amp;lt;key&amp;gt;&lt;/code&gt;; the &lt;a href="https://smartgate.network/docs/connect" rel="noopener noreferrer"&gt;Connect page&lt;/a&gt;
generates the exact block for Cursor, Claude Desktop, Windsurf, OpenClaw, or a generic client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call &lt;code&gt;tools/list&lt;/code&gt; once and read the annotations.&lt;/strong&gt; Seven tools appear, each with a title and a
read-only hint — that response is the contract your client can rely on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then call &lt;code&gt;tools/call&lt;/code&gt; on &lt;code&gt;smart_fetch&lt;/code&gt;&lt;/strong&gt; with a URL, and check that the JSON result comes back
as text content rather than an error object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the audit row appear&lt;/strong&gt; in Activity Logs; if it is missing, the message never reached the
gateway.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start on Free — 2M tokens/month, all seven tools, &lt;strong&gt;120 MCP requests/min per key&lt;/strong&gt;: &lt;strong&gt;&lt;a href="https://smartgate.network/login?from=/dashboard" rel="noopener noreferrer"&gt;start free&lt;/a&gt;&lt;/strong&gt;,&lt;br&gt;
then compare per-key limits and log retention on the &lt;strong&gt;&lt;a href="https://smartgate.network/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Is the MCP message format just JSON-RPC?&lt;/strong&gt;&lt;br&gt;
Effectively yes: JSON-RPC 2.0 envelopes carrying MCP-defined methods. The protocol adds semantics on&lt;br&gt;
top — &lt;code&gt;initialize&lt;/code&gt; negotiation, capabilities, tool annotations — but the wire format is JSON-RPC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which methods does a tool-using client actually need?&lt;/strong&gt;&lt;br&gt;
Four in the common path: initialize, notifications/initialized, tools/list, and tools/call. Everything&lt;br&gt;
else (resources, prompts, sampling) is optional surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why would a server accept a tool name as the method?&lt;/strong&gt;&lt;br&gt;
Because hand-written clients do it, and a rewrite is friendlier than an error. The gateway converts&lt;br&gt;
&lt;code&gt;{method: "smart_fetch", params: {url: …}}&lt;/code&gt; into a conforming &lt;code&gt;tools/call&lt;/code&gt; and logs both names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;"params": []&lt;/code&gt; valid MCP?&lt;/strong&gt;&lt;br&gt;
It is valid JSON-RPC (params may be a structured value), and at least one widely used client sends it&lt;br&gt;
for parameterless methods. Strict servers reject it; a normalizing middleware coerces it to &lt;code&gt;{}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a session id?&lt;/strong&gt;&lt;br&gt;
Not for a stateless server. Sessions are optional in the Streamable HTTP transport, and the gateway&lt;br&gt;
runs without them, which is also why a config change does not require reconnecting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a call arrives before initialization completes?&lt;/strong&gt;&lt;br&gt;
Read-only tool traffic is allowed during the handshake race, and anything else raises — a deliberate&lt;br&gt;
middle ground between the strict specification and clients that connect, list, and disconnect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do tool results come back?&lt;/strong&gt;&lt;br&gt;
As JSON-RPC results whose content is text: tool output is serialized to compact JSON, and a failed&lt;br&gt;
tool becomes a JSON-RPC error rather than a success object with an error field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and what this does not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The compatibility layer is a compatibility layer.&lt;/strong&gt; It exists because hosts disagree; a client
that sends malformed JSON still fails, and normalization does not repair an unparseable body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statelessness costs per-connection state.&lt;/strong&gt; A server that accepts &lt;code&gt;tools/list&lt;/code&gt; during
initialization gives up the guarantee that a session was properly established first; that is the
trade, not an oversight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotations are hints, not enforcement.&lt;/strong&gt; &lt;code&gt;readOnlyHint&lt;/code&gt; influences client UI; the gateway's
actual controls are the rate limit, the budget cap, and the key's scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool-count and protocol-version details move.&lt;/strong&gt; The messages here are stable, but supported
versions and tool metadata are maintained in code and can change between releases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Model Context Protocol — specification (2026-07-28): &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2026-07-28&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model Context Protocol — transports, including stateless Streamable HTTP: &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/transports" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2026-07-28/basic/transports&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model Context Protocol — server tools (&lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;): &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-06-18/server/tools&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic — introducing the Model Context Protocol: &lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;https://www.anthropic.com/news/model-context-protocol&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Stack Exchange — clarification on the MCP message format vs. communication architecture: &lt;a href="https://softwareengineering.stackexchange.com/questions/458556/clarification-on-model-context-protocol-message-format-vs-communication-archit" rel="noopener noreferrer"&gt;https://softwareengineering.stackexchange.com/questions/458556/clarification-on-model-context-protocol-message-format-vs-communication-archit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SmartGate — product site and pricing: &lt;a href="https://smartgate.network" rel="noopener noreferrer"&gt;https://smartgate.network&lt;/a&gt; · &lt;a href="https://smartgate.network/pricing" rel="noopener noreferrer"&gt;https://smartgate.network/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Method note
&lt;/h2&gt;

&lt;p&gt;The code in this article is not transcribed. Each block was cut directly out of the slice body&lt;br&gt;
returned by the SmartGate slice API and then re-asserted byte-for-byte as a substring of that body&lt;br&gt;
before publication; the first line inside every fence records the file and the exact source lines.&lt;br&gt;
Symbols were pinned with whole-name containment (rule A level 2) and confirmed by the service's&lt;br&gt;
slot-proof endpoint. Sections describe the gateway's message path only; no third-party client&lt;br&gt;
implementation is quoted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slice provenance
&lt;/h3&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;SERP keyword&lt;/th&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Source lines&lt;/th&gt;
&lt;th&gt;How it was pinned&lt;/th&gt;
&lt;th&gt;sha256(12)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;mount_mcp_routes how the MCP Streamable HTTP POST route is mounted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mount_mcp_routes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;399–406&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e66f6b69a172&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;normalize_jsonrpc_body normalize the JSON-RPC body of an MCP message&lt;/td&gt;
&lt;td&gt;&lt;code&gt;normalize_jsonrpc_body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_sse_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;67–99&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;d8359617452f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NormalizeJsonRpcMiddleware ASGI middleware for MCP JSON-RPC parsing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NormalizeJsonRpcMiddleware&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_sse_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;102–138&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;472493b3162e&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;_normalize_params_object nested params object quirk in MCP tool calls&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_normalize_params_object&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_sse_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;57–64&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;444ddb5db5f2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;_rewrite_direct_tool_method rewrite direct tool method MCP messages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_rewrite_direct_tool_method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_sse_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27–54&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2deb57048f8c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;replay_receive replay the SSE receive stream for MCP messages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;replay_receive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_sse_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;131–136&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;74963ba50239&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;_stateless_server_run stateless MCP session run without session state&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_stateless_server_run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_session_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;70–86&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22066742f8ed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;apply_mcp_session_compat MCP session compatibility patch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apply_mcp_session_compat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_session_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;89–103&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2aec583c6238&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;_compat_received_request accept tools list during MCP initialize&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_compat_received_request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_session_compat.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24–57&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc03b6abdf85&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;register_mcp_tools register all seven smart tools on the MCP server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;register_mcp_tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;106–126&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9d4a1623b28c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;_run_with_audit run each MCP tool call with audit context&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_run_with_audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90–103&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;454c08008ffa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;tool_annotations MCP tool annotations in the tools list response&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tool_annotations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend/smartgate/api/mcp_tool_docs.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;63–67&lt;/td&gt;
&lt;td&gt;rule A L2 → slot-proof&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a822944005fe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before&lt;br&gt;
publication. 12 of 12 sections pinned, 0 abstentions, 0 misses.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This guide is republished from &lt;a href="https://smartgate.network/industry/mcp-protocol-message-format-explained" rel="noopener noreferrer"&gt;smartgate.network&lt;/a&gt;; it was drafted with AI assistance and reviewed by our team.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>programming</category>
      <category>api</category>
    </item>
  </channel>
</rss>
