Interactive Testing
Build a REPL for exploring and testing MCP servers.
Building an MCP REPL
A REPL (Read-Eval-Print Loop) is an interactive shell for exploring MCP servers. It's invaluable for testing and debugging. We'll build this in 3 sections.
An interactive command-line tool that connects to any STDIO MCP server and lets you explore its capabilities, call tools, read resources, and test prompts.
Section 1: REPL Command Structure
Create the REPL class with command registration and the main loop.
Create: mcp_repl.py
#!/usr/bin/env python3
"""
mcp_repl.py - Interactive MCP client REPL.
Commands:
help Show help message
info Show server information
tools List available tools
resources List available resources
prompts List available prompts
call [json] Call a tool with optional JSON arguments
read Read a resource
prompt [json] Get a prompt with optional arguments
raw [json] Send a raw JSON-RPC request
quit Exit the REPL
"""
import json
import sys
from typing import Any
from stdio_client import Tool, Resource, Prompt
# Type alias for the client (will be passed in)
MCPClient = Any
class MCPREPL:
"""Interactive REPL for MCP servers."""
def __init__(self, client: MCPClient):
self.client = client
self.commands = {
"help": self.cmd_help,
"tools": self.cmd_tools,
"resources": self.cmd_resources,
"prompts": self.cmd_prompts,
"call": self.cmd_call,
"read": self.cmd_read,
"prompt": self.cmd_prompt,
"raw": self.cmd_raw,
"info": self.cmd_info,
"quit": self.cmd_quit,
"exit": self.cmd_quit,
}
def run(self) -> None:
"""Start the REPL loop."""
print("\nMCP Interactive Client")
print("Type 'help' for commands, 'quit' to exit\n")
while True:
try:
line = input("mcp> ").strip()
if not line:
continue
# Split only to get command, preserve rest for JSON parsing
parts = line.split(None, 1)
cmd = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
if cmd in self.commands:
self.commands[cmd](rest)
else:
print(f"Unknown command: {cmd}")
print("Type 'help' for available commands")
except KeyboardInterrupt:
print("\nUse 'quit' to exit")
except EOFError:
break
except Exception as e:
print(f"Error: {e}")
def cmd_help(self, rest: str) -> None:
"""Show help message."""
print("""
Available commands:
help Show this help message
info Show server information
tools List available tools
resources List available resources
prompts List available prompts
call [json] Call a tool with optional JSON arguments
read Read a resource
prompt [json] Get a prompt with optional arguments
raw [json] Send a raw JSON-RPC request
quit Exit the REPL
""")
def cmd_info(self, rest: str) -> None:
"""Show server information."""
print(f"\nServer: {self.client.server_info.get('name', 'Unknown')}")
print(f"Version: {self.client.server_info.get('version', 'Unknown')}")
print(f"Protocol: {self.client.protocol_version}")
print(f"\nCapabilities:")
for cap, value in self.client.server_capabilities.items():
print(f" {cap}: {value}")
print()
def cmd_quit(self, rest: str) -> None:
"""Exit the REPL."""
print("Goodbye!")
sys.exit(0)
Section 2: List Commands
Add commands to list tools, resources, and prompts.
Edit: mcp_repl.py
Add the following methods before cmd_quit:
def cmd_tools(self, rest: str) -> None:
"""List available tools."""
if not self.client.has_tools():
print("Server does not support tools")
return
tools_data = self.client.list_tools()
if not tools_data:
print("No tools available")
return
tools = [Tool.from_dict(t) for t in tools_data]
print(f"\nAvailable tools ({len(tools)}):\n")
for tool in tools:
print(f" {tool.name}")
print(f" {tool.description}")
if tool.input_schema.get("properties"):
props = tool.input_schema["properties"]
required = tool.input_schema.get("required", [])
print(" Parameters:")
for name, schema in props.items():
req = "*" if name in required else ""
desc = schema.get("description", schema.get("type", ""))
print(f" {name}{req}: {desc}")
print()
def cmd_resources(self, rest: str) -> None:
"""List available resources."""
if not self.client.has_resources():
print("Server does not support resources")
return
resources_data = self.client.list_resources()
if not resources_data:
print("No resources available")
return
resources = [Resource.from_dict(r) for r in resources_data]
print(f"\nAvailable resources ({len(resources)}):\n")
for resource in resources:
print(f" {resource.uri}")
print(f" {resource.description}")
if resource.mime_type:
print(f" Type: {resource.mime_type}")
print()
def cmd_prompts(self, rest: str) -> None:
"""List available prompts."""
if not self.client.has_prompts():
print("Server does not support prompts")
return
prompts_data = self.client.list_prompts()
if not prompts_data:
print("No prompts available")
return
prompts = [Prompt.from_dict(p) for p in prompts_data]
print(f"\nAvailable prompts ({len(prompts)}):\n")
for prompt in prompts:
print(f" {prompt.name}")
print(f" {prompt.description}")
if prompt.arguments:
print(" Arguments:")
for arg in prompt.arguments:
req = "*" if arg.required else ""
print(f" {arg.name}{req}: {arg.description}")
print()
Section 3: Action Commands
Add commands to call tools, read resources, and get prompts.
Edit: mcp_repl.py
Add the following methods before cmd_quit:
def cmd_call(self, rest: str) -> None:
"""Call a tool."""
if not rest:
print("Usage: call [json_arguments]")
return
# Split into tool name and JSON
parts = rest.split(None, 1)
tool_name = parts[0]
arguments = {}
if len(parts) > 1:
try:
arguments = json.loads(parts[1])
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
return
print(f"\nCalling tool: {tool_name}")
print(f"Arguments: {json.dumps(arguments)}\n")
try:
result = self.client.call_tool(tool_name, arguments)
print("Result:")
for content in result:
if content.get("type") == "text":
print(content.get("text", ""))
else:
print(json.dumps(content, indent=2))
except Exception as e:
print(f"Error: {e}")
def cmd_read(self, rest: str) -> None:
"""Read a resource."""
if not rest:
print("Usage: read ")
return
uri = rest.split()[0]
print(f"\nReading resource: {uri}\n")
try:
result = self.client.read_resource(uri)
for content in result:
if content.get("text"):
print(content["text"])
elif content.get("blob"):
print(f"[Binary data: {len(content['blob'])} bytes]")
else:
print(json.dumps(content, indent=2))
except Exception as e:
print(f"Error: {e}")
def cmd_prompt(self, rest: str) -> None:
"""Get a prompt."""
if not rest:
print("Usage: prompt [json_arguments]")
return
# Split into prompt name and JSON
parts = rest.split(None, 1)
prompt_name = parts[0]
arguments = {}
if len(parts) > 1:
try:
arguments = json.loads(parts[1])
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
return
print(f"\nGetting prompt: {prompt_name}")
print(f"Arguments: {json.dumps(arguments)}\n")
try:
messages = self.client.get_prompt(prompt_name, arguments)
print("Messages:")
for msg in messages:
role = msg.get("role", "unknown")
content = msg.get("content", {})
if isinstance(content, dict):
text = content.get("text", str(content))
else:
text = str(content)
# Truncate long messages for display
display_text = text[:200] + "..." if len(text) > 200 else text
print(f" [{role}]: {display_text}")
except Exception as e:
print(f"Error: {e}")
def cmd_raw(self, rest: str) -> None:
"""Send a raw JSON-RPC request."""
if not rest:
print("Usage: raw [json_params]")
return
# Split into method and JSON
parts = rest.split(None, 1)
method = parts[0]
params = None
if len(parts) > 1:
try:
params = json.loads(parts[1])
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
return
print(f"\nSending: {method}")
if params:
print(f"Params: {json.dumps(params)}")
print()
try:
result = self.client.call(method, params)
print("Result:")
print(json.dumps(result, indent=2))
except Exception as e:
print(f"Error: {e}")
Section 4: Main Launcher
Create a launcher script to start the REPL with any server.
Create: main.py
#!/usr/bin/env python3
"""
main.py - Launch the MCP REPL.
Usage:
python main.py [args...]
Examples:
python main.py python my_server.py
python main.py ./calculator_server
"""
import sys
from stdio_client import StdioClient
from mcp_repl import MCPREPL
def main() -> None:
if len(sys.argv) < 2:
print("Usage: python main.py [args...]")
print("Example: python main.py python my_server.py")
sys.exit(1)
server_command = sys.argv[1:]
print(f"Starting server: {' '.join(server_command)}")
try:
with StdioClient(server_command) as client:
# Initialize connection
client.initialize()
print(f"Connected to {client.server_info.get('name', 'server')}")
# Start REPL
repl = MCPREPL(client)
repl.run()
except FileNotFoundError:
print(f"Error: Could not find command: {server_command[0]}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Try It
Test the REPL with one of your MCP servers from previous lessons.
Make sure you have both stdio_client.py (from earlier steps)
and the server you want to test.
# Test with your STDIO server from Lesson 7
python main.py python stdio_example.py
Example REPL Session
$ python main.py python stdio_example.py
Starting server: python stdio_example.py
Connected to mcp-server
MCP Interactive Client
Type 'help' for commands, 'quit' to exit
mcp> info
Server: mcp-server
Version: 1.0.0
Protocol: 2024-11-05
Capabilities:
tools: {}
mcp> tools
Available tools (3):
echo
Echo back a message
Parameters:
message*: Message to echo
get_time
Get current server time
add_numbers
Add two numbers
Parameters:
a*: First number
b*: Second number
mcp> call add_numbers {"a": 5, "b": 3}
Calling tool: add_numbers
Arguments: {"a": 5, "b": 3}
Result:
5 + 3 = 8
mcp> quit
Goodbye!
The REPL is excellent for debugging servers. Use the raw
command to send arbitrary JSON-RPC requests and see exactly what
the server returns.
What You've Built
Congratulations! You now have a complete MCP client that can:
- Launch and connect to STDIO-based MCP servers
- Perform the MCP initialization handshake
- Track requests and match responses
- List tools, resources, and prompts
- Call tools, read resources, and get prompts
- Interactively explore servers via REPL
What's Next?
In the next lesson, we'll integrate MCP servers with Claude Desktop, seeing how real AI applications use the protocol you've mastered.