Lesson 10 - Step 6 of 6

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.

What We're Building

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

python 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:

python mcp_repl.py - Add 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:

python mcp_repl.py - Add 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

python 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.

bash
# Test with your STDIO server from Lesson 7
python main.py python stdio_example.py

Example REPL Session

text
$ 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!
Debugging Tip

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.