fix(openapi-converter): stop dropping tool inputs on non-JSON request bodies and content-form parameters - #106
Conversation
… bodies and content-form parameters
_extract_inputs hard-coded content['application/json'] for the request body,
with no fallback to any other media type, unlike _extract_outputs which
already falls back to the first schema-bearing media type. A requestBody
declared only under a non-JSON media type (application/xml, text/plain, a
vendor +json subtype) produced a tool with no body input at all.
Separately, _extract_inputs only read a parameter's top-level 'schema' key,
so an OAS3 Parameter Object using the content form (content: {<media-type>:
{schema, example}}) instead of 'schema' lost both its schema and its
examples.
Scoped to _extract_inputs; the third gap in universal-tool-calling-protocol#98 (OAS2 response examples
sibling map in _extract_outputs) is not touched here.
Refs universal-tool-calling-protocol#98
There was a problem hiding this comment.
2 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="plugins/communication_protocols/http/src/utcp_http/openapi_converter.py">
<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/openapi_converter.py:649">
P2: When a content-form query parameter uses `application/json`, this branch exposes an object input but drops its serialization metadata. Retain the media type in the generated call template and JSON-serialize that query value.</violation>
<violation number="2" location="plugins/communication_protocols/http/src/utcp_http/openapi_converter.py:688">
P1: When the fallback selects a non-JSON media type, the generated tool still sends `application/json` because the selected media type is not propagated. Preserve the media type and use JSON encoding for `+json` types.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if json_schema is None and isinstance(content, dict): | ||
| for candidate_media_type_obj in content.values(): | ||
| if isinstance(candidate_media_type_obj, dict) and "schema" in candidate_media_type_obj: | ||
| media_type_obj = candidate_media_type_obj |
There was a problem hiding this comment.
P1: When the fallback selects a non-JSON media type, the generated tool still sends application/json because the selected media type is not propagated. Preserve the media type and use JSON encoding for +json types.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/http/src/utcp_http/openapi_converter.py, line 688:
<comment>When the fallback selects a non-JSON media type, the generated tool still sends `application/json` because the selected media type is not propagated. Preserve the media type and use JSON encoding for `+json` types.</comment>
<file context>
@@ -667,12 +677,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
+ if json_schema is None and isinstance(content, dict):
+ for candidate_media_type_obj in content.values():
+ if isinstance(candidate_media_type_obj, dict) and "schema" in candidate_media_type_obj:
+ media_type_obj = candidate_media_type_obj
+ json_schema = candidate_media_type_obj.get("schema")
+ break
</file context>
| # e.g. for parameters that need a media type other than the implicit one. | ||
| for media_type_obj_candidate in param["content"].values(): | ||
| if isinstance(media_type_obj_candidate, dict): | ||
| param_content_obj = media_type_obj_candidate |
There was a problem hiding this comment.
P2: When a content-form query parameter uses application/json, this branch exposes an object input but drops its serialization metadata. Retain the media type in the generated call template and JSON-serialize that query value.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/http/src/utcp_http/openapi_converter.py, line 649:
<comment>When a content-form query parameter uses `application/json`, this branch exposes an object input but drops its serialization metadata. Retain the media type in the generated call template and JSON-serialize that query value.</comment>
<file context>
@@ -639,6 +639,16 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
+ # e.g. for parameters that need a media type other than the implicit one.
+ for media_type_obj_candidate in param["content"].values():
+ if isinstance(media_type_obj_candidate, dict):
+ param_content_obj = media_type_obj_candidate
+ schema = self._resolve_ref_obj(param_content_obj.get("schema", {}), set()) or {}
+ break
</file context>
…onor requestBody.required cubic-dev-ai flagged two follow-on gaps in the fallback added for universal-tool-calling-protocol#98: the fallback picked a non-JSON media type for the body but never told the call template, so the tool still sent it as application/json; and the required check read json_schema.get("required") (the body object's own required properties) instead of requestBody.get("required") (whether the body itself may be omitted), so a required body with no property-level required list stayed optional. Threads the resolved media type into HttpCallTemplate's existing content_type field and switches the required check to the outer flag. Two new tests, 241/241 pass in a clean python:3.12-slim container.
|
Fixed #2 and #3, both real. The body-fallback picked application/xml but never told the call template, so it would still have sent application/json; content_type now carries through from HttpCallTemplate's existing field. The required check was reading json_schema.get("required") (the body's own required properties) instead of requestBody.get("required") (whether the body itself is optional), so a required body with a schema like {"type": "object"} stayed optional. Pushed 2c15915 with a test for each, 241/241 pass. #1 is real too but I'm leaving it out of this PR: the fix belongs in call_tool's query-param serialization (a different file, applies to every query param the connector sends, not just content-form ones), and aiohttp already treats list-valued params as repeated query params, so a blanket JSON-encode there would need to special-case that instead of just wrapping dicts. Wanted to look at it properly rather than rush it in here. |
This covers the two gaps in
_extract_inputsfrom the issue; the third gap (OAS2 responseexamplessibling map, in_extract_outputs) is a separate function and stays out of this PR.Root cause 1: for the OAS3
requestBody,_extract_inputsread onlycontent["application/json"], with no fallback to any other media type._extract_outputsalready falls back to the first schema-bearing media type for responses;_extract_inputsnever got the same treatment. A body declared only underapplication/xml,text/plain, or a vendor+jsonsubtype produced a tool with nobodyinput field at all.Root cause 2: an OAS3 Parameter Object may carry its schema under a
contentmap (content: {<media-type>: {schema, example}}) instead of a top-levelschemakey, for parameters that need a non-default media type._extract_inputsonly readparam["schema"], so acontent-form parameter lost both its schema and its examples.Fix: mirror
_extract_outputs's existing media-type fallback in the request-body branch, and add acontent-form branch for parameters that falls back to the first media type entry whenschemais absent, feeding its schema and examples into the same_merge_examplespath the rest of the function already uses.Verified: added two regression tests (
test_openapi_converter_request_body_falls_back_to_first_schema_bearing_media_type,test_openapi_converter_parameter_content_form_schema_and_examples); both fail on currentmain(missing/emptybody/filterinput) and pass on this branch, run in a cleanpython:3.11-slimandpython:3.13-slimcontainer (the matrix's low and high Python versions) alongside the plugin's full existing test suite (240 passed, unchanged). Coverage confirms every changed line is exercised by the new tests. Not verified: Windows/macOS (no runner available here); the CI matrix's--doctest-modulesflag doesn't touch this file since no doctests were added.Refs #98
Summary by cubic
Fixes the OpenAPI converter dropping tool inputs for non-JSON request bodies and
content-form parameters, and now propagates the resolved body media type and honorsrequestBody.required.Bug Fixes
application/jsonis absent, and that media type now reaches the call template so bodies aren't sent as JSON.contentmap now pick up the schema and examples from the first media type entry.bodyinput is required wheneverrequestBody.requiredis true, not just when the schema lists required properties.examplessibling map) stays for a separate PR.Written for commit 2c15915. Summary will update on new commits.