Lesson 10 - Step 5 of 6

Supporting Classes

Add dataclasses for tools, resources, and prompts.

Now we'll add supporting dataclasses that make working with MCP primitives easier. These go after the StdioClient class - just append to the end of the file.

Appending to the File

All code in this step goes at the end of the file, after the StdioClient class. You're always appending, never going back into the class.

Step 1: Add the Tool Dataclass

First, let's create a dataclass to represent MCP tools with their metadata.

Edit: stdio_client.py

Append this to the end of the file (after the StdioClient class):

python stdio_client.py - Append to end of file

# =============================================================================
# Dataclasses for MCP primitives
# =============================================================================

@dataclass
class Tool:
    """Represents an MCP tool."""
    name: str
    description: str
    input_schema: dict

    @classmethod
    def from_dict(cls, data: dict) -> "Tool":
        return cls(
            name=data["name"],
            description=data.get("description", ""),
            input_schema=data.get("inputSchema", {})
        )

Tool Response Example

When you call tools/list, the server returns data like this:

json
{
    "tools": [
        {
            "name": "add_numbers",
            "description": "Add two numbers",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "a": {"type": "number", "description": "First number"},
                    "b": {"type": "number", "description": "Second number"}
                },
                "required": ["a", "b"]
            }
        }
    ]
}

Step 2: Add the Resource Dataclass

Resources represent data that the server can provide.

Edit: stdio_client.py

Continue appending to the end of the file:

python stdio_client.py - Append to end of file
@dataclass
class Resource:
    """Represents an MCP resource."""
    uri: str
    name: str
    description: str
    mime_type: Optional[str] = None

    @classmethod
    def from_dict(cls, data: dict) -> "Resource":
        return cls(
            uri=data["uri"],
            name=data.get("name", data["uri"]),
            description=data.get("description", ""),
            mime_type=data.get("mimeType")
        )

Step 3: Add the Prompt Dataclasses

Prompts are templates that can be filled with arguments. We need two dataclasses: one for arguments and one for the prompt itself.

Edit: stdio_client.py

Continue appending to the end of the file:

python stdio_client.py - Append to end of file
@dataclass
class PromptArgument:
    """Represents a prompt argument."""
    name: str
    description: str
    required: bool = False

    @classmethod
    def from_dict(cls, data: dict) -> "PromptArgument":
        return cls(
            name=data["name"],
            description=data.get("description", ""),
            required=data.get("required", False)
        )


@dataclass
class Prompt:
    """Represents an MCP prompt."""
    name: str
    description: str
    arguments: List[PromptArgument]

    @classmethod
    def from_dict(cls, data: dict) -> "Prompt":
        args = data.get("arguments", [])
        return cls(
            name=data["name"],
            description=data.get("description", ""),
            arguments=[PromptArgument.from_dict(a) for a in args]
        )

Complete File Structure

Your stdio_client.py should now have this structure:

python
"""
stdio_client.py - Basic STDIO MCP client.
"""

import subprocess
import json
from dataclasses import dataclass
from typing import Optional, List, Any


class StdioClient:
    """MCP client that communicates via STDIO."""

    # ... all the methods we added ...
    # __init__, connect, disconnect, send_request, send_notification
    # __enter__, __exit__, initialize
    # has_tools, has_resources, has_prompts, supports_resource_subscriptions
    # call, call_tool, read_resource, get_prompt
    # list_tools, list_resources, list_prompts


# =============================================================================
# Dataclasses for MCP primitives
# =============================================================================

@dataclass
class Tool:
    ...

@dataclass
class Resource:
    ...

@dataclass
class PromptArgument:
    ...

@dataclass
class Prompt:
    ...

Using the Dataclasses

You can convert the raw dictionaries from list_tools() etc. into these dataclasses for easier access:

python
with StdioClient(["python", "my_server.py"]) as client:
    client.initialize()

    # Get tools as dataclass objects
    tools_data = client.list_tools()
    tools = [Tool.from_dict(t) for t in tools_data]

    for tool in tools:
        print(f"Tool: {tool.name}")
        print(f"  Description: {tool.description}")
        print(f"  Parameters: {tool.input_schema}")

Helper Function

Here's a utility function to display everything a server offers:

python
def print_server_inventory(client):
    """Print a summary of what the server offers."""
    print(f"\n{'='*60}")
    print(f"Server: {client.server_info.get('name', 'Unknown')}")
    print(f"Version: {client.server_info.get('version', 'Unknown')}")
    print(f"{'='*60}\n")

    # Tools
    if client.has_tools():
        tools = [Tool.from_dict(t) for t in client.list_tools()]
        print(f"TOOLS ({len(tools)}):")
        for tool in tools:
            print(f"  - {tool.name}: {tool.description}")
        print()
    else:
        print("TOOLS: None\n")

    # Resources
    if client.has_resources():
        resources = [Resource.from_dict(r) for r in client.list_resources()]
        print(f"RESOURCES ({len(resources)}):")
        for resource in resources:
            print(f"  - {resource.uri}")
            print(f"    {resource.description}")
        print()
    else:
        print("RESOURCES: None\n")

    # Prompts
    if client.has_prompts():
        prompts = [Prompt.from_dict(p) for p in client.list_prompts()]
        print(f"PROMPTS ({len(prompts)}):")
        for prompt in prompts:
            args = ", ".join(a.name for a in prompt.arguments)
            print(f"  - {prompt.name}({args})")
            print(f"    {prompt.description}")
        print()
    else:
        print("PROMPTS: None\n")
File Complete!

Your stdio_client.py is now complete. In the next step, we'll build an interactive REPL to test the client.