Skip to content

fix(openapi-converter): stop dropping tool inputs on non-JSON request bodies and content-form parameters - #106

Open
AmirF194 wants to merge 2 commits into
universal-tool-calling-protocol:mainfrom
AmirF194:fix/98-openapi-converter-input-data-loss
Open

fix(openapi-converter): stop dropping tool inputs on non-JSON request bodies and content-form parameters#106
AmirF194 wants to merge 2 commits into
universal-tool-calling-protocol:mainfrom
AmirF194:fix/98-openapi-converter-input-data-loss

Conversation

@AmirF194

@AmirF194 AmirF194 commented Sep 7, 2026

Copy link
Copy Markdown

This covers the two gaps in _extract_inputs from the issue; the third gap (OAS2 response examples sibling map, in _extract_outputs) is a separate function and stays out of this PR.

Root cause 1: for the OAS3 requestBody, _extract_inputs read only content["application/json"], with no fallback to any other media type. _extract_outputs already falls back to the first schema-bearing media type for responses; _extract_inputs never got the same treatment. A body declared only under application/xml, text/plain, or a vendor +json subtype produced a tool with no body input field at all.

Root cause 2: an OAS3 Parameter Object may carry its schema under a content map (content: {<media-type>: {schema, example}}) instead of a top-level schema key, for parameters that need a non-default media type. _extract_inputs only read param["schema"], so a content-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 a content-form branch for parameters that falls back to the first media type entry when schema is absent, feeding its schema and examples into the same _merge_examples path 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 current main (missing/empty body/filter input) and pass on this branch, run in a clean python:3.11-slim and python:3.13-slim container (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-modules flag 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 honors requestBody.required.

Bug Fixes

  • Request bodies fall back to the first schema-bearing media type when application/json is absent, and that media type now reaches the call template so bodies aren't sent as JSON.
  • Parameters using the OAS3 content map now pick up the schema and examples from the first media type entry.
  • The body input is required whenever requestBody.required is true, not just when the schema lists required properties.
  • The third gap from OpenAPI converter: pre-existing data-loss gaps in schema/example extraction #98 (OAS2 response examples sibling map) stays for a separate PR.
  • Adds regression tests for the fixed behaviors.

Written for commit 2c15915. Summary will update on new commits.

Review in cubic

… 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
Copilot AI lite review requested due to automatic review settings September 7, 2026 19:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@AmirF194

AmirF194 commented Sep 7, 2026

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants