Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
88 changes: 88 additions & 0 deletions src/mcipy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
33 changes: 33 additions & 0 deletions src/mcipy/tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading