DEV Community

ericode01
ericode01

Posted on

Model Context Protocol (MCP) deep dive with context of the Python SDK

I enjoy reading SDK implements to understand what’s actually going on under the hood. Reading through MCP Python SDK is really helpful to understand the MCP spec and to see an effective implementation of the spec. This aids in developing no matter new servers, transport layers or other components to meet specific requirements.

First, a 10,000 ft view on the architecture.
Image description

Server

Image description

Low-level server

Low-level server is where the protocol spec is implemented and where the server sessions are managed.

A server session can be created by calling server.run() and it takes two streams: read_stream and write_stream which are exposed by the transport layer. The transport layer interface with a server session only through these two streams, allowing loosely coupling of these two components. Therefore, you could create your own transport layer if you want as long as it would expose a read_stream and a write_stream. We will take a closer look at transport layer later.

Handling incoming message

An incoming message can be a ClientRequest, a ClientNotification or a ClientResult. Take ClientRequest as an example. A server session has a receive loop which continuously reads message from the read_stream. For each message, creates a RequestResponder and write it to the internal session message_stream. On the other of end of the internal session message_stream, for each message, a task is started to process the message. It looks up the corresponding MCP core handler by the type of the request from the registered MCP core handlers and then calls the handler to get the result. The RequestResponder will call session’s send_response which construct the JSONRPCResponse and send it over to the transport layer through the session’s write stream. An incoming ClientNotification is processed in a similar way. Notable differences are: 1. Looking up in registered notification handlers 2. No response sent.

/* Client messages */
export type ClientRequest =
  | PingRequest
  | InitializeRequest
  | CompleteRequest
  | SetLevelRequest
  | GetPromptRequest
  | ListPromptsRequest
  | ListResourcesRequest
  | ListResourceTemplatesRequest
  | ReadResourceRequest
  | SubscribeRequest
  | UnsubscribeRequest
  | CallToolRequest
  | ListToolsRequest;

export type ClientNotification =
  | CancelledNotification
  | ProgressNotification
  | InitializedNotification
  | RootsListChangedNotification;

export type ClientResult = EmptyResult | CreateMessageResult | ListRootsResult;
Enter fullscreen mode Exit fullscreen mode

High-level server

A high-level server (e.g. FastMcp in Python SDK) encapsulates the low-level server. It implements MCP core handlers and provides high-level functionalities, for example, tool management and resource management.

Taking tool use as an example, the ClientRequest seen by the low-level server are CallToolRequest or ListToolsRequest. The high-level server implements MCP core handlers for CallToolRequest and ListToolsRequest and registers them in the low-level server. The logic to triage and to invoke a specific tool for a CallToolRequest lies in the high-level server.

Client

Image description
A client can initiate multiple client sessions. Each client session communicates with a server session on the other side.

A Client session is capable of:

  1. Send InitializeRequest to a server session to exchange protocol version, capabilities, client/server info etc. This must be the first step after creating a client session.
  2. Send ClientRequest, ClientNotification or ClientResult.
  3. Receive ServerRequest, ServerNotification or ServerResult.

All outgoing messages are written to the transport layer exposed writer by a client session. All incoming messages are read from the transport layer exposed reader iteratively by a client session. A ServerResult is returned as the response for a particular ClientRequest based on the request ID in the message. For ServerRequest and ServerNotification, the corresponding handlers will be called which are registered at the creation time of a client session.

/* Server messages */
export type ServerRequest =
  | PingRequest
  | CreateMessageRequest
  | ListRootsRequest;

export type ServerNotification =
  | CancelledNotification
  | ProgressNotification
  | LoggingMessageNotification
  | ResourceUpdatedNotification
  | ResourceListChangedNotification
  | ToolListChangedNotification
  | PromptListChangedNotification;

export type ServerResult =
  | EmptyResult
  | InitializeResult
  | CompleteResult
  | GetPromptResult
  | ListPromptsResult
  | ListResourceTemplatesResult
  | ListResourcesResult
  | ReadResourceResult
  | CallToolResult
  | ListToolsResult;
Enter fullscreen mode Exit fullscreen mode

Transport layer

The client transport layer connects and talks to the server transport layer. Stdio is recommended for local client-server communication and SSE is recommended for remote communication by MCP. Python-SDK also provides WebSocket. You could also build your own transport layer using another protocol as long as it exposes the read and write interface to the client and server and you can easily plug it in.

Let’s take a closer look at how the two-recommended transport layer protocol stdio and SSE are implemented in Python SDK.

Stdio
Image description
Client and server stdio look mostly alike. One difference is that client stdio, as its first step, would run the server script path you passed in to start the server process.

SSE
Image description
A client SSE session, as its first step, would send a GET to establish the SSE connection with a server SSE session and to receive an endpoint URL which looks something like 127.0.0.1:8000/messages/?session_id=1. The client SSE session will send subsequent POST to the endpoint URL.

Top comments (0)

close