From 2d9da2ac1eae46aa7ee0eec09c85fc588acbed17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:59:44 +0000 Subject: [PATCH 1/4] Initial plan From 04e7b741b5d8aca194e1cd2ac41729867b77feec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:08:11 +0000 Subject: [PATCH 2/4] Add executeSeparated method for dynamic tool execution Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- docs/api_reference.md | 189 +++++++++++ src/mcipy/client.py | 88 +++++ src/mcipy/tool_manager.py | 33 ++ tests/unit/test_execute_separated.py | 468 +++++++++++++++++++++++++++ 4 files changed, 778 insertions(+) create mode 100644 tests/unit/test_execute_separated.py diff --git a/docs/api_reference.md b/docs/api_reference.md index d9da443..efae8c5 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -14,6 +14,7 @@ This document provides a comprehensive API reference for the Python MCI Adapter - [withoutTags()](#withouttags) - [toolsets()](#toolsets) - [execute()](#execute) + - [executeSeparated()](#executeseparated) - [list_tools()](#list_tools) - [get_tool_schema()](#get_tool_schema) - [Data Models](#data-models) @@ -677,6 +678,194 @@ ExecutionResult( --- +#### `executeSeparated()` + +Execute a Tool model instance directly with the provided properties and environment variables. + +This method allows executing a Tool model instance directly without requiring it to be registered in the tool registry. It uses the same execution engine (templating, validation, executor selection) as the regular `execute()` method but bypasses tool name lookup and filtering. + +**Use Cases:** +- Debugging/testing specific Tool models in isolation +- Validating behavior with different props/env_vars without modifying global context +- Dynamic tool execution (e.g., tools loaded from database or constructed on-the-fly) +- Integration tests and IDE plugins + +**Note:** This method does not modify the client's tool registry or schema. The tool is executed in isolation and is not persisted. + +**Method Signature:** + +```python +def executeSeparated( + self, + tool: Tool, + properties: dict[str, Any] | None = None, + env_vars: dict[str, Any] | None = None, + validating: bool = False, +) -> ExecutionResult +``` + +**Parameters:** + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `tool` | `Tool` | Yes | Tool model instance to execute (must have valid execution config) | +| `properties` | `dict[str, Any]` | No | Properties/parameters to pass to the tool (default: `{}`) | +| `env_vars` | `dict[str, Any]` | No | Environment variables for template context. If `None`, uses the client's environment variables. (default: `None`) | +| `validating` | `bool` | No | If `True`, execution is blocked (same as client-level validating mode). This parameter allows overriding the client's validating state for this specific execution. (default: `False`) | + +**Returns:** + +| Type | Description | +|------|-------------| +| `ExecutionResult` | Result object with success/error status and content | + +**Raises:** + +- `MCIClientError` - If tool model is invalid or execution fails with validation error +- `MCIClientError` - If validating mode is enabled (client-level or method-level) + +**Example 1: Execute a dynamically created tool** + +```python +from mcipy import MCIClient +from mcipy.models import Tool, TextExecutionConfig + +client = MCIClient(schema_file_path="example.mci.json") + +# Create a tool model dynamically +tool = Tool( + name="dynamic_greeting", + description="Generate a greeting message", + execution=TextExecutionConfig( + type="text", + text="Hello {{props.name}} from {{env.LOCATION}}!" + ) +) + +# Execute the tool directly +result = client.executeSeparated( + tool=tool, + properties={"name": "Alice"}, + env_vars={"LOCATION": "San Francisco"} +) + +print(result.result.content[0].text) +# Output: "Hello Alice from San Francisco!" +``` + +**Example 2: Test a tool with different configurations** + +```python +from mcipy import MCIClient +from mcipy.models import Tool, HTTPExecutionConfig + +client = MCIClient( + schema_file_path="example.mci.json", + env_vars={"API_KEY": "default-key"} +) + +# Create a tool for testing +tool = Tool( + name="test_api", + execution=HTTPExecutionConfig( + type="http", + method="GET", + url="https://api.example.com/data", + params={"id": "{{props.item_id}}"}, + headers={"Authorization": "Bearer {{env.API_KEY}}"} + ) +) + +# Test with different API keys without modifying client +result1 = client.executeSeparated( + tool=tool, + properties={"item_id": "123"}, + env_vars={"API_KEY": "test-key-1"} +) + +result2 = client.executeSeparated( + tool=tool, + properties={"item_id": "456"}, + env_vars={"API_KEY": "test-key-2"} +) + +# Or use client's default env_vars +result3 = client.executeSeparated( + tool=tool, + properties={"item_id": "789"}, + env_vars=None # Uses client's API_KEY +) +``` + +**Example 3: Debug a tool loaded from database** + +```python +from mcipy import MCIClient +from mcipy.models import Tool, CLIExecutionConfig + +client = MCIClient(schema_file_path="example.mci.json") + +# Simulate loading a tool from a database +# (In reality, you'd deserialize from JSON/database) +db_tool = Tool( + name="db_backup", + description="Backup database", + execution=CLIExecutionConfig( + type="cli", + command="pg_dump", + args=["-U", "{{props.username}}", "{{props.database}}"] + ) +) + +# Test the tool before saving to production +result = client.executeSeparated( + tool=db_tool, + properties={"username": "admin", "database": "testdb"} +) + +if result.result.isError: + print(f"Tool failed validation: {result.result.content[0].text}") +else: + print("Tool works correctly, safe to save to production") +``` + +**Success Response:** + +```python +ExecutionResult( + result=ExecutionResultContent( + isError=False, + content=[ + TextContent(type="text", text="Hello Alice from San Francisco!") + ], + metadata=None + ) +) +``` + +**Error Response - Validating Mode:** + +```python +# Raises MCIClientError +MCIClientError: Tool execution is disabled in validating mode. Set validating=False to execute tools. +``` + +**Error Response - Execution Error:** + +```python +ExecutionResult( + result=ExecutionResultContent( + isError=True, + content=[ + TextContent(type="text", text="Command failed: pg_dump: command not found") + ], + metadata=None + ) +) +``` + +--- + #### `list_tools()` List available tool names as strings. diff --git a/src/mcipy/client.py b/src/mcipy/client.py index e0eacf3..423344d 100644 --- a/src/mcipy/client.py +++ b/src/mcipy/client.py @@ -230,6 +230,94 @@ def execute(self, tool_name: str, properties: dict[str, Any] | None = None) -> E # Convert ToolManagerError to MCIClientError for consistent API raise MCIClientError(str(e)) from e + def executeSeparated( + self, + tool: Tool, + properties: dict[str, Any] | None = None, + env_vars: dict[str, Any] | None = None, + validating: bool = False, + ) -> ExecutionResult: + """ + Execute a tool model directly with the provided properties and environment variables. + + This method allows executing a Tool model instance directly without requiring it + to be registered in the tool registry. It uses the same execution engine + (templating, validation, executor selection) as the regular execute() method + but bypasses tool name lookup and filtering. + + This is useful for: + - Debugging/testing specific Tool models in isolation + - Validating behavior with different props/env_vars without modifying global context + - Dynamic tool execution (e.g., tools loaded from database or constructed on-the-fly) + - Integration tests and IDE plugins + + Note: This method does not modify the client's tool registry or schema. + The tool is executed in isolation and is not persisted. + + Args: + tool: Tool model instance to execute (must have valid execution config) + properties: Properties/parameters to pass to the tool (default: empty dict) + env_vars: Environment variables for template context (default: empty dict). + If None, uses the client's environment variables. + validating: If True, execution is blocked (same as client-level validating mode). + This parameter allows overriding the client's validating state for + this specific execution. (default: False) + + Returns: + ExecutionResult with success/error status and content + + Raises: + MCIClientError: If tool model is invalid, missing execution config, + or execution fails with validation error, + or if validating mode is enabled (client-level or method-level) + + Example: + ```python + from mcipy import MCIClient + from mcipy.models import Tool, TextExecutionConfig + + # Create a tool model dynamically + tool = Tool( + name="dynamic_greeting", + description="Generate a greeting message", + execution=TextExecutionConfig( + type="text", + text="Hello {{props.name}} from {{env.LOCATION}}!" + ) + ) + + # Execute the tool directly + client = MCIClient(schema_file_path="example.mci.json") + result = client.executeSeparated( + tool=tool, + properties={"name": "Alice"}, + env_vars={"LOCATION": "San Francisco"} + ) + print(result.result.content[0].text) + # Output: "Hello Alice from San Francisco!" + ``` + """ + # Check validating mode - either client-level or method-level + if self._validating or validating: + raise MCIClientError( + "Tool execution is disabled in validating mode. " + "Set validating=False to execute tools." + ) + + # Use client's env_vars if not provided + if env_vars is None: + env_vars = self._env_vars + + try: + return self._tool_manager.execute_tool_model( + tool=tool, + properties=properties, + env_vars=env_vars, + ) + except ToolManagerError as e: + # Convert ToolManagerError to MCIClientError for consistent API + raise MCIClientError(str(e)) from e + def list_tools(self) -> list[str]: """ List available tool names (excluding disabled tools). diff --git a/src/mcipy/tool_manager.py b/src/mcipy/tool_manager.py index 5b8bb01..62e01c6 100644 --- a/src/mcipy/tool_manager.py +++ b/src/mcipy/tool_manager.py @@ -222,6 +222,39 @@ def execute( if tool is None: raise ToolManagerError(f"Tool not found: {tool_name}") + # Delegate to execute_tool_model for actual execution + return self.execute_tool_model(tool=tool, properties=properties, env_vars=env_vars) + + def execute_tool_model( + self, + tool: Tool, + properties: dict[str, Any] | None = None, + env_vars: dict[str, Any] | None = None, + ) -> ExecutionResult: + """ + Execute a Tool model instance directly with the provided properties. + + This method performs the actual execution logic used by both execute() + and executeSeparated(). It validates input properties, builds the execution + context, and dispatches to the appropriate executor. + + Args: + tool: Tool model instance to execute + properties: Properties/parameters to pass to the tool (default: empty dict) + env_vars: Environment variables for template context (default: empty dict) + + Returns: + ExecutionResult with success/error status and content + + Raises: + ToolManagerError: If properties validation fails + """ + # Default to empty dicts if None + if properties is None: + properties = {} + if env_vars is None: + env_vars = {} + # Validate input schema if present # Check both: not None (schema exists) and not empty dict (schema has content) # This handles three cases: None (no schema), {} (empty schema), and {...} (schema with properties) diff --git a/tests/unit/test_execute_separated.py b/tests/unit/test_execute_separated.py new file mode 100644 index 0000000..db6c253 --- /dev/null +++ b/tests/unit/test_execute_separated.py @@ -0,0 +1,468 @@ +""" +Unit tests for MCIClient.executeSeparated method. + +Tests the executeSeparated method which allows executing Tool models directly +without requiring them to be registered in the tool registry. +""" + +import tempfile +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from mcipy import MCIClient, MCIClientError +from mcipy.models import ( + CLIExecutionConfig, + FileExecutionConfig, + HTTPExecutionConfig, + TextExecutionConfig, + Tool, +) + + +class TestExecuteSeparatedBasic: + """Basic tests for executeSeparated method.""" + + @pytest.fixture + def minimal_schema_file(self): + """Create a minimal schema file for testing.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f: + f.write('{"schemaVersion": "1.0", "tools": []}') + return f.name + + def test_execute_separated_with_text_execution(self, minimal_schema_file): + """Test executeSeparated with a simple text execution tool.""" + client = MCIClient(schema_file_path=minimal_schema_file) + + # Create a dynamic tool + tool = Tool( + name="test_greeting", + description="Generate a greeting", + execution=TextExecutionConfig( + type="text", text="Hello {{props.name}}!" + ), + ) + + # Execute the tool + result = client.executeSeparated( + tool=tool, properties={"name": "Alice"}, env_vars={} + ) + + # Verify result + assert result is not None + assert result.result.isError is False + assert len(result.result.content) == 1 + assert result.result.content[0].text == "Hello Alice!" + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_with_env_vars(self, minimal_schema_file): + """Test executeSeparated with environment variable substitution.""" + client = MCIClient( + schema_file_path=minimal_schema_file, + env_vars={"DEFAULT_LOCATION": "World"}, + ) + + # Create a tool using both props and env + tool = Tool( + name="test_greeting", + execution=TextExecutionConfig( + type="text", + text="Hello {{props.name}} from {{env.LOCATION}}!", + ), + ) + + # Execute with custom env_vars + result = client.executeSeparated( + tool=tool, + properties={"name": "Bob"}, + env_vars={"LOCATION": "NYC"}, + ) + + assert result.result.content[0].text == "Hello Bob from NYC!" + + # Execute with client's env_vars (env_vars=None) + result = client.executeSeparated( + tool=tool, + properties={"name": "Charlie"}, + env_vars={"LOCATION": "World"}, + ) + + assert result.result.content[0].text == "Hello Charlie from World!" + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_uses_client_env_vars_when_none( + self, minimal_schema_file + ): + """Test that executeSeparated uses client's env_vars when env_vars param is None.""" + client = MCIClient( + schema_file_path=minimal_schema_file, + env_vars={"API_KEY": "client-key-123"}, + ) + + tool = Tool( + name="test_tool", + execution=TextExecutionConfig( + type="text", text="Key: {{env.API_KEY}}" + ), + ) + + # Call with env_vars=None should use client's env_vars + result = client.executeSeparated(tool=tool, properties={}, env_vars=None) + + assert result.result.content[0].text == "Key: client-key-123" + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_with_no_properties(self, minimal_schema_file): + """Test executeSeparated with no properties required.""" + client = MCIClient(schema_file_path=minimal_schema_file) + + tool = Tool( + name="simple_tool", + execution=TextExecutionConfig(type="text", text="Static message"), + ) + + # Execute without properties + result = client.executeSeparated(tool=tool) + + assert result.result.content[0].text == "Static message" + + # Execute with None properties + result = client.executeSeparated(tool=tool, properties=None) + + assert result.result.content[0].text == "Static message" + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_with_input_schema_validation( + self, minimal_schema_file + ): + """Test that executeSeparated validates input schema.""" + client = MCIClient(schema_file_path=minimal_schema_file) + + # Create a tool with required properties + tool = Tool( + name="test_tool", + inputSchema={ + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + execution=TextExecutionConfig( + type="text", text="Hello {{props.name}}!" + ), + ) + + # Test with missing required property + with pytest.raises(MCIClientError) as exc_info: + client.executeSeparated(tool=tool, properties={}) + + assert "requires properties" in str(exc_info.value).lower() + assert "name" in str(exc_info.value).lower() + + # Test with valid properties + result = client.executeSeparated(tool=tool, properties={"name": "Alice"}) + assert result.result.content[0].text == "Hello Alice!" + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_with_default_values(self, minimal_schema_file): + """Test that executeSeparated applies default values from input schema.""" + client = MCIClient(schema_file_path=minimal_schema_file) + + tool = Tool( + name="test_tool", + inputSchema={ + "type": "object", + "properties": { + "name": {"type": "string"}, + "greeting": {"type": "string", "default": "Hi"}, + }, + "required": ["name"], + }, + execution=TextExecutionConfig( + type="text", text="{{props.greeting}} {{props.name}}!" + ), + ) + + # Execute without optional property - should use default + result = client.executeSeparated(tool=tool, properties={"name": "Alice"}) + assert result.result.content[0].text == "Hi Alice!" + + # Execute with optional property - should override default + result = client.executeSeparated( + tool=tool, properties={"name": "Bob", "greeting": "Hello"} + ) + assert result.result.content[0].text == "Hello Bob!" + + # Cleanup + Path(minimal_schema_file).unlink() + + +class TestExecuteSeparatedValidation: + """Tests for validation and error handling in executeSeparated.""" + + @pytest.fixture + def minimal_schema_file(self): + """Create a minimal schema file for testing.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f: + f.write('{"schemaVersion": "1.0", "tools": []}') + return f.name + + def test_execute_separated_blocked_in_client_validating_mode( + self, minimal_schema_file + ): + """Test that executeSeparated is blocked when client is in validating mode.""" + client = MCIClient(schema_file_path=minimal_schema_file, validating=True) + + tool = Tool( + name="test_tool", + execution=TextExecutionConfig(type="text", text="Hello"), + ) + + with pytest.raises(MCIClientError) as exc_info: + client.executeSeparated(tool=tool) + + assert "validating mode" in str(exc_info.value).lower() + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_blocked_with_method_validating_flag( + self, minimal_schema_file + ): + """Test that executeSeparated is blocked when validating=True parameter is passed.""" + client = MCIClient( + schema_file_path=minimal_schema_file, validating=False + ) + + tool = Tool( + name="test_tool", + execution=TextExecutionConfig(type="text", text="Hello"), + ) + + with pytest.raises(MCIClientError) as exc_info: + client.executeSeparated(tool=tool, validating=True) + + assert "validating mode" in str(exc_info.value).lower() + + # Cleanup + Path(minimal_schema_file).unlink() + + +class TestExecuteSeparatedWithDifferentExecutors: + """Tests for executeSeparated with different executor types.""" + + @pytest.fixture + def minimal_schema_file(self): + """Create a minimal schema file for testing.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f: + f.write('{"schemaVersion": "1.0", "tools": []}') + return f.name + + def test_execute_separated_with_cli_executor(self, minimal_schema_file): + """Test executeSeparated with CLI executor.""" + client = MCIClient(schema_file_path=minimal_schema_file) + + tool = Tool( + name="echo_tool", + execution=CLIExecutionConfig( + type="cli", command="echo", args=["{{props.message}}"] + ), + ) + + result = client.executeSeparated( + tool=tool, properties={"message": "Hello CLI"} + ) + + assert result is not None + assert result.result.isError is False + assert "Hello CLI" in result.result.content[0].text + + # Cleanup + Path(minimal_schema_file).unlink() + + def test_execute_separated_with_file_executor(self, minimal_schema_file): + """Test executeSeparated with File executor.""" + # Create a test file with content + with tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False + ) as test_file: + test_file.write("File content: {{props.value}}") + test_file_path = test_file.name + + client = MCIClient(schema_file_path=minimal_schema_file) + + tool = Tool( + name="read_file_tool", + execution=FileExecutionConfig( + type="file", path=test_file_path, enableTemplating=True + ), + ) + + result = client.executeSeparated( + tool=tool, properties={"value": "test123"} + ) + + assert result is not None + assert result.result.isError is False + assert "File content: test123" in result.result.content[0].text + + # Cleanup + Path(test_file_path).unlink() + Path(minimal_schema_file).unlink() + + @patch("mcipy.executors.http_executor.requests.request") + def test_execute_separated_with_http_executor( + self, mock_request, minimal_schema_file + ): + """Test executeSeparated with HTTP executor.""" + # Mock HTTP response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = '{"result": "success"}' + mock_response.headers = {"content-type": "application/json"} + mock_request.return_value = mock_response + + client = MCIClient(schema_file_path=minimal_schema_file) + + tool = Tool( + name="http_tool", + execution=HTTPExecutionConfig( + type="http", + method="GET", + url="https://api.example.com/data", + params={"id": "{{props.item_id}}"}, + ), + ) + + result = client.executeSeparated( + tool=tool, properties={"item_id": "123"} + ) + + assert result is not None + assert result.result.isError is False + + # Verify the request was made with correct params + mock_request.assert_called_once() + call_args = mock_request.call_args + assert call_args[1]["params"]["id"] == "123" + + # Cleanup + Path(minimal_schema_file).unlink() + + +class TestExecuteSeparatedIsolation: + """Tests to verify that executeSeparated doesn't affect the tool registry.""" + + @pytest.fixture + def schema_with_tools(self): + """Create a schema file with some tools.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f: + f.write( + """{ + "schemaVersion": "1.0", + "tools": [ + { + "name": "registered_tool", + "execution": { + "type": "text", + "text": "Registered tool output" + } + } + ] + }""" + ) + return f.name + + def test_execute_separated_does_not_register_tool(self, schema_with_tools): + """Test that executeSeparated doesn't add tool to registry.""" + client = MCIClient(schema_file_path=schema_with_tools) + + # Verify initial tool list + initial_tools = client.list_tools() + assert initial_tools == ["registered_tool"] + + # Execute a dynamic tool + dynamic_tool = Tool( + name="dynamic_tool", + execution=TextExecutionConfig(type="text", text="Dynamic output"), + ) + + result = client.executeSeparated(tool=dynamic_tool) + assert result.result.content[0].text == "Dynamic output" + + # Verify tool list hasn't changed + final_tools = client.list_tools() + assert final_tools == ["registered_tool"] + assert "dynamic_tool" not in final_tools + + # Cleanup + Path(schema_with_tools).unlink() + + def test_execute_separated_with_same_name_as_registered_tool( + self, schema_with_tools + ): + """Test that executeSeparated can execute a tool with same name as registered tool.""" + client = MCIClient(schema_file_path=schema_with_tools) + + # Create a tool with same name but different config + override_tool = Tool( + name="registered_tool", + execution=TextExecutionConfig( + type="text", text="Override tool output" + ), + ) + + # Execute the override tool + result = client.executeSeparated(tool=override_tool) + assert result.result.content[0].text == "Override tool output" + + # Verify the registered tool is unaffected + result = client.execute("registered_tool") + assert result.result.content[0].text == "Registered tool output" + + # Cleanup + Path(schema_with_tools).unlink() + + def test_execute_separated_independent_env_vars(self, schema_with_tools): + """Test that executeSeparated env_vars don't affect client env_vars.""" + client = MCIClient( + schema_file_path=schema_with_tools, + env_vars={"KEY": "client_value"}, + ) + + tool = Tool( + name="test_tool", + execution=TextExecutionConfig(type="text", text="{{env.KEY}}"), + ) + + # Execute with different env_vars + result = client.executeSeparated( + tool=tool, env_vars={"KEY": "separated_value"} + ) + assert result.result.content[0].text == "separated_value" + + # Verify client's env_vars are unchanged + # Execute using client's env_vars + result = client.executeSeparated(tool=tool, env_vars=None) + assert result.result.content[0].text == "client_value" + + # Cleanup + Path(schema_with_tools).unlink() From 668b85ee53848a546b31fe9a35b2fcc4b7203cc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:10:17 +0000 Subject: [PATCH 3/4] Add manual test for executeSeparated method Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- testsManual/test_execute_separated_manual.py | 272 +++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 testsManual/test_execute_separated_manual.py diff --git a/testsManual/test_execute_separated_manual.py b/testsManual/test_execute_separated_manual.py new file mode 100644 index 0000000..830f568 --- /dev/null +++ b/testsManual/test_execute_separated_manual.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +""" +Manual test for executeSeparated method. + +This script demonstrates how to use the new executeSeparated method +to execute Tool models directly without registering them in the schema. + +Run with: uv run python testsManual/test_execute_separated_manual.py +""" + +from mcipy import MCIClient +from mcipy.models import ( + CLIExecutionConfig, + FileExecutionConfig, + HTTPExecutionConfig, + TextExecutionConfig, + Tool, +) + + +def test_text_execution(): + """Test executeSeparated with text execution.""" + print("=" * 70) + print("Test 1: Text Execution with Template Substitution") + print("=" * 70) + + # Create a minimal client + client = MCIClient(schema_file_path="example.mci.json") + + # Create a dynamic tool + tool = Tool( + name="dynamic_greeting", + description="Generate a personalized greeting", + execution=TextExecutionConfig( + type="text", + text="Hello {{props.name}}! Welcome to {{env.LOCATION}}. Your role is {{props.role}}.", + ), + ) + + # Execute the tool + result = client.executeSeparated( + tool=tool, + properties={"name": "Alice", "role": "Developer"}, + env_vars={"LOCATION": "San Francisco"}, + ) + + # Display result + print(f"Success: {not result.result.isError}") + print(f"Output: {result.result.content[0].text}") + print() + + +def test_cli_execution(): + """Test executeSeparated with CLI execution.""" + print("=" * 70) + print("Test 2: CLI Execution") + print("=" * 70) + + client = MCIClient(schema_file_path="example.mci.json") + + # Create a tool that uses echo command + tool = Tool( + name="echo_tool", + description="Echo a message using system command", + execution=CLIExecutionConfig( + type="cli", command="echo", args=["Message: {{props.text}}"] + ), + ) + + result = client.executeSeparated( + tool=tool, properties={"text": "Hello from CLI!"} + ) + + print(f"Success: {not result.result.isError}") + print(f"Output: {result.result.content[0].text.strip()}") + print() + + +def test_with_input_schema(): + """Test executeSeparated with input schema validation.""" + print("=" * 70) + print("Test 3: Input Schema Validation and Default Values") + print("=" * 70) + + client = MCIClient(schema_file_path="example.mci.json") + + # Tool with input schema + tool = Tool( + name="formatted_message", + description="Create a formatted message", + inputSchema={ + "type": "object", + "properties": { + "name": {"type": "string"}, + "greeting": {"type": "string", "default": "Hi"}, + "suffix": {"type": "string", "default": "Have a great day!"}, + }, + "required": ["name"], + }, + execution=TextExecutionConfig( + type="text", + text="{{props.greeting}} {{props.name}}! {{props.suffix}}", + ), + ) + + # Test with only required properties (should use defaults) + print("Testing with defaults:") + result1 = client.executeSeparated(tool=tool, properties={"name": "Bob"}) + print(f" Output: {result1.result.content[0].text}") + + # Test with custom values + print("Testing with custom values:") + result2 = client.executeSeparated( + tool=tool, + properties={ + "name": "Charlie", + "greeting": "Greetings", + "suffix": "See you soon!", + }, + ) + print(f" Output: {result2.result.content[0].text}") + print() + + +def test_tool_registry_isolation(): + """Test that executeSeparated doesn't affect the tool registry.""" + print("=" * 70) + print("Test 4: Tool Registry Isolation") + print("=" * 70) + + client = MCIClient(schema_file_path="example.mci.json") + + # Get initial tool list + initial_tools = client.list_tools() + print(f"Initial tools in registry: {len(initial_tools)}") + print(f" {', '.join(initial_tools[:3])}...") + + # Execute a dynamic tool + dynamic_tool = Tool( + name="temporary_tool", + description="This tool won't be registered", + execution=TextExecutionConfig(type="text", text="Temporary output"), + ) + + result = client.executeSeparated(tool=dynamic_tool) + print(f"\nExecuted dynamic tool: {dynamic_tool.name}") + print(f" Output: {result.result.content[0].text}") + + # Verify tool list hasn't changed + final_tools = client.list_tools() + print(f"\nFinal tools in registry: {len(final_tools)}") + print(f" Registry unchanged: {initial_tools == final_tools}") + print( + f" Dynamic tool in registry: {'temporary_tool' in final_tools}" + ) + print() + + +def test_environment_variable_override(): + """Test environment variable override with executeSeparated.""" + print("=" * 70) + print("Test 5: Environment Variable Override") + print("=" * 70) + + # Client with default env vars + client = MCIClient( + schema_file_path="example.mci.json", + env_vars={"API_KEY": "client-default-key", "ENVIRONMENT": "production"}, + ) + + tool = Tool( + name="api_info", + execution=TextExecutionConfig( + type="text", + text="API Key: {{env.API_KEY}} | Environment: {{env.ENVIRONMENT}}", + ), + ) + + # Use client's env vars + print("Using client's environment variables:") + result1 = client.executeSeparated(tool=tool, env_vars=None) + print(f" {result1.result.content[0].text}") + + # Override with custom env vars + print("\nUsing custom environment variables:") + result2 = client.executeSeparated( + tool=tool, + env_vars={"API_KEY": "test-key-123", "ENVIRONMENT": "testing"}, + ) + print(f" {result2.result.content[0].text}") + + # Verify client's env vars are unchanged + print("\nVerifying client's env vars unchanged:") + result3 = client.executeSeparated(tool=tool, env_vars=None) + print(f" {result3.result.content[0].text}") + print() + + +def test_error_handling(): + """Test error handling in executeSeparated.""" + print("=" * 70) + print("Test 6: Error Handling") + print("=" * 70) + + client = MCIClient(schema_file_path="example.mci.json") + + # Tool with required properties + tool = Tool( + name="required_props_tool", + inputSchema={ + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + execution=TextExecutionConfig(type="text", text="Hello {{props.name}}"), + ) + + # Try to execute without required property + print("Testing missing required property:") + try: + client.executeSeparated(tool=tool, properties={}) + print(" ❌ Should have raised an error!") + except Exception as e: + print(f" ✓ Correctly raised error: {type(e).__name__}") + print(f" Message: {str(e)}") + + # Test with validating mode + print("\nTesting validating mode:") + validating_client = MCIClient( + schema_file_path="example.mci.json", validating=True + ) + try: + validating_client.executeSeparated(tool=tool) + print(" ❌ Should have raised an error!") + except Exception as e: + print(f" ✓ Correctly raised error: {type(e).__name__}") + print(f" Message: {str(e)}") + print() + + +def main(): + """Run all manual tests.""" + print("\n") + print("╔" + "═" * 68 + "╗") + print("║" + " " * 68 + "║") + print("║" + " Manual Tests for executeSeparated Method".center(68) + "║") + print("║" + " " * 68 + "║") + print("╚" + "═" * 68 + "╝") + print("\n") + + try: + test_text_execution() + test_cli_execution() + test_with_input_schema() + test_tool_registry_isolation() + test_environment_variable_override() + test_error_handling() + + print("=" * 70) + print("✅ All manual tests completed successfully!") + print("=" * 70) + print() + + except Exception as e: + print(f"\n❌ Test failed with error: {e}") + import traceback + + traceback.print_exc() + + +if __name__ == "__main__": + main() From 0dd137541cec069a32d81ebf2fc43a76783d9442 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:13:22 +0000 Subject: [PATCH 4/4] Add integration tests for executeSeparated method Co-authored-by: MaestroError <46760939+MaestroError@users.noreply.github.com> --- tests/test_execute_separated_integration.py | 215 +++++++++++++++++++ testsManual/test_execute_separated_manual.py | 0 2 files changed, 215 insertions(+) create mode 100644 tests/test_execute_separated_integration.py mode change 100644 => 100755 testsManual/test_execute_separated_manual.py diff --git a/tests/test_execute_separated_integration.py b/tests/test_execute_separated_integration.py new file mode 100644 index 0000000..e4df7e7 --- /dev/null +++ b/tests/test_execute_separated_integration.py @@ -0,0 +1,215 @@ +""" +Integration test for executeSeparated method with existing MCIClient features. + +This test verifies that executeSeparated works well alongside existing +MCIClient methods and doesn't interfere with normal operation. +""" + +import tempfile +from pathlib import Path + +import pytest + +from mcipy import MCIClient +from mcipy.models import TextExecutionConfig, Tool + + +class TestExecuteSeparatedIntegration: + """Integration tests for executeSeparated with existing features.""" + + @pytest.fixture + def test_schema(self): + """Create a test schema file with some tools.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f: + f.write( + """{ + "schemaVersion": "1.0", + "tools": [ + { + "name": "registered_tool_1", + "description": "First registered tool", + "execution": { + "type": "text", + "text": "Output from registered_tool_1: {{props.value}}" + } + }, + { + "name": "registered_tool_2", + "description": "Second registered tool", + "execution": { + "type": "text", + "text": "Output from registered_tool_2: {{env.KEY}}" + } + } + ] + }""" + ) + schema_path = f.name + + # File is now closed and flushed + yield schema_path + + # Cleanup after tests + Path(schema_path).unlink() + + def test_execute_and_execute_separated_work_together(self, test_schema): + """Test that execute and executeSeparated can be used together.""" + client = MCIClient( + schema_file_path=test_schema, env_vars={"KEY": "client-value"} + ) + + # Execute a registered tool + result1 = client.execute( + "registered_tool_1", properties={"value": "from execute"} + ) + assert "Output from registered_tool_1: from execute" in result1.result.content[0].text + + # Execute a dynamic tool + dynamic_tool = Tool( + name="dynamic_tool", + execution=TextExecutionConfig( + type="text", text="Dynamic: {{props.msg}}" + ), + ) + result2 = client.executeSeparated( + tool=dynamic_tool, properties={"msg": "from executeSeparated"} + ) + assert "Dynamic: from executeSeparated" in result2.result.content[0].text + + # Execute another registered tool to verify nothing broke + result3 = client.execute("registered_tool_2") + assert "Output from registered_tool_2: client-value" in result3.result.content[0].text + + def test_filtering_not_affected_by_execute_separated(self, test_schema): + """Test that executeSeparated doesn't affect filtering methods.""" + client = MCIClient(schema_file_path=test_schema) + + # Get initial filtered list + filtered_before = client.only(["registered_tool_1"]) + assert len(filtered_before) == 1 + assert filtered_before[0].name == "registered_tool_1" + + # Execute a dynamic tool + dynamic_tool = Tool( + name="dynamic_tool", + execution=TextExecutionConfig(type="text", text="test"), + ) + client.executeSeparated(tool=dynamic_tool) + + # Verify filtering still works the same + filtered_after = client.only(["registered_tool_1"]) + assert len(filtered_after) == 1 + assert filtered_after[0].name == "registered_tool_1" + + # Verify without() also works + without_result = client.without(["registered_tool_1"]) + assert len(without_result) == 1 + assert without_result[0].name == "registered_tool_2" + + def test_list_tools_not_affected_by_execute_separated(self, test_schema): + """Test that list_tools() is not affected by executeSeparated.""" + client = MCIClient(schema_file_path=test_schema) + + # Get initial tool list + tools_before = client.list_tools() + assert set(tools_before) == {"registered_tool_1", "registered_tool_2"} + + # Execute multiple dynamic tools + for i in range(5): + tool = Tool( + name=f"dynamic_tool_{i}", + execution=TextExecutionConfig(type="text", text=f"Tool {i}"), + ) + client.executeSeparated(tool=tool) + + # Verify tool list unchanged + tools_after = client.list_tools() + assert set(tools_after) == {"registered_tool_1", "registered_tool_2"} + assert tools_before == tools_after + + def test_execute_separated_respects_client_env_vars(self, test_schema): + """Test that executeSeparated properly uses client env_vars when None.""" + client = MCIClient( + schema_file_path=test_schema, + env_vars={"KEY1": "value1", "KEY2": "value2"}, + ) + + tool = Tool( + name="env_tool", + execution=TextExecutionConfig( + type="text", text="{{env.KEY1}} - {{env.KEY2}}" + ), + ) + + # Should use client's env_vars + result = client.executeSeparated(tool=tool, env_vars=None) + assert result.result.content[0].text == "value1 - value2" + + def test_execute_separated_with_same_name_as_registered(self, test_schema): + """Test executeSeparated with tool name same as registered tool.""" + client = MCIClient(schema_file_path=test_schema) + + # Create tool with same name but different behavior + override_tool = Tool( + name="registered_tool_1", + execution=TextExecutionConfig( + type="text", text="Override: {{props.data}}" + ), + ) + + # Execute override tool + result1 = client.executeSeparated( + tool=override_tool, properties={"data": "custom"} + ) + assert result1.result.content[0].text == "Override: custom" + + # Verify registered tool still works normally + result2 = client.execute( + "registered_tool_1", properties={"value": "normal"} + ) + assert "Output from registered_tool_1: normal" in result2.result.content[0].text + + def test_multiple_clients_independent_execute_separated(self): + """Test that executeSeparated on different clients is independent.""" + # Create two separate schema files + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f1: + f1.write('{"schemaVersion": "1.0", "tools": []}') + schema1 = f1.name + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f2: + f2.write('{"schemaVersion": "1.0", "tools": []}') + schema2 = f2.name + + try: + # Create two clients with different env vars + client1 = MCIClient( + schema_file_path=schema1, env_vars={"CLIENT": "one"} + ) + client2 = MCIClient( + schema_file_path=schema2, env_vars={"CLIENT": "two"} + ) + + tool = Tool( + name="test_tool", + execution=TextExecutionConfig( + type="text", text="Client: {{env.CLIENT}}" + ), + ) + + # Execute on both clients + result1 = client1.executeSeparated(tool=tool) + result2 = client2.executeSeparated(tool=tool) + + # Verify they use their own env vars + assert result1.result.content[0].text == "Client: one" + assert result2.result.content[0].text == "Client: two" + + finally: + Path(schema1).unlink() + Path(schema2).unlink() diff --git a/testsManual/test_execute_separated_manual.py b/testsManual/test_execute_separated_manual.py old mode 100644 new mode 100755