Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d28dc58
Update docstrings and fix README
h3xxit Aug 26, 2025
018806c
Merge pull request #52 from universal-tool-calling-protocol/dev
h3xxit Aug 27, 2025
0f2af7e
Merge pull request #53 from universal-tool-calling-protocol/dev
h3xxit Aug 27, 2025
d28c0af
Update documentation and fix MCP plugin
h3xxit Sep 7, 2025
7ba8b3c
Merge pull request #61 "Update CLI" from universal-tool-calling-proto…
h3xxit Sep 7, 2025
908cd40
Merge pull request #63 from universal-tool-calling-protocol/dev
h3xxit Sep 8, 2025
74a11e2
Merge pull request #69 from universal-tool-calling-protocol/dev
h3xxit Sep 21, 2025
03a4b9f
Merge pull request #70 from universal-tool-calling-protocol/dev
h3xxit Oct 7, 2025
8443cda
Merge branch 'dev'
h3xxit Oct 7, 2025
0150a3b
Merge branch 'dev'
h3xxit Oct 7, 2025
629621e
Merge pull request #76 from universal-tool-calling-protocol/dev
h3xxit Nov 29, 2025
9d80c32
Merge pull request #77 from universal-tool-calling-protocol/dev
h3xxit Nov 29, 2025
a9df439
Merge pull request #78 from universal-tool-calling-protocol/dev
h3xxit Nov 30, 2025
1d1c3a7
Merge pull request #79 from universal-tool-calling-protocol/dev
h3xxit Dec 1, 2025
6bf6d66
Merge pull request #81 from universal-tool-calling-protocol/dev
h3xxit Dec 3, 2025
dd150cb
Merge pull request #84 from universal-tool-calling-protocol/dev
h3xxit May 10, 2026
89ea517
Merge pull request #85 from universal-tool-calling-protocol/dev
h3xxit May 10, 2026
e915093
Merge branch 'dev'
h3xxit May 10, 2026
4ed0a48
Merge branch 'dev'
h3xxit May 10, 2026
ad8014d
fix: OpenAPI Converter was not parsing examples for request parameters
shane-rand Jun 17, 2026
15878ea
fix: resolve in extract_examples. extract_outputs now extracts examp…
shane-rand Jun 17, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"""

import json
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Literal, cast
import sys
import uuid
from urllib.parse import urlparse
Expand Down Expand Up @@ -248,14 +248,14 @@ def _is_auth_compatible(self, openapi_auth: Optional[Auth], auth_tools: Optional

# 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 ""
openapi_var = getattr(openapi_auth, 'var_name', "").lower() if getattr(openapi_auth, 'var_name', None) else ""
tools_var = getattr(auth_tools, 'var_name', "").lower() if getattr(auth_tools, 'var_name', None) 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:
if getattr(openapi_auth, 'location', None) != getattr(auth_tools, 'location', None):
return False

return True
Expand Down Expand Up @@ -300,6 +300,32 @@ def _resolve_ref_obj(self, obj: Any, visited: Optional[set] = None) -> Any:
if isinstance(obj, dict) and "$ref" in obj:
return self._resolve_ref_path(obj["$ref"], visited)
return obj

def _extract_examples(self, obj: Dict[str, Any]) -> Optional[List[Any]]:
"""
Extract examples from an OpenAPI parameter or Media Type Object (Parameter, Media Type, Schema).

Supports both 'example' (single value) and 'examples' (map of Example Objects).
Returns a list of example values suitable for JSON Schema 'examples' keyword.
"""
examples = []

# Handle single 'example' field
if "example" in obj and obj["example"] is not None:
examples.append(obj["example"])

# Handle 'examples' map (OpenAPI 3.0+)
if "examples" in obj and isinstance(obj["examples"], dict):
for example_obj in obj["examples"].values():
if isinstance(example_obj, dict) and "$ref" in example_obj:
example_obj = self._resolve_ref_obj(example_obj, set()) or {}
if isinstance(example_obj, dict):
# Example Object can have 'value' or 'externalValue'
if "value" in example_obj:
examples.append(example_obj["value"])
# Note: externalValue is a URI reference, we skip it as it's not inline

return examples if examples else None

def _create_auth_from_scheme(self, scheme: Dict[str, Any], scheme_name: str) -> Optional[Auth]:
"""Creates an Auth object from an OpenAPI security scheme."""
Expand Down Expand Up @@ -417,7 +443,7 @@ def _create_tool(self, path: str, method: str, operation: Dict[str, Any], base_u

call_template = HttpCallTemplate(
name=self.call_template_name,
http_method=method.upper(),
http_method=cast(Literal["GET", "POST", "PUT", "DELETE", "PATCH"], method.upper()),
url=full_url,
body_field=body_field if body_field else None,
header_fields=header_fields if header_fields else None,
Expand Down Expand Up @@ -466,10 +492,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
if param.get("in") == "body":
body_field = "body"
json_schema = self._resolve_ref_obj(param.get("schema", {}), set()) or {}
properties[body_field] = {

# Extract examples from body parameter
body_examples = self._extract_examples(param)

prop = {
"description": param.get("description", "Request body"),
**json_schema,
}
if body_examples:
prop["examples"] = body_examples

properties[body_field] = prop
if param.get("required"):
required.append(body_field)
continue
Expand All @@ -484,10 +518,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
schema["items"] = param.get("items")
if "enum" in param:
schema["enum"] = param.get("enum")
properties[param_name] = {

# Extract examples from parameter
param_examples = self._extract_examples(param)

prop = {
"description": param.get("description", ""),
**schema,
}
if param_examples:
prop["examples"] = param_examples

properties[param_name] = prop
if param.get("required"):
required.append(param_name)

Expand All @@ -497,13 +539,22 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
content = request_body.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
json_schema = self._resolve_ref_obj(json_schema, set()) if json_schema else None

# Extract examples from request body media type
media_type_obj = content.get("application/json", {})
body_examples = self._extract_examples(media_type_obj)

if json_schema:
# Add a single 'body' field to represent the request body
body_field = "body"
properties[body_field] = {
prop = {
"description": json_schema.get("description", "Request body"),
**json_schema
}
if body_examples:
prop["examples"] = body_examples

properties[body_field] = prop
if json_schema.get("required"):
required.append(body_field)

Expand All @@ -518,14 +569,17 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
return JsonSchema()

json_schema = None
media_type_obj = None
if "content" in success_response:
content = success_response.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
media_type_obj = content.get("application/json", {})
# Fallback to any content type if application/json missing
if json_schema is None and isinstance(content, dict):
for v in content.values():
if isinstance(v, dict) and "schema" in v:
json_schema = v.get("schema")
media_type_obj = v
break
elif "schema" in success_response: # OpenAPI 2.0
json_schema = success_response.get("schema")
Expand All @@ -536,6 +590,13 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
# Resolve $ref in response schema
json_schema = self._resolve_ref_obj(json_schema, set()) or {}

# Extract examples from response media type and schema level
response_examples = list(self._extract_examples(media_type_obj) or []) if media_type_obj else []
for ex in self._extract_examples(json_schema) or []:
if ex not in response_examples:
response_examples.append(ex)
response_examples = response_examples or None

schema_args = {
"type": json_schema.get("type", "object"),
"properties": json_schema.get("properties", {}),
Expand All @@ -552,5 +613,9 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
for attr in ["enum", "minimum", "maximum", "format"]:
if attr in json_schema:
schema_args[attr] = json_schema.get(attr)

# Add examples if present
if response_examples:
schema_args["examples"] = response_examples

return JsonSchema(**schema_args)
162 changes: 162 additions & 0 deletions plugins/communication_protocols/http/tests/test_openapi_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,165 @@ async def test_openapi_converter_with_auth_tools():

# Verify auth_tools is stored
assert converter.auth_tools == auth_tools


def test_openapi_converter_parameter_examples():
"""Test that parameter examples are correctly extracted from OpenAPI spec."""
# Create a minimal OpenAPI spec with parameter examples
openapi_spec = {
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/users/{userId}": {
"get": {
"operationId": "getUser",
"parameters": [
{
"name": "userId",
"in": "path",
"description": "ID of the user",
"required": True,
"schema": {
"type": "string"
},
"example": "user123"
},
{
"name": "includeDetails",
"in": "query",
"description": "Include detailed information",
"required": False,
"schema": {
"type": "boolean"
},
"examples": {
"trueExample": {
"summary": "Include details",
"value": True
},
"falseExample": {
"summary": "Exclude details",
"value": False
}
}
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"}
}
},
"examples": {
"userExample": {
"summary": "Example user",
"value": {
"id": "user123",
"name": "John Doe"
}
}
}
}
}
}
}
}
},
"/users": {
"post": {
"operationId": "createUser",
"requestBody": {
"description": "User to create",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
},
"required": ["name", "email"]
},
"examples": {
"newUser": {
"summary": "New user example",
"value": {
"name": "Jane Smith",
"email": "jane@example.com"
}
}
}
}
}
},
"responses": {
"201": {
"description": "User created",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"email": {"type": "string"}
}
}
}
}
}
}
}
}
}
}

converter = OpenApiConverter(openapi_spec)
manual = converter.convert()

assert len(manual.tools) == 2

# Test getUser tool - path and query parameter examples
get_user_tool = next((tool for tool in manual.tools if tool.name == "getUser"), None)
assert get_user_tool is not None

# Check path parameter example
user_id_param = get_user_tool.inputs.properties.get("userId")
assert user_id_param is not None
assert user_id_param.examples is not None
assert "user123" in user_id_param.examples

# Check query parameter examples
include_details_param = get_user_tool.inputs.properties.get("includeDetails")
assert include_details_param is not None
assert include_details_param.examples is not None
assert True in include_details_param.examples
assert False in include_details_param.examples

# Check response examples
assert get_user_tool.outputs.examples is not None
assert len(get_user_tool.outputs.examples) > 0
example_value = get_user_tool.outputs.examples[0]
assert example_value["id"] == "user123"
assert example_value["name"] == "John Doe"

# Test createUser tool - request body examples
create_user_tool = next((tool for tool in manual.tools if tool.name == "createUser"), None)
assert create_user_tool is not None

body_param = create_user_tool.inputs.properties.get("body")
assert body_param is not None
assert body_param.examples is not None
assert len(body_param.examples) > 0
example_value = body_param.examples[0]
assert example_value["name"] == "Jane Smith"
assert example_value["email"] == "jane@example.com"