|
| 1 | +#!/usr/bin/env python |
| 2 | +#!/usr/bin/env python |
| 3 | +""" |
| 4 | +mcp_stdio_example_calling_usage.py |
| 5 | +Demonstrates JSON / XML / function-call parsing, sequential & parallel execution, |
| 6 | +timeouts, and colourised results – but routing all calls to the MCP stdio echo server. |
| 7 | +""" |
| 8 | + |
| 9 | +import asyncio |
| 10 | +import json |
| 11 | +import os |
| 12 | +import sys |
| 13 | +from typing import Any, List |
| 14 | + |
| 15 | +from colorama import init as colorama_init, Fore, Style |
| 16 | +colorama_init(autoreset=True) |
| 17 | + |
| 18 | +# ----------------------------------------------------- # |
| 19 | +# Project imports # |
| 20 | +# ----------------------------------------------------- # |
| 21 | +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| 22 | + |
| 23 | +from chuk_tool_processor.logging import get_logger, log_context_span |
| 24 | +from chuk_tool_processor.mcp import setup_mcp_stdio |
| 25 | +from chuk_tool_processor.registry import ToolRegistryProvider |
| 26 | + |
| 27 | +from chuk_tool_processor.plugins.parsers.json_tool_plugin import JsonToolPlugin |
| 28 | +from chuk_tool_processor.plugins.parsers.xml_tool import XmlToolPlugin |
| 29 | +from chuk_tool_processor.plugins.parsers.function_call_tool_plugin import FunctionCallPlugin |
| 30 | +from chuk_tool_processor.models.tool_call import ToolCall |
| 31 | +from chuk_tool_processor.models.tool_result import ToolResult |
| 32 | + |
| 33 | +from chuk_tool_processor.execution.tool_executor import ToolExecutor |
| 34 | +from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy |
| 35 | + |
| 36 | +logger = get_logger("mcp-demo") |
| 37 | + |
| 38 | +# ----------------------------------------------------- # |
| 39 | +# MCP bootstrap # |
| 40 | +# ----------------------------------------------------- # |
| 41 | +CONFIG_FILE = "server_config.json" |
| 42 | +ECHO_SERVER = "echo" |
| 43 | + |
| 44 | +async def bootstrap_mcp() -> None: |
| 45 | + """Ensure the stdio echo server is configured, started and its tools registered.""" |
| 46 | + if not os.path.exists(CONFIG_FILE): |
| 47 | + with open(CONFIG_FILE, "w") as fh: |
| 48 | + json.dump( |
| 49 | + { |
| 50 | + "mcpServers": { |
| 51 | + ECHO_SERVER: { |
| 52 | + "command": "uv", |
| 53 | + "args": [ |
| 54 | + "--directory", |
| 55 | + "/Users/you/path/to/chuk-mcp-echo-server", |
| 56 | + "run", |
| 57 | + "src/chuk_mcp_echo_server/main.py", |
| 58 | + ], |
| 59 | + } |
| 60 | + } |
| 61 | + }, |
| 62 | + fh, |
| 63 | + ) |
| 64 | + logger.info("Created %s", CONFIG_FILE) |
| 65 | + |
| 66 | + processor, sm = await setup_mcp_stdio( |
| 67 | + config_file=CONFIG_FILE, |
| 68 | + servers=[ECHO_SERVER], |
| 69 | + server_names={0: ECHO_SERVER}, |
| 70 | + namespace="stdio", |
| 71 | + ) |
| 72 | + bootstrap_mcp.stream_manager = sm # type: ignore[attr-defined] |
| 73 | + |
| 74 | +# ----------------------------------------------------- # |
| 75 | +# Parsers / test payloads # |
| 76 | +# ----------------------------------------------------- # |
| 77 | +plugins = [ |
| 78 | + ( |
| 79 | + "JSON Plugin", |
| 80 | + JsonToolPlugin(), |
| 81 | + json.dumps( |
| 82 | + {"tool_calls": [{"tool": "stdio.echo", "arguments": {"message": "Hello JSON"}}]} |
| 83 | + ), |
| 84 | + ), |
| 85 | + ( |
| 86 | + "XML Plugin", |
| 87 | + XmlToolPlugin(), |
| 88 | + '<tool name="stdio.echo" args=\'{"message": "Hello XML"}\'/>', |
| 89 | + ), |
| 90 | + ( |
| 91 | + "FunctionCall Plugin", |
| 92 | + FunctionCallPlugin(), |
| 93 | + json.dumps( |
| 94 | + { |
| 95 | + "function_call": { |
| 96 | + "name": "stdio.echo", |
| 97 | + "arguments": {"message": "Hello FunctionCall"}, |
| 98 | + } |
| 99 | + } |
| 100 | + ), |
| 101 | + ), |
| 102 | +] |
| 103 | + |
| 104 | +# ----------------------------------------------------- # |
| 105 | +# Pretty-print helper # |
| 106 | +# ----------------------------------------------------- # |
| 107 | +def print_results(title: str, calls: List[ToolCall], results: List[ToolResult]) -> None: |
| 108 | + print(Fore.CYAN + f"\n=== {title} ===") |
| 109 | + for call, r in zip(calls, results): |
| 110 | + duration = (r.end_time - r.start_time).total_seconds() |
| 111 | + hdr = (Fore.GREEN if not r.error else Fore.RED) + f"{r.tool} ({duration:.3f}s) [pid:{r.pid}]" |
| 112 | + print(hdr + Style.RESET_ALL) |
| 113 | + print(f" {Fore.YELLOW}Args:{Style.RESET_ALL} {call.arguments}") |
| 114 | + if r.error: |
| 115 | + print(f" {Fore.RED}Error:{Style.RESET_ALL} {r.error!r}") |
| 116 | + else: |
| 117 | + print(f" {Fore.MAGENTA}Result:{Style.RESET_ALL} {r.result!r}") |
| 118 | + print(f" Started: {r.start_time.isoformat()}") |
| 119 | + print(f" Finished:{r.end_time.isoformat()}") |
| 120 | + print(f" Host: {r.machine}") |
| 121 | + print(Style.DIM + "-" * 60) |
| 122 | + |
| 123 | +# ----------------------------------------------------- # |
| 124 | +# Demo runner # |
| 125 | +# ----------------------------------------------------- # |
| 126 | +async def run_demo() -> None: |
| 127 | + print(Fore.GREEN + "=== MCP Tool-Calling Demo ===") |
| 128 | + await bootstrap_mcp() |
| 129 | + |
| 130 | + registry = ToolRegistryProvider.get_registry() |
| 131 | + executor = ToolExecutor( |
| 132 | + registry, |
| 133 | + strategy=InProcessStrategy(registry, default_timeout=2.0, max_concurrency=4), |
| 134 | + ) |
| 135 | + |
| 136 | + # sequential tests |
| 137 | + for title, plugin, raw in plugins: |
| 138 | + calls = plugin.try_parse(raw) |
| 139 | + results = await executor.execute(calls) |
| 140 | + print_results(f"{title} (sequential)", calls, results) |
| 141 | + |
| 142 | + # parallel echo spam |
| 143 | + print(Fore.CYAN + "\n=== Parallel Echo Tasks ===") |
| 144 | + parallel_calls = [ |
| 145 | + ToolCall(tool="stdio.echo", arguments={"message": f"parallel-{i}"}) for i in range(5) |
| 146 | + ] |
| 147 | + tasks = [asyncio.create_task(executor.execute([c]), name=f"echo-{i}") for i, c in enumerate(parallel_calls)] |
| 148 | + for call, task in zip(parallel_calls, tasks): |
| 149 | + try: |
| 150 | + res = await asyncio.wait_for(task, timeout=3.0) |
| 151 | + print_results("Parallel echo", [call], res) |
| 152 | + except asyncio.TimeoutError: |
| 153 | + print(Fore.RED + f"Task {task.get_name()} timed out") |
| 154 | + |
| 155 | + await bootstrap_mcp.stream_manager.close() # type: ignore[attr-defined] |
| 156 | + |
| 157 | +# ----------------------------------------------------- # |
| 158 | +# Entry-point # |
| 159 | +# ----------------------------------------------------- # |
| 160 | +if __name__ == "__main__": |
| 161 | + import logging |
| 162 | + logging.getLogger("chuk_tool_processor").setLevel( |
| 163 | + getattr(logging, os.environ.get("LOGLEVEL", "INFO").upper()) |
| 164 | + ) |
| 165 | + asyncio.run(run_demo()) |
0 commit comments