Skip to content
Merged
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
85 changes: 85 additions & 0 deletions docs/schema_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,91 @@ execution:
url: "https://api.example.com/resources/{{props.id}}"
```

### Input Schema and Default Values

The `inputSchema` field uses JSON Schema to define the expected properties for a tool. When executing a tool, the MCI adapter processes properties as follows:

1. **Required Properties**: Must be provided, or execution will fail with a validation error
2. **Optional Properties with Defaults**: If not provided, the default value is used
3. **Optional Properties without Defaults**: If not provided, they are skipped (not included in template context)

This behavior prevents template substitution errors for optional properties that aren't needed for a particular execution.

#### Example: Properties with Defaults

```json
{
"name": "search_files",
"description": "Search for text in files",
"inputSchema": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Search pattern"
},
"directory": {
"type": "string",
"description": "Directory to search in"
},
"include_images": {
"type": "boolean",
"description": "Include image files in search",
"default": false
},
"case_sensitive": {
"type": "boolean",
"description": "Use case-sensitive search",
"default": true
},
"max_results": {
"type": "number",
"description": "Maximum number of results",
"default": 100
},
"file_extensions": {
"type": "string",
"description": "Optional comma-separated list of file extensions"
}
},
"required": ["pattern", "directory"]
},
"execution": {
"type": "text",
"text": "Searching '{{props.pattern}}' in {{props.directory}} (images: {{props.include_images}}, max: {{props.max_results}})"
}
}
```

**Execution with minimal properties:**
```python
# Only required properties provided
client.execute("search_files", properties={
"pattern": "TODO",
"directory": "/home/user/projects"
})
# Result: include_images=false, case_sensitive=true, max_results=100 (defaults used)
# file_extensions is skipped (not in template, no default)
```

**Execution with overridden defaults:**
```python
# Some defaults overridden
client.execute("search_files", properties={
"pattern": "FIXME",
"directory": "/tmp",
"include_images": true,
"max_results": 50
})
# Result: include_images=true, max_results=50 (overridden), case_sensitive=true (default)
```

**Property Resolution Rules:**
- Properties provided at execution time always take precedence over defaults
- Default values can be any valid JSON type: boolean, number, string, array, object, null
- Optional properties without defaults are not included in the template context if not provided
- This prevents `{{props.optional_prop}}` from causing errors when `optional_prop` is not provided

---

## Execution Types
Expand Down
4 changes: 3 additions & 1 deletion src/mcipy/mcp_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
fetching their tool definitions, and building MCI-compatible toolset schemas.
"""

import asyncio, concurrent.futures
import asyncio
import concurrent.futures
from datetime import UTC, datetime, timedelta
from typing import Any

Expand Down Expand Up @@ -102,6 +103,7 @@ def fetch_and_build_toolset(
- If a loop IS running (e.g., inside an async CLI), offload the async
work to a separate thread that owns its own loop, and block until it finishes.
"""

async def _coro():
return await MCPIntegration.fetch_and_build_toolset_async(
server_name, server_config, schema_version, env_context, template_engine
Expand Down
64 changes: 62 additions & 2 deletions src/mcipy/tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,17 @@ def execute(
# This handles three cases: None (no schema), {} (empty schema), and {...} (schema with properties)
if tool.inputSchema is not None and tool.inputSchema:
self._validate_input_properties(tool, properties)
# Resolve properties with defaults applied and optional properties skipped
resolved_properties = self._resolve_properties_with_defaults(tool, properties)
else:
# No schema, use properties as-is
resolved_properties = properties

# Build context for execution
context: dict[str, Any] = {
"props": properties,
"props": resolved_properties,
"env": env_vars,
"input": properties, # Alias for backward compatibility
"input": resolved_properties, # Alias for backward compatibility
}

# Build path validation context
Expand Down Expand Up @@ -299,3 +304,58 @@ def _validate_input_properties(self, tool: Tool, properties: dict[str, Any]) ->
f"Tool '{tool.name}' requires properties: {', '.join(required)}. "
f"Missing: {', '.join(missing_props)}"
)

def _resolve_properties_with_defaults(
self, tool: Tool, properties: dict[str, Any]
) -> dict[str, Any]:
"""
Resolve properties with default values and skip optional properties.

For each property in the input schema:
- If provided in properties: use the provided value
- Else if has default value in schema: use the default
- Else if required: already validated, should not happen
- Else (optional without default): skip, don't include in resolved properties

This prevents template substitution errors for optional properties that
are not provided and have no default value.

Args:
tool: Tool object with inputSchema
properties: Properties provided by the caller

Returns:
Resolved properties dictionary with defaults applied and optional properties skipped
"""
input_schema = tool.inputSchema
if not input_schema:
return properties

# Get schema properties definition
schema_properties = input_schema.get("properties", {})
if not schema_properties:
# No properties defined in schema, return as-is
return properties

# Get required properties list
required = set(input_schema.get("required", []))

# Build resolved properties
resolved: dict[str, Any] = {}

# Process each property in the schema
for prop_name, prop_schema in schema_properties.items():
if prop_name in properties:
# Property was provided, use it
resolved[prop_name] = properties[prop_name]
elif "default" in prop_schema:
# Property not provided but has default, use default
resolved[prop_name] = prop_schema["default"]
elif prop_name in required:
# Required property not provided - this should have been caught by validation
# but we'll include it anyway to maintain consistency
# (validation should have raised an error before we get here)
pass
# else: optional property without default - skip it

return resolved
Loading
Loading