Add M7.2 MCP client support with McpStep, CLI commands, and stdio transport
This commit is contained in:
+77
-2
@@ -9,11 +9,12 @@ from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from .tool import Tool, PromptStep, CodeStep, ToolStep
|
||||
from .tool import Tool, PromptStep, CodeStep, ToolStep, McpStep
|
||||
from .providers import call_provider, mock_provider
|
||||
from .resolver import resolve_tool, ToolNotFoundError, ToolSpec, install_from_registry
|
||||
from .manifest import load_manifest
|
||||
from .profiles import load_profile
|
||||
from .mcp_client import McpClientManager
|
||||
|
||||
# Maximum recursion depth for nested tool calls
|
||||
MAX_TOOL_DEPTH = 10
|
||||
@@ -352,7 +353,9 @@ def substitute_variables(template: str, variables: dict, warn_non_scalar: bool =
|
||||
if base_name == "settings":
|
||||
# Settings dict - leave unchanged (handled specially)
|
||||
return match.group(0)
|
||||
# Replace with empty string for None, str() for other values
|
||||
if isinstance(base_value, (dict, list)):
|
||||
return json.dumps(base_value)
|
||||
# Replace with empty string for None, str() for other scalar values
|
||||
return "" if base_value is None else str(base_value)
|
||||
|
||||
# Handle nested access
|
||||
@@ -901,6 +904,9 @@ def run_tool(
|
||||
output = substitute_variables(tool.output, variables, warn_non_scalar=verbose)
|
||||
return output, 0
|
||||
|
||||
# Reuse MCP configuration and schema caches across all MCP steps in this run.
|
||||
mcp_manager = None
|
||||
|
||||
# Execute each step
|
||||
for i, step in enumerate(tool.steps):
|
||||
if verbose:
|
||||
@@ -910,6 +916,8 @@ def run_tool(
|
||||
step_type = "CODE"
|
||||
elif isinstance(step, ToolStep):
|
||||
step_type = f"TOOL({step.tool})"
|
||||
elif isinstance(step, McpStep):
|
||||
step_type = f"MCP({step.server}/{step.tool})"
|
||||
else:
|
||||
step_type = "UNKNOWN"
|
||||
print(f"[verbose] Step {i+1}: {step_type} -> {{{step.output_var}}}", file=sys.stderr)
|
||||
@@ -981,12 +989,79 @@ def run_tool(
|
||||
return "", 3
|
||||
variables[step.output_var] = output
|
||||
|
||||
elif isinstance(step, McpStep):
|
||||
if verbose or dry_run:
|
||||
print(f"=== MCP (Step {i+1}) -> {{{step.output_var}}} ===", file=sys.stderr)
|
||||
print(f" Server: {step.server} Tool: {step.tool}", file=sys.stderr)
|
||||
print(f" Args: {step.arguments}", file=sys.stderr)
|
||||
print("=== END MCP ===", file=sys.stderr)
|
||||
|
||||
if dry_run:
|
||||
variables[step.output_var] = f"[DRY RUN - would call mcp:{step.server}/{step.tool}]"
|
||||
else:
|
||||
args = _substitute_mcp_args(step.arguments, variables)
|
||||
try:
|
||||
if mcp_manager is None:
|
||||
mcp_manager = McpClientManager()
|
||||
result = mcp_manager.call_tool(
|
||||
step.server, step.tool, args,
|
||||
result_mode=step.result_mode,
|
||||
)
|
||||
variables[step.output_var] = result
|
||||
except ImportError as e:
|
||||
print(f"{e}", file=sys.stderr)
|
||||
return "", 4
|
||||
except Exception as e:
|
||||
print(f"MCP step failed (server={step.server}, tool={step.tool}): {e}", file=sys.stderr)
|
||||
return "", 4
|
||||
|
||||
# Generate final output
|
||||
output = substitute_variables(tool.output, variables, warn_non_scalar=verbose)
|
||||
|
||||
return output, 0
|
||||
|
||||
|
||||
def _substitute_mcp_args(arguments: dict, variables: dict) -> dict:
|
||||
"""Substitute variables in MCP arguments, preserving types."""
|
||||
result = {}
|
||||
for key, value in arguments.items():
|
||||
result[key] = _deep_substitute(value, variables)
|
||||
return result
|
||||
|
||||
|
||||
def _deep_substitute(value, variables: dict):
|
||||
if isinstance(value, str):
|
||||
import re
|
||||
|
||||
exact_reference = re.fullmatch(
|
||||
r"\s*\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}\s*",
|
||||
value,
|
||||
)
|
||||
if exact_reference:
|
||||
path = exact_reference.group(1).split(".")
|
||||
if path[0] in variables:
|
||||
resolved = variables[path[0]]
|
||||
found = True
|
||||
for key in path[1:]:
|
||||
if isinstance(resolved, dict) and key in resolved:
|
||||
resolved = resolved[key]
|
||||
elif hasattr(resolved, key):
|
||||
resolved = getattr(resolved, key)
|
||||
else:
|
||||
found = False
|
||||
break
|
||||
if found:
|
||||
return resolved
|
||||
|
||||
substituted = substitute_variables(value, variables)
|
||||
return substituted if substituted != value else value
|
||||
elif isinstance(value, dict):
|
||||
return {k: _deep_substitute(v, variables) for k, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
return [_deep_substitute(v, variables) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def create_argument_parser(tool: Tool) -> argparse.ArgumentParser:
|
||||
"""
|
||||
Create an argument parser for a tool.
|
||||
|
||||
Reference in New Issue
Block a user