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 TypeVars, Generics, 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.