Short answer: MCP speaks JSON-RPC 2.0 over one of two transports: Streamable HTTP for remote servers and stdio for local processes, with the older two-endpoint HTTP+SSE transport kept only for backwards compatibility. Version and capability negotiation happens once, in the initialize exchange — the client proposes a dated protocol revision, the server answers with the revision it will actually speak plus a capability map — and after that a session is optional, so a stateless server is free to answer every POST independently.
Key takeaways
- Two transports are current. Streamable HTTP is one endpoint that accepts POST and GET; stdio is newline-delimited JSON-RPC over a child process's stdin and stdout. The 2024-era HTTP+SSE pair is legacy, and clients still send it.
initializeis the only negotiation in the protocol. If the server supports the revision the client asked for it must answer with that same revision; otherwise it answers with one it does support, and a client that cannot speak the answer is expected to disconnect.- Sessions are optional over HTTP. A server may hand out an
Mcp-Session-Idon the initialize response, and the client must then echo it on every later request — but a server that never issues one is still conformant, and it survives a restart without a reconnect dance. - Clients must carry the agreed revision forward in an
MCP-Protocol-Versionheader; a server that receives an unsupported value answers 400 instead of guessing. - The gateway work is small and specific: mount one endpoint, infer the transport for the audit row, and normalize the request body before a strict parser ever sees it.
- Read
tools/listfrom a live server before writing an integration. The transport decides whether your client can even reach that response; verifying both together is one test run, not two.
Who this is for: the transport decision before the wire format
If you are choosing how a host reaches a server, this page answers that, not how a message is shaped.
The decisions are narrow: remote or local, session or stateless, today's transport or the legacy pair
an older client still speaks. Get them right and the tool layer is invisible; get them wrong and you
get timeouts that look like model problems, 404s that look like routing problems, and handshake races
that look like bugs in your tools. The measurements are worth knowing because they tell you how the
audience phrases the question: model context protocol carries about 12,100 monthly US searches,
a2a protocol about 3,600, llm gateway about 1,600, streamable http about 880, and the
narrower mcp sse, mcp transport and mcp stdio between 140 and 260 each. The transport layer is
where engineers actually land when they search, which is why it gets its own page instead of a section
inside the message-format reference. Everything below is written from a gateway's side of the wire:
what the server must expose, and what has to be tolerated in front of it.
What the MCP protocol specifies: versions, transports, capabilities
MCP is JSON-RPC 2.0 carried by a transport, and the specification names exactly two of them — stdio and Streamable HTTP — while explicitly allowing a server to be stateless, so session management is optional (MCP transports). The server-side HTTP surface is therefore one mounted application on one path:
# backend/smartgate/api/mcp.py — source lines 399–406 (mount_mcp_routes)
def mount_mcp_routes(app: FastAPI) -> None:
"""Expose POST /mcp (Streamable HTTP, stateless)."""
apply_mcp_session_compat()
streamable_app = mcp.streamable_http_app()
streamable_app.router.lifespan_context = _noop_starlette_lifespan(streamable_app)
app.mount("/mcp", streamable_app)
logger.info("MCP Streamable HTTP at POST /mcp")
The docstring is the whole design decision: POST /mcp, Streamable HTTP, stateless. Two ordering details matter more than they look. Session compatibility is applied before mounting, because the patches must already be in place when the first request arrives rather than when the first session is created. And the mounted app's lifespan context is replaced with a no-op, so that mounting an MCP application inside a larger FastAPI process does not start a second lifecycle — the failure mode that reads as "the tools work locally but not in production". The log line at the end is deliberate: the transport a deployment is really serving should be greppable without reading code. How the document itself is laid out — which chapters are normative and which are optional — is the subject of the MCP specification walkthrough.
Streamable HTTP replaced the two-endpoint transport
The two transports differ in ways a client can observe directly, which is why the gateway records which one a request arrived on:
# backend/smartgate/core/audit_enrichment.py — source lines 29–32 (infer_transport)
def infer_transport(path: str) -> str:
if path.startswith("/mcp"):
return "mcp_sse"
return "rest"
The inference is a path prefix check, and it is applied per request so every audit row can say whether
the call came in over MCP or over the ordinary REST surface. That is the difference between "the agent
did something odd" and "someone hit the wrong endpoint with the right key". Streamable HTTP itself is
the simpler of the two MCP transports: one endpoint path that supports POST for client messages and GET
for a server-initiated stream, with the response to a POST allowed to be either a JSON body or an SSE
stream. Sessions are optional in this transport — a server may assign an identifier at initialization
and include it in an Mcp-Session-Id response header, and if it does, clients must send that header on
every subsequent request. A server that assigns none is not a degraded server; it is the stateless case
the specification anticipates, and it is the case that survives deploys, restarts, and horizontal
scaling without any reconnect logic.
The MCP SSE transport is legacy, and clients still send it
The older transport used two endpoints — an SSE stream to receive, a POST endpoint to send — and it is the reason a compatibility shim exists at all. The patches are applied in one guarded function:
# backend/smartgate/api/mcp_session_compat.py — source lines 89–103 (apply_mcp_session_compat)
def apply_mcp_session_compat() -> None:
"""Idempotent patches applied before mounting MCP SSE."""
global _PATCHED
if _PATCHED:
return
ServerSession._received_request = _compat_received_request # type: ignore[method-assign]
ServerSession._received_notification = _compat_received_notification # type: ignore[method-assign]
if not hasattr(_stateless_server_run, "_orig"):
_stateless_server_run._orig = lowlevel_server.Server.run # type: ignore[attr-defined]
lowlevel_server.Server.run = _stateless_server_run # type: ignore[method-assign]
_PATCHED = True
logger.info("MCP session compat enabled (stateless SSE + relaxed init gate)")
Two things get replaced: the session class's request and notification handlers, and the low-level
server's run method — wrapped, with the original kept on _orig so the wrapper delegates instead of
reimplementing the loop. The _PATCHED module flag is what makes calling this safe from anywhere: the
mount path calls it unconditionally during startup, and a second call is a no-op rather than a double
patch. The honest naming matters too. This is a compatibility layer for clients that exist, not a claim
that the specification was wrong; the specification moved on, and the shim's job is to keep a dated
client working without forking the tool code. Note that the statelessness is a property of both the
run loop and the transport: the compatibility run method does not require a completed handshake before
answering a tools/list, because hosts connect, list, and disconnect in whatever order their transport
allows. The revision history behind that shim — which chapters the first implementer wrote, and why
the single endpoint displaced the pair — is told in
Anthropic MCP clients and servers.
Choosing an MCP transport: stdio, SSE, or HTTP
The three transports you will meet in the wild answer different questions, and the choice is usually made for you by where the server runs:
| Transport | Reach | Session | Who uses it |
|---|---|---|---|
| stdio | One client, one child process | Process lifetime is the session | Local servers, desktop hosts, editors, CI |
| Streamable HTTP | Many clients, one endpoint | Optional Mcp-Session-Id |
Hosted servers, gateways, anything multi-tenant |
| HTTP+SSE (legacy) | Two endpoints (stream + POST) | Server-assigned session | Clients written before the current revision |
The trade is not performance; it is identity and reach. stdio has no authentication problem because it has no network: the parent process starts the server and the OS handles the boundary, which is why desktop hosts default to it. Streamable HTTP has to solve authentication, tenant separation, and timeouts, and it gets an optional session identifier to do the middle one — a session id is a routing hint, not an authorization token, and treating it as one is a security bug rather than a design choice. Authentication is the part of that the message itself never carries, and auth for MCP traffic is what turns a POST body into a call the server can attribute. The legacy pair is strictly more moving parts than Streamable HTTP for the same capability, which is why it stayed behind as compatibility instead of becoming the norm. Practically: pick stdio when the server is local and single-user, Streamable HTTP when it is remote or shared, and support the legacy transport only for the clients you can name. What the server side of that split owns — the tool surface, the session it may or may not keep, and where the process runs — is set out in the what how to evaluate a server asks about in order.
MCP stdio for local servers, one URL for a hosted endpoint
Where a hosted server has to be reachable from a host that expects to spawn a process, the adapter is a single upstream URL rather than a second implementation:
# lib/connect/mcp-proxy.ts — source lines 8–11 (mcpStreamableUpstreamUrl)
function mcpStreamableUpstreamUrl(base?: string): string {
const root = (base ?? smartgateBackendBase()).replace(/\/$/, "");
return `${root}/mcp/`;
}
This is the whole shape of a stdio-to-HTTP bridge: derive the root, strip a trailing slash, append the
MCP path, and hand that to a proxy that speaks stdin/stdout on the host side. Keeping it to four lines
is the point — a bridge that grows business logic becomes a second server with its own bugs, and the
tools behind it drift from the tools your remote clients see. The one trap worth naming: stdout is the
protocol channel for a stdio server, so anything a bridge prints to stdout that is not a JSON-RPC
message corrupts the stream. Diagnostics belong on stderr, always. The second trap is version skew —
the local bridge and the hosted endpoint must agree on a protocol revision at initialize, and that
agreement is per connection, not per installation, so upgrading one side is enough to move both.
MCP version negotiation, then body normalization
Negotiation is one exchange, and it has rules rather than a handshake loop. In the initialize
request the client must send a protocol version it supports; if the server supports that revision it
must answer with the same one, and if it does not, it answers with a revision it does support — the
latest it can speak, in practice
(MCP lifecycle). Because
the answer can differ from the request, a client that cannot handle the returned revision should
disconnect rather than continue and hope. In front of that exchange sits the normalization that keeps
strict parsers from rejecting legal-but-awkward bodies:
# backend/smartgate/api/mcp_sse_compat.py — source lines 67–99 (normalize_jsonrpc_body)
def normalize_jsonrpc_body(body: bytes) -> bytes:
"""Coerce non-object JSON-RPC params (e.g. []) to {} for pydantic validation."""
if not body:
return body
try:
data: Any = json.loads(body)
except (json.JSONDecodeError, UnicodeDecodeError):
return body
if not isinstance(data, dict):
return body
changed = _rewrite_direct_tool_method(data)
params = data.get("params")
if params is None:
data["params"] = {}
changed = True
elif isinstance(params, list):
# Cursor: tools/list, notifications/initialized with "params": []
data["params"] = {}
changed = True
elif isinstance(params, dict):
if _normalize_params_object(params):
changed = True
if not changed:
return body
logger.info(
"Normalized JSON-RPC body: method=%s params_type=%s",
data.get("method"),
type(data.get("params")).__name__,
)
return json.dumps(data, separators=(",", ":")).encode("utf-8")
Three cases are handled, and each one is a client you can name. A message with no params gets an
empty object. A message whose params is a list — the "params": [] that one widely used client
sends for tools/list and notifications/initialized — also gets an empty object, with the comment
naming the client so the reason survives refactoring. A message whose params is an object goes to
the nested-field fixer. The changed flag is what makes the function idempotent: when nothing was
rewritten the original bytes are returned untouched, which keeps signatures and ordering stable. The
log line records the method and the resulting params type — the first thing worth grepping when a
host's handshake fails for reasons the error message does not explain. The wider picture around that
exchange — the three actors, the capabilities each side declares, and the transport the session runs
on — is described in the protocol's actors and lifecycle.
A Streamable HTTP example: where normalization sits in the stack
Normalization is middleware, and that placement is a deliberate answer to "where can this possibly run?" — before the framework's own parser touches the body:
# backend/smartgate/api/mcp_sse_compat.py — source lines 102–138 (NormalizeJsonRpcMiddleware)
class NormalizeJsonRpcMiddleware:
"""ASGI middleware: fix params: [] before MCP sse.handle_post_message parses body."""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http" or scope.get("method") != "POST":
await self.app(scope, receive, send)
return
path = scope.get("path", "")
if "messages" not in path:
await self.app(scope, receive, send)
return
chunks: list[bytes] = []
while True:
message = await receive()
if message["type"] != "http.request":
await self.app(scope, receive, send)
return
chunks.append(message.get("body", b""))
if not message.get("more_body", False):
break
body = normalize_jsonrpc_body(b"".join(chunks))
sent = False
async def replay_receive() -> dict[str, Any]:
nonlocal sent
if sent:
return {"type": "http.disconnect"}
sent = True
return {"type": "http.request", "body": body, "more_body": False}
await self.app(scope, replay_receive, send)
The guards carry as much weight as the fix: it only acts on POST, and only on paths containing
messages, so tool responses and unrelated routes pay nothing. The middleware buffers the body until
more_body is false, normalizes the joined bytes once, and then hands the application a receive
callable that replays the rewritten body. That replay is the standard ASGI idiom for "the bytes are
consumed, here is what the app should see instead", and it is also where careless implementations
break: the closure returns the body on the first call and http.disconnect on every call after that,
because the application may ask again and there is genuinely no more data. Getting that wrong produces
a server that looks like it hangs on a second read, followed by a client timeout that points at the
network. The single-normalization detail is the other half: because the rewrite happens once in the
middle of the stack, no tool ever has to know that some hosts send an empty list where the schema
wanted an object. The request and result envelopes the rewrite protects are walked through in
Model Context Protocol Example.
HTTP transport details a client has to get right
Beyond the endpoint path, a handful of details break integrations in ways that are hard to diagnose from an error message, so they are worth a checklist:
- Accept both content types. A POST to the MCP endpoint should advertise that the client can
consume either
application/jsonortext/event-stream, because the server may answer a request with a single JSON body or a stream. - Echo the session header. If
initializecame back with anMcp-Session-Id, every later request must carry it; a server is entitled to reject requests that omit it. - Send the negotiated revision. Clients using HTTP include the agreed protocol version in an
MCP-Protocol-Versionrequest header, and a server that receives an unsupported value is expected to answer400 Bad Requestrather than fall back silently (MCP transports). - Expect a bare acknowledgment. Notifications and responses sent to the server may be answered
with
202 Acceptedand no body at all. A client that insists on parsing a JSON-RPC payload from that response will report a protocol error where nothing went wrong. - Reconnect deliberately. The GET stream can drop; resumption semantics and session expiry are where proxy timeouts surface, so a client should re-initialize rather than assume the old session survived a long idle period.
None of this is exotic, and all of it is invisible until it fails. A gateway compresses the problem by owning these details once, in front of every tool.
A2A protocol: a different layer of the stack
A2A — the agent-to-agent protocol — is not a competing transport for MCP, and the confusion costs people
weeks. MCP answers "how does a model call a capability with a schema attached?"; A2A answers "how does
one autonomous agent hand a task to another agent that may be run by a different team, vendor, or
platform?" An A2A interaction is task-shaped: a request has a lifecycle, may be long-running, may
return artifacts, and may be delegated further. An MCP tool call is much smaller than that — it is one
request, one result, and annotations that describe risk rather than a workflow. The transport question
is answered separately in both protocols, which is precisely why arguing about "which protocol is
better" is unproductive: they sit at different layers of the same stack. A realistic 2026 system uses
MCP for the tool layer, A2A for the delegation layer, and a gateway in between for identity, budget,
and audit. The measured demand underlines that this is a real question and not a naming quirk:
a2a protocol carries about 3,600 monthly US searches, on the same order as the narrower MCP
transport terms.
MCP vs A2A protocol: what each one actually carries
The cleanest way to hold the difference in mind is to ask what a message carries:
| Question | MCP | A2A |
|---|---|---|
| What travels | A JSON-RPC request naming a tool, a resource, or a prompt | A task, its state, and its artifacts |
| Lifetime | One request, one response | A task lifecycle, possibly hours |
| Who the peer is | A capability (server) and its client | Another agent, with its own identity |
| Who typically asks | The model, through its host | A planner, a scheduler, or a human |
| What governance attaches to | Budget, rate limit, and audit per call | Authorization, delegation, and completion |
Both rely on capability discovery, and both are transport-agnostic in principle, but the governance surface is where they differ for an operator: an MCP tool call is cheap enough to meter per request, while an A2A task is something you authorize and then observe. A gateway that meters tool calls and ignores delegation is leaving the expensive path ungoverned — the loop that costs money is usually the one that keeps re-planning, not the single fetch. That asymmetry is the reason the two belong in one architecture diagram and two different control planes.
Agentic RAG over MCP: what the gateway adds
An agentic RAG loop calls the model repeatedly, and the bill is driven by what is re-sent rather than by what is new. The gateway's contribution is one call that shrinks the context before the next model call:
# backend/smartgate/api/mcp.py — source lines 150–174 (smart_context_gate)
@server.tool(
name="smart_context_gate",
description=TOOL_DESCRIPTIONS["smart_context_gate"],
annotations=tool_annotations("smart_context_gate"),
)
async def smart_context_gate(
text: str = Field(description="Long text to compress before the host LLM call."),
ratio: float = Field(
default=0.5,
description="Target compression ratio (e.g. 0.3–0.7).",
),
purpose: str | None = Field(
default=None,
description="Optional goal to pre-filter paragraphs (step intent, user query).",
),
) -> str:
_, registry = _app_state()
module = registry.get("context_gate")
ctx = _tool_ctx()
return await _run_with_audit(
"compress",
ctx,
module.process(ctx, text=text, ratio=ratio, purpose=purpose),
{"ratio": ratio, "purpose": purpose},
)
The tool is declared like any other — a name, a description pulled from a shared table, and annotations
derived from the tool name — and the parameters are ordinary: the text to compress, a target ratio, and
an optional purpose string that pre-filters paragraphs toward the current step's intent. The return
statement is the interesting part. It does not answer directly; it routes through the audited call
path (_run_with_audit) with the ratio and purpose recorded, so a compression call is visible in the
same activity log as every other tool call. That is what makes an agentic loop governable: not a
special compression endpoint, but the same metering, the same budget checks, and the same audit row as
everything else. The default ratio of 0.5 is a starting point rather than a policy — a retrieval step
that re-reads a document it already summarized can compress far harder than a first read, and the
audit row is where you find out which of those two happened.
How an LLM gateway classifies MCP traffic
Once MCP and REST traffic share a deployment, every audit row needs to say which surface a request came in on, and that classification is deliberately tiny:
# backend/smartgate/core/audit_enrichment.py — source lines 35–40 (infer_route)
def infer_route(path: str, route_hdr: str) -> str:
if route_hdr:
return route_hdr
if path.startswith("/mcp"):
return "mcp"
return "rest"
Three lines: honour an explicit route header if the caller supplied one, otherwise treat anything under the MCP path as MCP, and call everything else REST. Two properties make this worth keeping small. It is explicit-first — an operator or an upstream proxy can override the inference without a code change, which is the escape hatch you want during a migration. And it is total: there is no third answer and no failure mode, so aggregation over the audit table never has to handle a null route. For an LLM gateway the interesting consequence is that transport, route, and tool are three separate dimensions in the log. A spike in REST requests is a client problem; a spike in MCP requests with a single tool name is a loop; and neither would be visible if the gateway recorded only the model and the token count.
How SmartGate handles transports and versions
| Transport | Session | What you get | What it costs | |
|---|---|---|---|---|
| SmartGate (hosted) | Streamable HTTP, stateless, one POST endpoint (/mcp) |
None required; no session id to carry | Seven smart_* tools with annotations, budget and rate controls, audit rows per call. Free: 2M tokens/mo, 120 req/min/key |
Platform fee; the savings-share only starts after $15 saved (pricing) |
| Local stdio server | stdin/stdout in a spawned process | The process lifetime | Whatever that server implements, and nothing at the gateway layer | Your own process management |
| Legacy HTTP+SSE server | Two endpoints, server-assigned session | Required, and expiring | Works with older clients; more moving parts than the current transport | Reconnect logic you have to maintain |
| Hand-rolled JSON-RPC shim | Your own HTTP handling | Yours to invent | Bugs proportional to how much of the protocol you re-implement | The protocol's own edge cases, rediscovered |
The honest reading: if you run a single local server for a single host, stdio is the right answer and you need nothing from this page. The transport work only pays for itself when many hosts you do not control talk to one endpoint you do — which is exactly the situation a stateless Streamable HTTP mount with a normalization layer in front is built for.
How to get started
- Start with the endpoint, not the code. The Connect page generates the exact client block for Cursor, Claude Desktop, Windsurf, OpenClaw, or a generic client, and the MCP endpoint reference documents the path and headers.
- Ask for
tools/listand read the annotations. Seven tools come back, each with a title and a read-only hint — that response is the contract your host can rely on. The tools reference lists them with their parameters. - Then call one tool and watch the audit row. If the row is missing, the request never reached the gateway — a transport problem, not a tool problem.
- Compare against the layers around it. If you are deciding between a tool call and a delegated task, read the MCP vs A2A breakdown; if you are wiring a shared deployment, start from what an MCP gateway does.
- Then read the tool surface. The MCP tools reference documents
the
tools/listschemas and annotations your client will consume, and standing up an MCP server covers the server half of the same connection.
Start on Free — 2M tokens/month, all seven tools, 120 MCP requests/min per key: start free, then compare limits and log retention on the pricing page.
Frequently Asked Questions
What are the MCP protocol versions?
MCP does not enumerate versions in the message flow; each revision is a dated string. The client proposes one it supports in the initialize exchange, and the server answers with the revision it will actually speak — the latest it can speak when it cannot match the request. The client then carries the agreed revision forward in the MCP-Protocol-Version header.
Which MCP transport should I use?
Pick stdio when the server is local and single-user, Streamable HTTP when it is remote or shared, and support the older HTTP+SSE pair only for clients you can name. Streamable HTTP replaced the two-endpoint transport, and that pair is kept for backwards compatibility rather than as the norm.
Does MCP have more than one transport?
Two are current: Streamable HTTP and stdio. The earlier HTTP+SSE pair is legacy that exists for backwards compatibility, and clients written against it still work because servers and gateways keep the session shim in place.
Which protocol version should a client send?
The latest one it supports. If the server supports that revision it must answer with the same one; otherwise it answers with a revision it does support, and a client that cannot speak the answer should disconnect rather than continue.
Do I need a session id to use Streamable HTTP?
No. Sessions are optional in that transport. A server may assign an identifier at initialization and expect it back on later requests, but a stateless server that assigns none is conformant and does not need any reconnect logic after a deploy or restart.
What happens if I send the wrong protocol version?
An HTTP server can reject the request outright when the MCP-Protocol-Version header names something it does not support, answering with a 400 rather than negotiating silently. That is a designed failure, not a bug to work around.
Can I use MCP and A2A in the same system?
Yes, and most realistic systems do. MCP covers the tool layer where one request maps to one result, and A2A covers task delegation between agents. They are different layers, so the governance is different too: meter tool calls, authorize tasks.
Why does my integration work locally but not against a remote endpoint?
Check the header and session details before the tools. Most remote failures are a missing session header, an Accept header that does not allow an event stream, or a handshake race against a server that resets its session state on deploy.
Is the older SSE transport deprecated enough to ignore?
Deprecated is not the same as absent. If your users have clients configured against it, support it or document the upgrade — the compatibility layer exists precisely because ignoring it breaks real installations.
What MCP transports exist?
Common transports are stdio for local process servers, older HTTP+SSE streaming setups, and Streamable HTTP as the current remote transport shape.
Why do MCP protocol versions matter operationally?
Version and transport mismatches break handshakes, capability negotiation, and streaming behavior before any tool call succeeds.
Should production gateways still support SSE?
Many fleets still see legacy SSE clients during migration; plan for coexistence, but new remote paths should target Streamable HTTP per current MCP docs.
Where does SmartGate sit relative to transports?
As an MCP tool gateway it terminates or forwards tool traffic under policy; it does not replace the protocol specification or your model provider.
Limitations and what this does not do
- Stateless means per-request identity. A stateless gateway must re-establish what it knows about a caller from the key and headers on every request; anything you relied on a session to remember has to be passed explicitly or stored server-side.
- Compatibility layers are compatibility layers. They make dated clients work; they do not repair a malformed body, and they add a small amount of code that must be revisited when a client fixes its own bug.
- Transport choice does not fix an expensive loop. Choosing Streamable HTTP over SSE changes how a request arrives, not how much it costs; budgeting, dedup, and compression are separate layers.
- Version support moves. Supported revisions are maintained in code and can change between releases, so treat a specific revision string as an implementation detail of the deployment you are talking to.
- This page is not a message-shape reference. Request and result envelopes, and the tool-call path, are documented separately in /industry/mcp-protocol-message-format-explained.
Sources
- Model Context Protocol — transports, including Streamable HTTP, the session header and the protocol version header: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
- Model Context Protocol — lifecycle, including
initializenegotiation: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle - Model Context Protocol — server tools (
tools/list,tools/call): https://modelcontextprotocol.io/specification/2025-06-18/server/tools - Anthropic — introducing the Model Context Protocol: https://www.anthropic.com/news/model-context-protocol
- SmartGate — product site and pricing: https://smartgate.network · https://smartgate.network/pricing