Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,18 @@ Configuration examples for each protocol. Remember to replace `provider_type` wi
"url": "https://api.example.com/users/{user_id}", // Required
"http_method": "POST", // Required, default: "GET"
"content_type": "application/json", // Optional, default: "application/json"
"auth": { // Optional, example using ApiKeyAuth for a Bearer token. The client must prepend "Bearer " to the token.
"auth": { // Optional, authentication for accessing the OpenAPI spec URL (example using ApiKeyAuth for Bearer token)
Copy link
Contributor

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

Choose a reason for hiding this comment

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

Misleading comment: 'auth' is described as OpenAPI spec URL auth in a tool call example; should describe HTTP request auth.

Prompt for AI agents
Address the following comment on README.md at line 379:

<comment>Misleading comment: &#39;auth&#39; is described as OpenAPI spec URL auth in a tool call example; should describe HTTP request auth.</comment>

<file context>
@@ -376,12 +376,18 @@ Configuration examples for each protocol. Remember to replace `provider_type` wi
   &quot;http_method&quot;: &quot;POST&quot;, // Required, default: &quot;GET&quot;
   &quot;content_type&quot;: &quot;application/json&quot;, // Optional, default: &quot;application/json&quot;
-  &quot;auth&quot;: { // Optional, example using ApiKeyAuth for a Bearer token. The client must prepend &quot;Bearer &quot; to the token.
+  &quot;auth&quot;: { // Optional, authentication for accessing the OpenAPI spec URL (example using ApiKeyAuth for Bearer token)
     &quot;auth_type&quot;: &quot;api_key&quot;,
     &quot;api_key&quot;: &quot;Bearer $API_KEY&quot;, // Required
</file context>
Suggested change
"auth": { // Optional, authentication for accessing the OpenAPI spec URL (example using ApiKeyAuth for Bearer token)
"auth": { // Optional, authentication for the HTTP request (example using ApiKeyAuth for Bearer token)

βœ… Addressed in 7933fd4

"auth_type": "api_key",
"api_key": "Bearer $API_KEY", // Required
"var_name": "Authorization", // Optional, default: "X-Api-Key"
"location": "header" // Optional, default: "header"
},
"auth_tools": { // Optional, authentication for generated tools (applied only to endpoints requiring auth per OpenAPI spec)
"auth_type": "api_key",
"api_key": "Bearer $TOOL_API_KEY", // Required
"var_name": "Authorization", // Optional, default: "X-Api-Key"
"location": "header" // Optional, default: "header"
},
"headers": { // Optional
"X-Custom-Header": "value"
},
Expand Down Expand Up @@ -473,7 +479,13 @@ Note the name change from `http_stream` to `streamable_http`.
"name": "my_text_manual",
"call_template_type": "text", // Required
"file_path": "./manuals/my_manual.json", // Required
"auth": null // Optional (always null for Text)
"auth": null, // Optional (always null for Text)
"auth_tools": { // Optional, authentication for generated tools from OpenAPI specs
"auth_type": "api_key",
"api_key": "Bearer ${API_TOKEN}",
"var_name": "Authorization",
"location": "header"
}
}
```

Expand Down Expand Up @@ -569,7 +581,13 @@ client = await UtcpClient.create(config={
"manual_call_templates": [{
"name": "github",
"call_template_type": "http",
"url": "https://api.github.com/openapi.json"
"url": "https://api.github.com/openapi.json",
"auth_tools": { # Authentication for generated tools requiring auth
"auth_type": "api_key",
"api_key": "Bearer ${GITHUB_TOKEN}",
"var_name": "Authorization",
"location": "header"
}
}]
})
```
Expand All @@ -579,6 +597,7 @@ client = await UtcpClient.create(config={
- βœ… **Zero Infrastructure**: No servers to deploy or maintain
- βœ… **Direct API Calls**: Native performance, no proxy overhead
- βœ… **Automatic Conversion**: OpenAPI schemas β†’ UTCP tools
- βœ… **Selective Authentication**: Only protected endpoints get auth, public endpoints remain accessible
- βœ… **Authentication Preserved**: API keys, OAuth2, Basic auth supported
- βœ… **Multi-format Support**: JSON, YAML, OpenAPI 2.0/3.0
- βœ… **Batch Processing**: Convert multiple APIs simultaneously
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ class HttpCallTemplate(CallTemplate):
"var_name": "Authorization",
"location": "header"
},
"auth_tools": {
"auth_type": "api_key",
"api_key": "Bearer ${TOOL_API_KEY}",
"var_name": "Authorization",
"location": "header"
},
"headers": {
"X-Custom-Header": "value"
},
Expand Down Expand Up @@ -85,7 +91,8 @@ class HttpCallTemplate(CallTemplate):
url: The base URL for the HTTP endpoint. Supports path parameters like
"https://api.example.com/users/{user_id}/posts/{post_id}".
content_type: The Content-Type header for requests.
auth: Optional authentication configuration.
auth: Optional authentication configuration for accessing the OpenAPI spec URL.
auth_tools: Optional authentication configuration for generated tools. Applied only to endpoints requiring auth per OpenAPI spec.
headers: Optional static headers to include in all requests.
body_field: Name of the tool argument to map to the HTTP request body.
header_fields: List of tool argument names to map to HTTP request headers.
Expand All @@ -96,6 +103,7 @@ class HttpCallTemplate(CallTemplate):
url: str
content_type: str = Field(default="application/json")
auth: Optional[Auth] = None
auth_tools: Optional[Auth] = Field(default=None, description="Authentication configuration for generated tools (applied only to endpoints requiring auth per OpenAPI spec)")
headers: Optional[Dict[str, str]] = None
body_field: Optional[str] = Field(default="body", description="The name of the single input field to be sent as the request body.")
header_fields: Optional[List[str]] = Field(default=None, description="List of input fields to be sent as request headers.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ async def register_manual(self, caller, manual_call_template: CallTemplate) -> R
utcp_manual = UtcpManualSerializer().validate_dict(response_data)
else:
logger.info(f"Assuming OpenAPI spec from '{manual_call_template.name}'. Converting to UTCP manual.")
converter = OpenApiConverter(response_data, spec_url=manual_call_template.url, call_template_name=manual_call_template.name)
converter = OpenApiConverter(response_data, spec_url=manual_call_template.url, call_template_name=manual_call_template.name, auth_tools=manual_call_template.auth_tools)
utcp_manual = converter.convert()

return RegisterManualResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class OpenApiConverter:
call_template_name: Normalized name for the call_template derived from the spec.
"""

def __init__(self, openapi_spec: Dict[str, Any], spec_url: Optional[str] = None, call_template_name: Optional[str] = None):
def __init__(self, openapi_spec: Dict[str, Any], spec_url: Optional[str] = None, call_template_name: Optional[str] = None, auth_tools: Optional[Auth] = None):
"""Initializes the OpenAPI converter.

Args:
Expand All @@ -96,9 +96,12 @@ def __init__(self, openapi_spec: Dict[str, Any], spec_url: Optional[str] = None,
Used for base URL determination if servers are not specified.
call_template_name: Optional custom name for the call_template if
the specification title is not provided.
auth_tools: Optional auth configuration for generated tools.
Applied only to endpoints that require authentication per OpenAPI spec.
"""
self.spec = openapi_spec
self.spec_url = spec_url
self.auth_tools = auth_tools
# Single counter for all placeholder variables
self.placeholder_counter = 0
if call_template_name is None:
Expand Down Expand Up @@ -160,19 +163,22 @@ def convert(self) -> UtcpManual:

def _extract_auth(self, operation: Dict[str, Any]) -> Optional[Auth]:
"""
Extracts authentication information from OpenAPI operation and global security schemes."""
Extracts authentication information from OpenAPI operation and global security schemes.
Uses auth_tools configuration when compatible with OpenAPI auth requirements.
Supports both OpenAPI 2.0 and 3.0 security schemes.
"""
# First check for operation-level security requirements
security_requirements = operation.get("security", [])

# If no operation-level security, check global security requirements
if not security_requirements:
security_requirements = self.spec.get("security", [])

# If no security requirements, return None
# If no security requirements, return None (endpoint is public)
if not security_requirements:
return None

# Get security schemes - support both OpenAPI 2.0 and 3.0
# Generate auth from OpenAPI security schemes - support both OpenAPI 2.0 and 3.0
security_schemes = self._get_security_schemes()

# Process the first security requirement (most common case)
Expand All @@ -181,9 +187,47 @@ def _extract_auth(self, operation: Dict[str, Any]) -> Optional[Auth]:
for scheme_name, scopes in security_req.items():
if scheme_name in security_schemes:
scheme = security_schemes[scheme_name]
return self._create_auth_from_scheme(scheme, scheme_name)
openapi_auth = self._create_auth_from_scheme(scheme, scheme_name)

# If compatible with auth_tools, use actual values from manual call template
if self._is_auth_compatible(openapi_auth, self.auth_tools):
return self.auth_tools
else:
return openapi_auth # Use placeholder from OpenAPI scheme

return None

def _is_auth_compatible(self, openapi_auth: Optional[Auth], auth_tools: Optional[Auth]) -> bool:
"""
Checks if auth_tools configuration is compatible with OpenAPI auth requirements.

Args:
openapi_auth: Auth generated from OpenAPI security scheme
auth_tools: Auth configuration from manual call template

Returns:
True if compatible and auth_tools should be used, False otherwise
"""
if not openapi_auth or not auth_tools:
return False

# Must be same auth type
if type(openapi_auth) != type(auth_tools):
return False

# For API Key auth, check header name and location compatibility
if hasattr(openapi_auth, 'var_name') and hasattr(auth_tools, 'var_name'):
openapi_var = openapi_auth.var_name.lower() if openapi_auth.var_name else ""
tools_var = auth_tools.var_name.lower() if auth_tools.var_name else ""

if openapi_var != tools_var:
return False

if hasattr(openapi_auth, 'location') and hasattr(auth_tools, 'location'):
if openapi_auth.location != auth_tools.location:
return False

return True

def _get_security_schemes(self) -> Dict[str, Any]:
"""
Expand Down
Loading
Loading