Short answer: The Model Context Protocol message format is JSON-RPC 2.0 — one request object
with jsonrpc, method, params, and an id, one result or error object back, and notifications
that carry no id at all. Over HTTP the whole conversation is POSTed to a single endpoint:
initialize, then tools/list, then tools/call, with the session optional. The parts that break
integrations are the small ones: an empty params array, an arguments field typed as a list, and a
client that calls the tool name as the method.
Key takeaways
-
Three shapes, one envelope. Requests (
id+method+params), notifications (noid), and results (id+resultorerror) — every MCP message is one of them. -
initializeis a negotiation, not a formality. The client proposes a protocol version, the server answers with the version it will speak plus its capabilities and instructions. -
tools/listandtools/callare the two messages that matter for tool use. One advertises what exists (with annotations), the other names a tool and passesarguments. -
params: []is legal JSON and invalid for most strict parsers — Cursor sends exactly that fortools/listandnotifications/initialized, which is why normalization sits in front of the parser rather than inside each tool. -
A stateless HTTP server should not gate
tools/listbehind a completed handshake, because clients connect, list, and disconnect in whatever order their transport allows. -
Read
tools/listfrom a live server before writing a client. 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.
The short version for whoever signs the invoice
"Model context protocol" carries roughly 12,100 monthly US searches, and the first page for
model context protocol message format is an AI Overview built from the specification itself
(MCP spec). That tells you two things
about this topic: demand is real, and the audience is engineers who are comparing implementations,
not shoppers. What they cannot get from the specification is what holds up in production — which
message shapes real hosts send, and where a gateway has to be lenient.
SmartGate is an MCP-native algorithm gateway for token control, traffic shaping, and agent audit. It
sits between a host (Cursor, Claude Desktop, Windsurf, OpenClaw, or your own client) and the open
web, exposing seven tools: smart_fetch, smart_search, smart_context_gate, smart_dedup,
smart_budget_guard, smart_memory, and smart_pipe. The message path described below is the layer that
makes those seven tools work across hosts that disagree on details — and the same layer is where
budget checks, rate limits, and audit rows are attached.
What the protocol actually specifies
MCP is JSON-RPC 2.0 over a transport. The specification covers two transports, stdio and Streamable
HTTP, and explicitly allows a server to be stateless — session management is optional, and a server
may answer each request independently
(MCP transports). The
server-side surface for tools is defined as two messages: tools/list to advertise tools and
tools/call to invoke one
(MCP tools).
A worked MCP JSON-RPC round trip is easier to
follow than the schema alone, because it shows the four fields and the two responses in
the order a client actually sends them.
The gap between the specification and a working integration is visible in the wild: there is a
Stack Exchange question asking precisely what the MCP message format is and how it differs from the
communication architecture, with answers pointing at the same conclusion — the format is JSON-RPC,
the architecture is transport plus session
(Stack Exchange).
Everything below is that distinction, from the gateway's side of the wire.
Which protocol versions a server will answer for, and which transport carries the messages, is the
other half of that picture — MCP Protocol Versions and Transports
covers both.
mount_mcp_routes: one endpoint, no session state
The whole HTTP surface is 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 specification in one line: POST /mcp, Streamable HTTP, stateless. Session
compatibility is applied before mounting, because the patches have to be in place when the first
request arrives rather than when the first session is created. The lifespan context is then replaced
with a no-op so that mounting the MCP app inside a larger FastAPI application does not run a second
startup lifecycle — a failure mode that looks like "the tools work locally but not in production".
normalize_jsonrpc_body: the leniency a real client needs
Before any parsing happens, the body is normalized. This is the function that turns a
non-conforming-but-legal message into something pydantic will accept:
# 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. A message with no params gets {}. A message whose params is a
list — the "params": [] that Cursor sends for tools/list and notifications/initialized —
also gets {}, with the comment naming the client. A message whose params is an object is
passed to the nested-field fixer below. The rewrite is a no-op when nothing changed, and the log line
records the method and the resulting params type, which is the first thing worth grepping when a
client's handshake fails.
NormalizeJsonRpcMiddleware: where in the stack it happens
Normalization is applied as ASGI middleware, which is the only place in a Python server where you can
rewrite a request body before the framework's own parser sees it:
# 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 matter as much as the fix: only POST, and only paths containing messages. Everything
else falls through untouched, so the gateway pays no cost on tool responses or on other routes. The
middleware buffers the body until more_body is false, normalizes the joined bytes, then hands the
application a receive callable that replays the rewritten body — the standard ASGI idiom for "the
body is consumed, here is what the app should see instead".
_normalize_params_object: the nested quirk
The object-level fix is deliberately tiny, because it fixes one observed shape rather than validating
arbitrary input:
# backend/smartgate/api/mcp_sse_compat.py — source lines 57–64 (_normalize_params_object)
def _normalize_params_object(params: dict[str, Any]) -> bool:
"""Fix nested params quirks from MCP hosts. Returns True if mutated."""
changed = False
arguments = params.get("arguments")
if isinstance(arguments, list):
params["arguments"] = {}
changed = True
return changed
MCP tool arguments belong in params.arguments as an object. Some hosts emit a list when the
tool takes no parameters, which strict validation rejects. Coercing [] to {} keeps the call
semantically identical — no arguments — while letting the same request pass validation on a server
that would otherwise return an error the client cannot interpret. The bool return value is what
makes the normalizer idempotent: it only re-serializes the body when something actually changed.
_rewrite_direct_tool_method: when the method is the tool name
The most pragmatic concession in the whole file is the direct-method rewrite, and it exists because
users write what feels obvious:
# backend/smartgate/api/mcp_sse_compat.py — source lines 27–54 (_rewrite_direct_tool_method)
def _rewrite_direct_tool_method(data: dict[str, Any]) -> bool:
"""Rewrite {method: smart_fetch, params: {url: ...}} → standard tools/call."""
method = data.get("method")
if not isinstance(method, str) or method not in _SMART_TOOL_METHODS:
return False
original = method
params = data.get("params")
if isinstance(params, list):
params = {}
elif not isinstance(params, dict):
params = {}
name = method
if isinstance(params.get("name"), str):
name = params["name"]
if isinstance(params.get("arguments"), dict):
arguments = params["arguments"]
else:
arguments = {
k: v for k, v in params.items() if k not in ("name", "arguments", "_meta")
}
data["method"] = "tools/call"
data["params"] = {"name": name, "arguments": arguments}
logger.info("Rewrote legacy MCP tool method %s → tools/call (name=%s)", original, name)
return True
A standard call is {method: "tools/call", params: {name: "smart_fetch", arguments: {...}}}. But a
hand-written request often looks like {method: "smart_fetch", params: {url: "…"}} — the tool name
promoted to the method, the arguments flattened into params. Rather than rejecting that with a
protocol error, the gateway rewrites it into a conforming tools/call, extracting inline keys as
arguments and stripping name, arguments, and _meta so nothing is passed through twice. The
logger.info line records both the original method and the resolved tool name, which turns "my
client says unknown method" into a one-line log search.
replay_receive: one body, exactly once
Replaying the consumed body is small enough to hide, and wrong implementations fail in ways that look
like networking problems:
# backend/smartgate/api/mcp_sse_compat.py — source lines 131–136 (replay_receive)
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}
The closure returns the rewritten body on the first call and http.disconnect on every call after
that. That is the ASGI contract for a request whose bytes have already been read: the application may
ask again, but there is no more data, so it must be told the client went away rather than being
handed an empty body. Getting this wrong produces a server that appears to hang on the second read —
and a client-side timeout that points at the wrong layer.
_stateless_server_run: why tools/list must not wait for init
Session-state handling is where stateless servers diverge from the textbook flow, and the gateway
patches the run loop rather than the tools:
# backend/smartgate/api/mcp_session_compat.py — source lines 70–86 (_stateless_server_run)
async def _stateless_server_run(
self: lowlevel_server.Server,
read_stream,
write_stream,
initialization_options,
raise_exceptions: bool = False,
stateless: bool = True,
):
"""SSE sessions start Initialized so tools/list is not rejected during init races."""
return await _stateless_server_run._orig( # type: ignore[attr-defined]
self,
read_stream,
write_stream,
initialization_options,
raise_exceptions=raise_exceptions,
stateless=stateless,
)
The docstring states the production reality: SSE sessions start already Initialized, so a
tools/list that arrives during an initialization race is answered instead of rejected. This is a
deliberate trade — a stricter server would require the handshake to complete first and would be
right by the book, while shedding exactly the clients that connect, list, and disconnect quickly.
The lifecycle a fully negotiating client walks through instead, with the actors named, is the
subject of Model Context Protocol Explained.
apply_mcp_session_compat: idempotent monkey-patching
The patches are applied in one guarded function, because applying them twice breaks the server:
# 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 are being replaced: the session class's request and notification handlers (both point at
the compatibility versions), and the low-level server's run method — wrapped, with the original
kept on _orig so the wrapper can delegate instead of reimplementing. The _PATCHED guard is what
makes this safe to call from anywhere: mount_mcp_routes calls it unconditionally, and a second call
is a no-op rather than a double patch. Note the honest naming — this is a compatibility layer for
real clients, not a claim that the specification is wrong.
_compat_received_request: the handshake answer
The relaxed handler is where initialize is answered, and where the version negotiation is visible:
# backend/smartgate/api/mcp_session_compat.py — source lines 24–57 (_compat_received_request)
async def _compat_received_request(
self: ServerSession,
responder: RequestResponder[types.ClientRequest, types.ServerResult],
) -> None:
"""Allow tools/* during Initializing; only block when session never started init."""
match responder.request.root:
case types.InitializeRequest(params=params):
requested_version = params.protocolVersion
self._initialization_state = InitializationState.Initializing
self._client_params = params
with responder:
await responder.respond(
types.ServerResult(
types.InitializeResult(
protocolVersion=requested_version
if requested_version in SUPPORTED_PROTOCOL_VERSIONS
else types.LATEST_PROTOCOL_VERSION,
capabilities=self._init_options.capabilities,
serverInfo=types.Implementation(
name=self._init_options.server_name,
version=self._init_options.server_version,
websiteUrl=self._init_options.website_url,
icons=self._init_options.icons,
),
instructions=self._init_options.instructions,
)
)
)
self._initialization_state = InitializationState.Initialized
case types.PingRequest():
pass
case _:
if self._initialization_state == InitializationState.NotInitialized:
raise RuntimeError("Received request before initialization was complete")
Three behaviours are worth reading closely. initialize records the requested protocol version,
sets the session to Initializing, and answers with the requested version when it is supported,
otherwise the latest — the standard MCP negotiation, and the reason a client speaking a newer
minor version still gets a usable session. ping is accepted silently. Any other request arriving
before initialization raises, which is the one gate the compatibility layer deliberately keeps: the
point is to allow tools/* during the handshake race, not to remove initialization as a concept.
register_mcp_tools: seven tools, one registration path
Tool advertisement is a plain function call per tool, which is what keeps tools/list and the
documentation from drifting apart:
# backend/smartgate/api/mcp.py — source lines 106–126 (register_mcp_tools)
def register_mcp_tools(server: FastMCP) -> None:
"""Register all 7 smart_* tools on a FastMCP instance."""
@server.tool(
name="smart_fetch",
description=TOOL_DESCRIPTIONS["smart_fetch"],
annotations=tool_annotations("smart_fetch"),
)
async def smart_fetch(
url: str = Field(description="Full HTTP or HTTPS URL to fetch."),
timeout: int = Field(default=30, description="HTTP timeout in seconds."),
) -> str:
_, registry = _app_state()
module = registry.get("fetch")
ctx = _tool_ctx()
return await _run_with_audit(
"fetch",
ctx,
module.process(ctx, url=url, timeout=timeout),
{"url": url},
)
Each tool is declared once, with its description read from a shared table and its annotations
derived from the tool name — so the model-facing metadata and the human-facing docs have a single
source. The two tools in this window show the pattern end to end: smart_fetch takes a URL and a
timeout, smart_search takes a query and a result cap, and both end in the same audited call path.
That shared ending is what makes tools/call predictable regardless of which of the seven tools is
invoked.
_run_with_audit: one exit path for every call
Every tool call — seven tools, any arguments — returns through the same function:
# backend/smartgate/api/mcp.py — source lines 90–103 (_run_with_audit)
async def _run_with_audit(
tool: str,
ctx: ToolContext,
process_coro,
params: Optional[Dict[str, Any]] = None,
) -> str:
_ensure_mcp_audit_context()
app, _registry = _app_state()
result = await process_coro
await app.state.audit_hook(ctx, result, tool, params or {})
if not result.success:
msg = result.error or f"{tool} failed"
raise ToolError(msg)
return json.dumps(result.data, ensure_ascii=False)
It re-binds the audit context for in-stream calls, awaits the tool's coroutine, writes an audit row
through the application's audit hook, and only then decides how to answer. A failed tool raises a
ToolError carrying the module's own message, which reaches the client as a JSON-RPC error instead
of a success payload with an error field inside it; a successful call returns the data as compact
JSON. Because every tool goes through this path, an audit log that is missing a call means the call
never reached the gateway, not that a tool forgot to log.
tool_annotations: what tools/list says about risk
The last piece of the message format that clients actually consume is the annotation block, and it is
derived, not hand-written:
# backend/smartgate/api/mcp_tool_docs.py — source lines 63–67 (tool_annotations)
def tool_annotations(name: str) -> ToolAnnotations:
return ToolAnnotations(
title=TOOL_TITLES.get(name),
readOnlyHint=name in READ_ONLY_TOOLS,
)
title gives the tool a human-readable name in the host's UI, and readOnlyHint tells the client
whether invoking it can change state. Read-only hints matter more than they look: hosts use them to
decide what may run without confirmation, so a wrong hint changes the approval experience for every
user of that client. Deriving the hint from a READ_ONLY_TOOLS set keeps it consistent with what the
tool actually does.
How this differs from a local MCP server
| Message path | Session model | What you get beyond the tools | |
|---|---|---|---|
| SmartGate (hosted, stateless) | JSON-RPC over Streamable HTTP at one POST endpoint (/api/mcp), normalized for host quirks |
Stateless; sessions optional, no session id required | Free: 2M tokens/mo, all 7 tools, 120 req/min/key. Pro from $18/mo, share only after $15 saved (pricing) |
| Local stdio MCP server | JSON-RPC over stdin/stdout, no HTTP layer | Process lifetime is the session | Whatever the server implements; nothing at the gateway layer |
| Hand-rolled JSON-RPC shim | Your own parsing of the same envelopes | Yours to invent | Bugs proportional to how much of the protocol you re-implement |
| LLM proxy/router | Different protocol entirely (model calls) | Provider sessions | Model routing, not tool governance |
The honest reading: if you are writing a client or a single-purpose server, the specification is
enough — JSON-RPC is small, and the two tool messages are small. The compatibility functions above
exist because you are talking to many hosts you do not control, and because "accept the message,
then record it" is a gateway's job rather than a library's.
How to get started
-
Look at the messages you are already sending. Point a client at
https://smartgate.network/api/mcp(POST) withAuthorization: Bearer <key>; the Connect page generates the exact block for Cursor, Claude Desktop, Windsurf, OpenClaw, or a generic client. -
Call
tools/listonce and read the annotations. Seven tools appear, each with a title and a read-only hint — that response is the contract your client can rely on. -
Then call
tools/callonsmart_fetchwith a URL, and check that the JSON result comes back as text content rather than an error object. - Watch the audit row appear in Activity Logs; if it is missing, the message never reached the gateway.
Start on Free — 2M tokens/month, all seven tools, 120 MCP requests/min per key: start free,
then compare per-key limits and log retention on the pricing page.
FAQ
Is the MCP message format just JSON-RPC?
Effectively yes: JSON-RPC 2.0 envelopes carrying MCP-defined methods. The protocol adds semantics on
top — initialize negotiation, capabilities, tool annotations — but the wire format is JSON-RPC.
Which methods does a tool-using client actually need?
Four in the common path: initialize, notifications/initialized, tools/list, and tools/call. Everything
else (resources, prompts, sampling) is optional surface.
Why would a server accept a tool name as the method?
Because hand-written clients do it, and a rewrite is friendlier than an error. The gateway converts
{method: "smart_fetch", params: {url: …}} into a conforming tools/call and logs both names.
Is "params": [] valid MCP?
It is valid JSON-RPC (params may be a structured value), and at least one widely used client sends it
for parameterless methods. Strict servers reject it; a normalizing middleware coerces it to {}.
Do I need a session id?
Not for a stateless server. Sessions are optional in the Streamable HTTP transport, and the gateway
runs without them, which is also why a config change does not require reconnecting.
What happens if a call arrives before initialization completes?
Read-only tool traffic is allowed during the handshake race, and anything else raises — a deliberate
middle ground between the strict specification and clients that connect, list, and disconnect.
Where do tool results come back?
As JSON-RPC results whose content is text: tool output is serialized to compact JSON, and a failed
tool becomes a JSON-RPC error rather than a success object with an error field.
Limitations and what this does not do
- The compatibility layer is a compatibility layer. It exists because hosts disagree; a client that sends malformed JSON still fails, and normalization does not repair an unparseable body.
-
Statelessness costs per-connection state. A server that accepts
tools/listduring initialization gives up the guarantee that a session was properly established first; that is the trade, not an oversight. -
Annotations are hints, not enforcement.
readOnlyHintinfluences client UI; the gateway's actual controls are the rate limit, the budget cap, and the key's scope. - Tool-count and protocol-version details move. The messages here are stable, but supported versions and tool metadata are maintained in code and can change between releases.
Sources
- Model Context Protocol — specification (2026-07-28): https://modelcontextprotocol.io/specification/2026-07-28
- Model Context Protocol — transports, including stateless Streamable HTTP: https://modelcontextprotocol.io/specification/2026-07-28/basic/transports
- 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
- Stack Exchange — clarification on the MCP message format vs. communication architecture: https://softwareengineering.stackexchange.com/questions/458556/clarification-on-model-context-protocol-message-format-vs-communication-archit
- SmartGate — product site and pricing: https://smartgate.network · https://smartgate.network/pricing
Method note
The code in this article is not transcribed. Each block was cut directly out of the slice body
returned by the SmartGate slice API and then re-asserted byte-for-byte as a substring of that body
before publication; the first line inside every fence records the file and the exact source lines.
Symbols were pinned with whole-name containment (rule A level 2) and confirmed by the service's
slot-proof endpoint. Sections describe the gateway's message path only; no third-party client
implementation is quoted.
Slice provenance
| # | SERP keyword | Symbol | File | Source lines | How it was pinned | sha256(12) |
|---|---|---|---|---|---|---|
| 1 | mount_mcp_routes how the MCP Streamable HTTP POST route is mounted | mount_mcp_routes |
backend/smartgate/api/mcp.py |
399–406 | rule A L2 → slot-proof | e66f6b69a172 |
| 2 | normalize_jsonrpc_body normalize the JSON-RPC body of an MCP message | normalize_jsonrpc_body |
backend/smartgate/api/mcp_sse_compat.py |
67–99 | rule A L2 → slot-proof | d8359617452f |
| 3 | NormalizeJsonRpcMiddleware ASGI middleware for MCP JSON-RPC parsing | NormalizeJsonRpcMiddleware |
backend/smartgate/api/mcp_sse_compat.py |
102–138 | rule A L2 → slot-proof | 472493b3162e |
| 4 | _normalize_params_object nested params object quirk in MCP tool calls | _normalize_params_object |
backend/smartgate/api/mcp_sse_compat.py |
57–64 | rule A L2 → slot-proof | 444ddb5db5f2 |
| 5 | _rewrite_direct_tool_method rewrite direct tool method MCP messages | _rewrite_direct_tool_method |
backend/smartgate/api/mcp_sse_compat.py |
27–54 | rule A L2 → slot-proof | 2deb57048f8c |
| 6 | replay_receive replay the SSE receive stream for MCP messages | replay_receive |
backend/smartgate/api/mcp_sse_compat.py |
131–136 | rule A L2 → slot-proof | 74963ba50239 |
| 7 | _stateless_server_run stateless MCP session run without session state | _stateless_server_run |
backend/smartgate/api/mcp_session_compat.py |
70–86 | rule A L2 → slot-proof | 22066742f8ed |
| 8 | apply_mcp_session_compat MCP session compatibility patch | apply_mcp_session_compat |
backend/smartgate/api/mcp_session_compat.py |
89–103 | rule A L2 → slot-proof | 2aec583c6238 |
| 9 | _compat_received_request accept tools list during MCP initialize | _compat_received_request |
backend/smartgate/api/mcp_session_compat.py |
24–57 | rule A L2 → slot-proof | cc03b6abdf85 |
| 10 | register_mcp_tools register all seven smart tools on the MCP server | register_mcp_tools |
backend/smartgate/api/mcp.py |
106–126 | rule A L2 → slot-proof | 9d4a1623b28c |
| 11 | _run_with_audit run each MCP tool call with audit context | _run_with_audit |
backend/smartgate/api/mcp.py |
90–103 | rule A L2 → slot-proof | 454c08008ffa |
| 12 | tool_annotations MCP tool annotations in the tools list response | tool_annotations |
backend/smartgate/api/mcp_tool_docs.py |
63–67 | rule A L2 → slot-proof | a822944005fe |
Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before
publication. 12 of 12 sections pinned, 0 abstentions, 0 misses.
This guide is republished from smartgate.network; it was drafted with AI assistance and reviewed by our team.
Top comments (0)