diff --git a/README.md b/README.md index 02f1817..4a58c6e 100644 --- a/README.md +++ b/README.md @@ -538,4 +538,61 @@ The build process now involves building each package (`core` and `plugins`) sepa 4. Run the build: `python -m build`. 5. The distributable files (`.whl` and `.tar.gz`) will be in the `dist/` directory. +## OpenAPI Ingestion - Zero Infrastructure Tool Integration + +🚀 **Transform any existing REST API into UTCP tools without server modifications!** + +UTCP's OpenAPI ingestion feature automatically converts OpenAPI 2.0/3.0 specifications into UTCP tools, enabling AI agents to interact with existing APIs directly - no wrapper servers, no API changes, no additional infrastructure required. + +### Quick Start with OpenAPI + +```python +from utcp_http.openapi_converter import OpenApiConverter +import aiohttp + +# Convert any OpenAPI spec to UTCP tools +async def convert_api(): + async with aiohttp.ClientSession() as session: + async with session.get("https://api.github.com/openapi.json") as response: + openapi_spec = await response.json() + + converter = OpenApiConverter(openapi_spec) + manual = converter.convert() + + print(f"Generated {len(manual.tools)} tools from GitHub API!") + return manual + +# Or use UTCP Client configuration for automatic detection +from utcp.utcp_client import UtcpClient + +client = await UtcpClient.create(config={ + "manual_call_templates": [{ + "name": "github", + "call_template_type": "http", + "url": "https://api.github.com/openapi.json" + }] +}) +``` + +### Key Benefits + +- ✅ **Zero Infrastructure**: No servers to deploy or maintain +- ✅ **Direct API Calls**: Native performance, no proxy overhead +- ✅ **Automatic Conversion**: OpenAPI schemas → UTCP tools +- ✅ **Authentication Preserved**: API keys, OAuth2, Basic auth supported +- ✅ **Multi-format Support**: JSON, YAML, OpenAPI 2.0/3.0 +- ✅ **Batch Processing**: Convert multiple APIs simultaneously + +### Multiple Ingestion Methods + +1. **Direct Converter**: `OpenApiConverter` class for full control +2. **Remote URLs**: Fetch and convert specs from any URL +3. **Client Configuration**: Include specs directly in UTCP config +4. **Batch Processing**: Process multiple specs programmatically +5. **File-based**: Convert local JSON/YAML specifications + +📖 **[Complete OpenAPI Ingestion Guide](docs/openapi-ingestion.md)** - Detailed examples and advanced usage + +--- + ## [Contributors](https://www.utcp.io/about) diff --git a/docs/openapi-ingestion.md b/docs/openapi-ingestion.md new file mode 100644 index 0000000..779fc12 --- /dev/null +++ b/docs/openapi-ingestion.md @@ -0,0 +1,150 @@ +# OpenAPI Ingestion Methods in python-utcp + +UTCP automatically converts OpenAPI 2.0/3.0 specifications into UTCP tools, enabling AI agents to interact with REST APIs without requiring server modifications or additional infrastructure. + +## Method 1: Direct OpenAPI Converter + +Use the `OpenApiConverter` class for maximum control over the conversion process. + +```python +from utcp_http.openapi_converter import OpenApiConverter # utcp-http plugin +import json + +# From local JSON file +with open("api_spec.json", "r") as f: + openapi_spec = json.load(f) + +converter = OpenApiConverter(openapi_spec) +manual = converter.convert() + +print(f"Generated {len(manual.tools)} tools") +``` + +```python +from utcp_http.openapi_converter import OpenApiConverter # utcp-http plugin +import yaml + +# From YAML file (can also be JSON) +with open("api_spec.yaml", "r") as f: + openapi_spec = yaml.safe_load(f) + +converter = OpenApiConverter(openapi_spec) +manual = converter.convert() +``` + +## Method 2: Remote OpenAPI Specification + +Fetch and convert OpenAPI specifications from remote URLs. + +```python +import aiohttp +from utcp_http.openapi_converter import OpenApiConverter # utcp-http plugin + +async def load_remote_spec(url): + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + response.raise_for_status() + openapi_spec = await response.json() + + converter = OpenApiConverter(openapi_spec, spec_url=url) + return converter.convert() + +# Usage +manual = await load_remote_spec("https://api.example.com/openapi.json") +``` + +## Method 3: UTCP Client Configuration + +Include OpenAPI specs directly in your UTCP client configuration. + +```python +from utcp.utcp_client import UtcpClient # core utcp package + +config = { + "manual_call_templates": [ + { + "name": "weather_api", + "call_template_type": "http", + "url": "https://api.weather.com/openapi.json", + "http_method": "GET" + } + ] +} + +client = await UtcpClient.create(config=config) +``` + +```python +# With authentication +config = { + "manual_call_templates": [ + { + "name": "authenticated_api", + "call_template_type": "http", + "url": "https://api.example.com/openapi.json", + "auth": { + "auth_type": "api_key", + "api_key": "${API_KEY}", + "var_name": "Authorization", + "location": "header" + } + } + ] +} +``` + +## Method 4: Batch Processing + +Process multiple OpenAPI specifications programmatically. + +```python +import aiohttp +from utcp_http.openapi_converter import OpenApiConverter # utcp-http plugin +from utcp.data.utcp_manual import UtcpManual # core utcp package + +async def process_multiple_specs(spec_urls): + all_tools = [] + + for i, url in enumerate(spec_urls): + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + openapi_spec = await response.json() + + converter = OpenApiConverter(openapi_spec, spec_url=url, call_template_name=f"api_{i}") + manual = converter.convert() + all_tools.extend(manual.tools) + + return UtcpManual(tools=all_tools) + +# Usage +spec_urls = [ + "https://api.github.com/openapi.json", + "https://api.stripe.com/openapi.yaml" +] + +combined_manual = await process_multiple_specs(spec_urls) +``` + +## Key Features + +### Authentication Mapping +OpenAPI security schemes automatically convert to UTCP auth objects: + +- `apiKey` → `ApiKeyAuth` +- `http` (basic) → `BasicAuth` +- `http` (bearer) → `ApiKeyAuth` +- `oauth2` → `OAuth2Auth` + +### Multi-format Support +- **OpenAPI 2.0 & 3.0**: Full compatibility +- **JSON & YAML**: Automatic format detection +- **Local & Remote**: Files or URLs + +### Schema Resolution +- Handles `$ref` references automatically +- Resolves nested object definitions +- Detects circular references + +## Examples + +See the [examples repository](https://github.com/universal-tool-calling-protocol/utcp-examples) for complete working examples. diff --git a/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py b/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py index adcce69..d462fbe 100644 --- a/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py +++ b/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py @@ -7,7 +7,8 @@ import traceback class CommandStep(BaseModel): - """Configuration for a single command step in a CLI execution flow. + """REQUIRED + Configuration for a single command step in a CLI execution flow. Attributes: command: The command string to execute. Can contain UTCP_ARG_argname_UTCP_END diff --git a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py index 1ed7bae..61ce33c 100644 --- a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py +++ b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py @@ -10,7 +10,6 @@ - Tool discovery by running a command that outputs a UTCP manual. - Flexible argument formatting for different CLI conventions. - Support for environment variables and custom working directories. - - Automatic parsing of JSON output with a fallback to raw text. - Cross-platform command parsing for Windows and Unix-like systems. Security Considerations: @@ -588,8 +587,7 @@ async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], too Returns: The result of the command execution. If the command exits with a code of 0, it returns the content of stdout. If the exit code is non-zero, - it returns the content of stderr. The output is parsed as JSON if - possible; otherwise, it is returned as a raw string. + it returns the content of stderr. Raises: ValueError: If `tool_call_template` is not an instance of diff --git a/plugins/communication_protocols/mcp/tests/test_mcp_transport.py b/plugins/communication_protocols/mcp/tests/test_mcp_transport.py index cbd6073..d127791 100644 --- a/plugins/communication_protocols/mcp/tests/test_mcp_transport.py +++ b/plugins/communication_protocols/mcp/tests/test_mcp_transport.py @@ -55,7 +55,7 @@ async def test_register_manual_discovers_tools(transport: McpCommunicationProtoc assert len(register_result.manual.tools) == 4 # Find the echo tool - echo_tool = next((tool for tool in register_result.manual.tools if tool.name ==f"{SERVER_NAME}.echo"), None) + echo_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{SERVER_NAME}.echo"), None) assert echo_tool is not None assert "echoes back its input" in echo_tool.description