Model Context Protocol (MCP) is great. Usage has exploded since its release, and I jumped in—reading the spec and contributing to the growing open source ecosystem.
Once I dug into the Python SDK, I struggled to map the spec’s clean concepts to the implementation.
Take an InitializeRequest
. The spec gives us this TypeScript:
1
2
3
4
5
6
7
8
|
export interface InitializeRequest extends Request {
method: "initialize";
params: {
protocolVersion: string;
capabilities: ClientCapabilities;
clientInfo: Implementation;
};
}
|
To understand the equivalent Python code, I had to trace through multiple abstraction layers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# Layer 1: The actual InitializeRequest
class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]]):
method: Literal["initialize"]
params: InitializeRequestParams
# Layer 2: The generic Request base class
RequestParamsT = TypeVar("RequestParamsT", bound=RequestParams | dict[str, Any] | None)
MethodT = TypeVar("MethodT", bound=str)
class Request(BaseModel, Generic[RequestParamsT, MethodT]):
method: MethodT
params: RequestParamsT
# Layer 3: The InitializeRequestParams
class InitializeRequestParams(RequestParams):
protocolVersion: str | int
capabilities: ClientCapabilities
clientInfo: Implementation
# Layer 4: The RequestParams base class
class RequestParams(BaseModel):
class Meta(BaseModel):
progressToken: ProgressToken | None = None
# ... more nested structure
meta: Meta | None = Field(alias="_meta", default=None)
|
And when I wanted to actually send an initialization request, all those layers created this construction pattern:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
result = await self.send_request(
types.ClientRequest(
types.InitializeRequest(
method="initialize",
params=types.InitializeRequestParams(
protocolVersion=types.LATEST_PROTOCOL_VERSION,
capabilities=types.ClientCapabilities(
sampling=sampling,
experimental=None,
roots=roots,
),
clientInfo=self._client_info,
),
)
),
types.InitializeResult,
)
|
I kept losing track of what lived where. The simple concept from the spec was buried under TypeVar
s, Generic
s, and nested parameter classes.
So I rewrote the types. Here’s my InitializeRequest
:
1
2
3
4
5
6
7
8
|
class InitializeRequest(Request):
"""
Initial request to establish an MCP connection.
"""
method: Literal["initialize"] = "initialize"
protocol_version: str = Field(default=PROTOCOL_VERSION, alias="protocolVersion")
client_info: Implementation = Field(alias="clientInfo")
capabilities: ClientCapabilities = Field(default_factory=ClientCapabilities)
|
Request
still matters, but there’s no inheritance maze. Everything’s visible, direct, and clearly named.
I also organized the types into focused modules instead of one 1,100-line file:
1
2
3
4
5
6
|
src/conduit/protocol/
├── base.py # Core protocol primitives
├── initialization.py # Connection setup
├── tools.py # Tool calling
├── resources.py # Resource access
└── ...
|
Now when I’m working on tool calling, I see only tool-related types. When I need initialization logic, it’s all in one place.
Next week: sessions.