Skip to content

Commit 7a61b5c

Browse files
committed
mcp stdio support
1 parent 3665894 commit 7a61b5c

15 files changed

+1281
-1
lines changed

examples/mcp_stdio_example.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# examples/mcp_stdio_example.py
2+
"""
3+
Example demonstrating flexible MCP integration with support for multiple transport types.
4+
"""
5+
import asyncio
6+
import os
7+
import sys
8+
import json
9+
from typing import Dict, List
10+
11+
# Add project root to path if running as script
12+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13+
14+
# CHUK Tool Processor imports
15+
from chuk_tool_processor.mcp import setup_mcp_stdio, setup_mcp_sse
16+
from chuk_tool_processor.registry import ToolRegistryProvider
17+
18+
async def stdio_example():
19+
"""Example using stdio transport."""
20+
print("\n=== MCP with Stdio Transport ===")
21+
22+
# Server configuration
23+
config_file = "server_config.json"
24+
25+
# Create or update config file for the example
26+
if not os.path.exists(config_file):
27+
server_config = {
28+
"mcpServers": {
29+
"echo": {
30+
"command": "uv",
31+
"args": ["--directory", "/Users/christopherhay/chris-source/agent-x/mcp-servers/chuk-mcp-echo-server", "run", "src/chuk_mcp_echo_server/main.py"]
32+
}
33+
}
34+
}
35+
36+
with open(config_file, "w") as f:
37+
json.dump(server_config, f)
38+
print(f"Created config file: {config_file}")
39+
else:
40+
print(f"Using existing config file: {config_file}")
41+
42+
servers = ["echo"]
43+
server_names = {0: "echo"}
44+
45+
try:
46+
processor, stream_manager = await setup_mcp_stdio(
47+
config_file=config_file,
48+
servers=servers,
49+
server_names=server_names,
50+
namespace="stdio"
51+
)
52+
53+
registry = ToolRegistryProvider.get_registry()
54+
tools = [t for t in registry.list_tools() if t[0] == "stdio"]
55+
print(f"Registered stdio tools ({len(tools)}):")
56+
for namespace, name in tools:
57+
metadata = registry.get_metadata(name, namespace)
58+
description = metadata.description if metadata else "No description"
59+
print(f" - {namespace}.{name}: {description}")
60+
61+
# Example LLM text with tool calls - use fully-qualified default namespace name
62+
llm_text = """
63+
I'll echo your message using stdio transport.
64+
65+
<tool name=\"stdio.echo\" args='{"message": "Hello from stdio transport!"}'/>
66+
"""
67+
68+
print("\nProcessing LLM text...")
69+
results = await processor.process_text(llm_text)
70+
71+
if results:
72+
print("\nResults:")
73+
for result in results:
74+
print(f"Tool: {result.tool}")
75+
if result.error:
76+
print(f" Error: {result.error}")
77+
else:
78+
print(f" Result: {json.dumps(result.result, indent=2) if isinstance(result.result, dict) else result.result}")
79+
print(f" Duration: {(result.end_time - result.start_time).total_seconds():.3f}s")
80+
else:
81+
print("\nNo tool calls found or executed.")
82+
83+
await stream_manager.close()
84+
85+
except Exception as e:
86+
print(f"Error in stdio example: {e}")
87+
import traceback
88+
traceback.print_exc()
89+
90+
async def sse_example():
91+
"""Example using SSE transport."""
92+
print("\n=== MCP with SSE Transport ===")
93+
94+
sse_servers = [
95+
{
96+
"name": "weather",
97+
"url": "https://api.example.com/sse/weather",
98+
"api_key": "your_api_key_here"
99+
}
100+
]
101+
server_names = {0: "weather"}
102+
103+
try:
104+
processor, stream_manager = await setup_mcp_sse(
105+
servers=sse_servers,
106+
server_names=server_names,
107+
namespace="sse"
108+
)
109+
110+
registry = ToolRegistryProvider.get_registry()
111+
tools = [t for t in registry.list_tools() if t[0] == "sse"]
112+
print(f"Registered SSE tools ({len(tools)}):")
113+
for namespace, name in tools:
114+
metadata = registry.get_metadata(name, namespace)
115+
description = metadata.description if metadata else "No description"
116+
print(f" - {namespace}.{name}: {description}")
117+
118+
print("\nNote: SSE transport is currently a placeholder implementation.")
119+
120+
await stream_manager.close()
121+
122+
except Exception as e:
123+
print(f"Error in SSE example: {e}")
124+
import traceback
125+
traceback.print_exc()
126+
127+
async def main():
128+
"""Run the examples."""
129+
print("\n=== Flexible MCP Integration Example ===")
130+
await stdio_example()
131+
#await sse_example()
132+
133+
registry = ToolRegistryProvider.get_registry()
134+
all_tools = registry.list_tools()
135+
136+
print("\n=== All Registered Tools ===")
137+
print(f"Total tools: {len(all_tools)}")
138+
139+
by_namespace = {}
140+
for namespace, name in all_tools:
141+
by_namespace.setdefault(namespace, []).append(name)
142+
143+
for namespace, tools in by_namespace.items():
144+
print(f"\nNamespace: {namespace} ({len(tools)} tools)")
145+
for name in tools:
146+
metadata = registry.get_metadata(name, namespace)
147+
description = metadata.description if metadata else "No description"
148+
print(f" - {name}: {description}")
149+
150+
if __name__ == "__main__":
151+
asyncio.run(main())
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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())

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ description = "Add your description here"
99
readme = "README.md"
1010
requires-python = ">=3.11"
1111
dependencies = [
12+
"chuk-mcp>=0.1.12",
1213
"dotenv>=0.9.9",
1314
"openai>=1.76.0",
1415
"pydantic>=2.11.3",
@@ -32,4 +33,5 @@ asyncio_mode = "strict"
3233
dev = [
3334
"pytest-asyncio>=0.26.0",
3435
"pytest>=8.3.5",
36+
"colorama>=0.4.6",
3537
]
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
# chuk_tool_processor/__init__.py
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# chuk_tool_processor/mcp/__init__.py
2+
"""
3+
MCP integration for CHUK Tool Processor.
4+
"""
5+
from chuk_tool_processor.mcp.transport import MCPBaseTransport, StdioTransport, SSETransport
6+
from chuk_tool_processor.mcp.stream_manager import StreamManager
7+
from chuk_tool_processor.mcp.mcp_tool import MCPTool
8+
from chuk_tool_processor.mcp.register_mcp_tools import register_mcp_tools
9+
from chuk_tool_processor.mcp.setup_mcp_stdio import setup_mcp_stdio
10+
from chuk_tool_processor.mcp.setup_mcp_sse import setup_mcp_sse
11+
12+
__all__ = [
13+
"MCPBaseTransport",
14+
"StdioTransport",
15+
"SSETransport",
16+
"StreamManager",
17+
"MCPTool",
18+
"register_mcp_tools",
19+
"setup_mcp_stdio",
20+
"setup_mcp_sse"
21+
]

0 commit comments

Comments
 (0)