Skip to content

Type system complexity creating developer friction and maintenance overhead #837

Open
@davenpi

Description

@davenpi

Context

I've been working with the Python SDK and noticed that the type system has quite a bit of abstraction complexity compared to the underlying TypeScript schema. I'm curious about the design decisions here and wondering if there might be opportunities to simplify.

The current implementation is comprehensive and handles edge cases well, but I'm finding that simple operations require understanding multiple abstraction layers, which creates friction when reading and debugging code.

Example: Current complexity vs. original schema

Here's how a progress notification is currently constructed:

# server/session.py
async def send_progress_notification(
    self,
    progress_token: str | int,
    progress: float,
    total: float | None = None,
    message: str | None = None,
    related_request_id: str | None = None,
) -> None:
    """Send a progress notification."""
    await self.send_notification(
        types.ServerNotification(
            types.ProgressNotification(
                method="notifications/progress",
                params=types.ProgressNotificationParams(
                    progressToken=progress_token,
                    progress=progress,
                    total=total,
                    message=message,
                ),
            )
        ),
        related_request_id,
    )

This requires understanding several layers of abstractions in the type system. To send a progress notification, you're working through this chain:

# 1. Base parameter class
class NotificationParams(BaseModel):
    class Meta(BaseModel):
        model_config = ConfigDict(extra="allow")
    meta: Meta | None = Field(alias="_meta", default=None)

# 2. Notification parameter type variables
NotificationParamsT = TypeVar(
    "NotificationParamsT", bound=NotificationParams | dict[str, Any] | None
)

# 3. Generic notification base
class Notification(BaseModel, Generic[NotificationParamsT, MethodT]):
    """Base class for JSON-RPC notifications."""
    method: MethodT
    params: NotificationParamsT
    model_config = ConfigDict(extra="allow")

# 4. Specific parameter class
class ProgressNotificationParams(NotificationParams):
    """Parameters for progress notifications."""
    progressToken: ProgressToken
    progress: float
    total: float | None = None
    message: str | None = None
    model_config = ConfigDict(extra="allow")

# 5. Specific notification class
class ProgressNotification(
    Notification[ProgressNotificationParams, Literal["notifications/progress"]]
):
    """An out-of-band notification used to inform the receiver of a progress update."""
    method: Literal["notifications/progress"]
    params: ProgressNotificationParams

# 6. Plus ServerNotification wrapper in actual usage

That's 5-6 levels of abstraction for what seems fairly simple. For example, the TypeScript schema give us:

export interface Notification {
  method: string;
  params?: {
    _meta?: { [key: string]: unknown };
    [key: string]: unknown;
  };
}

export interface ProgressNotification extends Notification {
  method: "notifications/progress";
  params: {
    progressToken: ProgressToken;
    progress: number;
    total?: number;
    message?: string;
  };
}

In general the Python version introduces several abstraction layers:

  • Generic base classes with type variables (Request[RequestParamsT, MethodT])
  • Nested parameter classes (ProgressNotificationParams)
  • Multiple wrapper types (ServerNotification(ProgressNotification(...)))

Questions

Am I missing some context about why these abstractions were chosen? Were there specific requirements that drove this design?

I'm wondering if we could explore simplifying the type system to more closely match the directness of the original TypeScript schema. This might:

  • Reduce cognitive load for developers working with the SDK
  • Make debugging easier (fewer abstraction layers to trace through)
  • Reduce maintenance burden when the protocol evolves
  • Make the codebase more approachable for new contributors

Potential approach

Happy to prototype a simplified version of a few message types (like PingRequest and ProgressNotification) to demonstrate what this might look like.

Thoughts?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions