Inside the adapters¶
5.1 Detection¶
detect_server_kind(server) checks, in order: NativeMCPServer instance; any class in the
MRO named FastMCP from a module whose top package is fastmcp; a type whose module starts
with mcp.server.mcpserver or mcp.server.fastmcp; an instance of
mcp.server.lowlevel.Server; duck typing (add_request_handler or request_handlers means
low-level; _lowlevel_server or _mcp_server means high-level). register_into sets the
prefix first, then calls the matching install.
5.2 lowlevel.py: LowLevelBinding¶
The binding wraps request handlers in place. _METHODS lists what it handles:
| Method | 1.x request class | 2.x params class | include kind |
Implementation |
|---|---|---|---|---|
tools/list |
ListToolsRequest |
PaginatedRequestParams |
tools | _list_tools |
tools/call |
CallToolRequest |
CallToolRequestParams |
tools | _call_tool |
resources/list |
ListResourcesRequest |
PaginatedRequestParams |
resources | _list_resources |
resources/templates/list |
ListResourceTemplatesRequest |
PaginatedRequestParams |
templates | _list_templates |
resources/read |
ReadResourceRequest |
ReadResourceRequestParams |
resources | _read_resource |
prompts/list |
ListPromptsRequest |
PaginatedRequestParams |
prompts | _list_prompts |
prompts/get |
GetPromptRequest |
GetPromptRequestParams |
prompts | _get_prompt |
completion/complete |
CompleteRequest |
CompleteRequestParams |
completions | _complete |
resources/subscribe |
SubscribeRequest |
SubscribeRequestParams |
subscriptions | _subscribe |
resources/unsubscribe |
UnsubscribeRequest |
UnsubscribeRequestParams |
subscriptions | _unsubscribe |
logging/setLevel |
SetLevelRequest |
SetLevelRequestParams |
logging | _set_level |
sdk_major is 2 when the server has add_request_handler, else 1.
On mcp 2.x, _install reads the existing entry with server.get_request_handler(method),
keeps its handler as orig and its params type, and registers
handler(ctx, params), which calls impl(ctx, params_as_wire_dict, orig_call) where
orig_call is lambda: orig(ctx, params) or None. On mcp 1.x it replaces
server.request_handlers[RequestClass] with handler(req), which fetches
server.request_context (a LookupError means no context), calls the implementation and
wraps the result in ServerResult; orig_call unwraps the original's .root. Every
implementation therefore receives the context, the params as a camelCase dict, and a way to
call the host's handler.
Routing by ownership:
tools/callandprompts/get:owns_tool / owns_promptrequire the prefix (when set) and a registry entry for the stripped name. Not ours: call the host, or raise -32602Unknown toolwhen there is no host handler.resources/read:owns_resourceis an exact URI match or any template match. Not ours: host, or -32602 / -32002 by era.completion/complete: ours when the prompt or the exact template string is ours; else the host, or an empty completion.resources/subscribe,unsubscribe,logging/setLevel: recorded for cloudg, then always passed to the host when it has a handler.
List methods call the host first (when there is a host handler) and append cloudg's entries
in _merge. When the host's result has a nextCursor, cloudg returns it untouched, so
cloudg's entries end up after the host's last page. Entries whose key (name, URI or template)
the host already has are dropped with a warning.
Errors raised to the SDK are MCPError(code, message, data) on 2.x and
McpError(ErrorData(...)) on 1.x. NotFoundError from read_resource uses -32602 on
2026-07-28 requests and -32002 otherwise; InvalidArgumentsError maps to -32602; other layer
errors keep their own code.
Era and session bookkeeping: _is_modern(ctx) compares ctx.protocol_version with
mcp_types.version.MODERN_PROTOCOL_VERSIONS. _session_key(ctx) is the per-connection
object used for subscriptions, log levels and list-changed fan-out: on 2.x the private
ctx.session._connection for handshake-era requests and None for 2026 requests (which have
no connection to notify); on 1.x the ServerSession itself.
Principal: request_info(ctx) reads ctx.request (the Starlette request on HTTP) for
headers and the ASGI scope (where TokenAuthASGIMiddleware put the principal), asks
mcp.server.auth.middleware.auth_context.get_access_token() for an OAuth token, and reads
ctx.session.client_params.client_info. The transport is http when there is a request and
stdio otherwise.
Tool context: _tool_context(ctx, principal) builds a ToolContext whose progress callback
calls session.report_progress(...) on 2.x (the SDK sends nothing unless the client sent a
progress token) or session.send_progress_notification(token, ...) on 1.x (only with a
progressToken in ctx.meta, falling back to the old signature without message), and
whose log callback filters by the level set with logging/setLevel (default info) on
handshake-era sessions and calls session.send_log_message(...). On 2026 requests the SDK
itself drops log notifications unless the request asked for them.
Three further patches:
_find_bus()(2.x): useMCPServer._subscriptionswhen the owner has one, else the bus of an existingsubscriptions/listenhandler (handler._bus), else installListenHandler(InMemorySubscriptionBus())whensubscriptionsis included. Change events are published on it asToolsListChanged,PromptsListChanged,ResourcesListChangedandResourceUpdated(uri)._patch_initialization_options(): wrapsserver.create_initialization_optionsso that, when called withoutNotificationOptions, it advertiseslistChangedfor tools, resources and prompts. Applied once per server (_cloudg_init_patched)._patch_input_schema()(2.x): wrapsserver.get_tool_input_schemaso the SDK'sMcp-Param-*header validation sees cloudg's input schemas for cloudg tools.
The binding appends itself to server.__dict__["_cloudg_bindings"], which keeps it (and its
ChangeFanout) alive as long as the server.
5.3 mcp_sdk.py¶
is_sdk_highlevel_server(obj) checks the module path; inner_lowlevel_server(server)
returns server._lowlevel_server (mcp 2.x MCPServer) or server._mcp_server (mcp 1.x
FastMCP) and raises TypeError with a pointer to build_server when neither exists;
install applies LowLevelBinding to the inner server with owner=server, which is where
_find_bus looks for _subscriptions. The host's decorated tools live in the same low-level
handlers, which is why wrapping works without touching the high-level API.
create_lowlevel_server(layer, name=None) builds Server(name, version=..., instructions=...),
retrying without instructions for very old 1.x; create_highlevel_server builds MCPServer
on 2.x and FastMCP on 1.x.
5.4 fastmcp.py: FastMCPBinding¶
fastmcp keeps tools, resources, templates and prompts as component objects, and its own list handlers render them. The binding defines four subclasses at install time, closing over the binding:
| Class | Base | Override |
|---|---|---|
CloudGTool |
fastmcp.tools.Tool |
run(arguments) calls binding._run_tool(self.name, arguments) |
CloudGResource |
fastmcp.resources.Resource |
read() calls binding._read(uri) |
CloudGTemplate |
fastmcp.resources.ResourceTemplate |
matches(uri) uses cloudg's regex; create_resource returns a CloudGResource; read(arguments) expands the template and reads |
CloudGPrompt |
fastmcp.prompts.Prompt |
render(arguments) calls binding._render(name, arguments) |
_register instantiates one per spec with parameters set to the layer's inputSchema,
output_schema, annotations (as mcp.types.ToolAnnotations), title and meta when the
installed fastmcp's model has those fields, and tags {"cloudg", category, *tags}. Template
parameters are a synthetic object schema with one required string per variable. Tools and
prompts are registered under their exposed names, so set the prefix before installing (which
register_into does).
Tool results: fastmcp normally rebuilds the CallToolResult from its own ToolResult. To
keep cloudg's exact result (including isError with structured content and _meta), the
binding uses ToolResult.from_mcp_result on fastmcp 4 and, on older versions, a ToolResult
subclass whose to_mcp_result() returns the stored raw CallToolResult. The subclass is a
pydantic model with a private attribute where the base class is a pydantic model (fastmcp
3.4.8) and a plain subclass otherwise (2.14.7).
Resource reads return fastmcp 3/4's ResourceResult([ResourceContent(...)]) when available,
else the first content's text or bytes. Prompt renders return fastmcp's PromptResult and
Message on newer versions and a list of SDK PromptMessage on fastmcp 2.
Policy filtering: fastmcp lists every registered component to everyone, so
_install_middleware adds a fastmcp Middleware subclass (on_list_tools,
on_list_resources, on_list_resource_templates, on_list_prompts) that drops cloudg
components the caller may not see and swaps in the per-principal outputSchema. Calls are
still enforced by the layer when the middleware is missing (fastmcp older than 2.9).
Protocol features: completions and resource subscriptions cannot be expressed as fastmcp
components, so _install_lowlevel applies LowLevelBinding with
include={"completions", "subscriptions"} and list_changed=False to server._mcp_server,
and the binding then shares that binding's ChangeFanout. Verified state per version:
| fastmcp | mcp | inner binding sdk_major |
2026 listen bus | result class |
|---|---|---|---|---|
| 4.0.11 | 2.3.0 | 2 | yes | none (from_mcp_result) |
| 3.4.8 | 1.30.0 | 1 | no | RawToolResult (pydantic) |
| 2.14.7 | 1.30.0 | 1 | no | PlainRawToolResult |
Request facts come from fastmcp.server.dependencies.get_context(), get_http_request() and
get_access_token(). Errors: NotFoundError becomes fastmcp's NotFoundError, other layer
errors ToolError, ResourceError or PromptError (ToolError when the installed version
has no PromptError).
5.5 export.py¶
export_definitions(layer, principal) snapshots the four wire lists for the principal and
returns closures over layer. The handlers take (params, *, principal=None) so a gateway
can serve many principals from one export; tool_functions are bound to the export's
principal. The converters read layer.tools_wire(principal) and reshape it; they do not
call the layer.
5.6 ChangeFanout¶
Registers a weakly referenced listener on the layer. track(session) records a session
(weak set) and binds the fan-out to the running loop. on_change(kind, uri) schedules
_dispatch on that loop, directly when called on it and with run_coroutine_threadsafe
from other threads. _dispatch expands resources with a URI into a list change plus an
update, publishes each event on the 2026 bus when publish is set, and calls
notifier(session, kind, uri) for every tracked session, sending resource events only to
sessions subscribed to that URI. A session whose notification raises is dropped. Sessions,
subscriptions and log levels are all held in weak containers, so closed connections
disappear on their own.