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.
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):
# =============================================================================
# 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:
{
"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:
@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:
@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:
"""
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:
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:
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")
Your stdio_client.py is now complete. In the next step,
we'll build an interactive REPL to test the client.