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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
150 changes: 150 additions & 0 deletions docs/openapi-ingestion.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring omits that successful stdout is parsed as JSON when possible; this line now misstates the function’s behavior, which returns parsed JSON via _parse_combined_output when the output looks like JSON.

Prompt for AI agents
Address the following comment on plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py at line 590:

<comment>Docstring omits that successful stdout is parsed as JSON when possible; this line now misstates the function’s behavior, which returns parsed JSON via _parse_combined_output when the output looks like JSON.</comment>

<file context>
@@ -588,8 +587,7 @@ async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], too
             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:
</file context>
Suggested change
it returns the content of stderr.
it returns the content of stderr. The output is parsed as JSON if possible; otherwise, it is returned as a raw string.
Fix with Cubic


Raises:
ValueError: If `tool_call_template` is not an instance of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading