Skip to content

feat(openapi-to-mcp): advertise outputSchema and restrict base_url with allowed_hosts - #13957

Draft
AlinsRan wants to merge 5 commits into
apache:masterfrom
AlinsRan:feat/openapi-to-mcp-output-schema
Draft

AlinsRan wants to merge 5 commits into
apache:masterfrom
AlinsRan:feat/openapi-to-mcp-output-schema

Conversation

@AlinsRan

@AlinsRan AlinsRan commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Two independent changes to openapi-to-mcp, one commit each, both found by driving the plugin with the official MCP SDK client and comparing the result against what the OpenAPI document promises. They are unrelated and can be evaluated separately.

Based on #13956; the first commit shown here is that PR's. Please review this one after it merges, at which point this branch is rebased onto master.


1. outputSchema and structuredContent (commit feat(openapi-to-mcp): advertise outputSchema and return structuredContent)

An operation whose success response is described as a JSON object currently tells an MCP client nothing about the shape of what it will get back, and the answer always arrives as an opaque text block.

The generated tool now advertises that schema as outputSchema, and a call whose answer satisfies it returns the API body as structuredContent, with the same body as the text block.

Which response the schema comes from, most specific first: 200, 201, any other explicit 2xx, then 2XX. Only an application/json schema that is an object with properties qualifies — an array, a default response, a bare type: object and a composition the document leaves unmerged (allOf) advertise nothing, since a client could not bind to them.

The error path deserves attention in review. MCP requires a tool that declares an outputSchema to return structured content unless the result is an error, so an answer that cannot satisfy the schema — a status outside 2xx, a body that is not a JSON object, or one that does not validate — is returned as an error result carrying the usual {status, statusText, headers, data} envelope. Returning that envelope on its own would leave an SDK client with MCP error -32600: Tool ... has an output schema but did not return structured content instead of a readable failure; the behaviour here is deliberately the compliant one. Tools that advertise no outputSchema are untouched and keep returning the envelope whatever the status.

The body is validated the way the client will validate it — the MCP SDKs check structured content with a validator that does not assert format, so asserting it here would reject bodies the client would have accepted.

Tests: response selection and the cases that qualify or do not, in t/plugin/openapi-to-mcp-tools-generator.t; end to end, that only the qualifying operations advertise a schema, that the advertised schema is the declared one, that a matching answer comes back as structured content and that a 404 comes back as an error result.

2. allowed_hosts (commit feat(openapi-to-mcp): add allowed_hosts to restrict where base_url may point)

base_url accepts variables, and http://${http_x_backend} is a documented configuration. Nothing constrained what it resolved to, so such a route makes the gateway a usable relay to any host it can reach.

The new allowed_hosts attribute takes exact host names and *.example.com wildcards that match one or more leading labels. When it is set, the host is checked before the document is fetched and before the API is called; a base_url that is not an http or https URL is rejected too. Both answer 400, and neither the URL nor the host appears in the response or in the log line, because a resolved base_url can carry values taken from the request. Leaving the attribute unset keeps today's behaviour.

Tests: a host outside the list rejected, an exact entry served, a wildcard entry matching a sub-domain but not the bare domain, a non-http URL rejected, and the schema rejecting an empty list and an entry that is a URL rather than a host.


Both changes are documented in docs/en/latest/plugins/openapi-to-mcp.md and the Chinese page.

…penAPI documents

Three fixes to the contract the plugin documents, found by driving it with the
official MCP SDK client.

Defaults declared in the document were applied after the arguments had been
validated, so a parameter or a body property that is both `required` and has a
`default` was rejected as missing and the call never reached the API. They are
now filled in before validation, and an argument the client sent is never
replaced.

With the SSE transport, `${...}` in `base_url` or in `headers` was resolved
again on every message POST. That request carries only the session id -- the
endpoint handed to the client is `<path>?sessionId=<uuid>` -- so a header such
as `Bearer ${http_x_api_token}` resolved to `Bearer ` and a variable base_url
failed to resolve at all. The values are now resolved once, on the request that
opens the stream, stored with the session and used for every message on it.

Any JSON or YAML document parsed, so a route pointed at the wrong URL -- an
error page, an index document, a spec that failed to render -- served an empty
tool list and looked healthy. The route's own document must now declare an
`openapi` or `swagger` version and a `paths` object; a document pulled in by an
external `$ref` is a fragment and is still accepted as before.

Adds tests for each, and documents the default handling, the SSE resolution
point and the query serialization the plugin performs.
…tent

When the document describes an operation's success response as a JSON object,
the generated tool now advertises that schema as `outputSchema`, and a call
whose answer satisfies it returns the API body as `structuredContent` with the
same body as the text block. The response is taken from `200`, then `201`, then
any other explicit `2xx`, then `2XX`; only an `application/json` object schema
with properties qualifies, so an array, a `default` response and an unmerged
composition advertise nothing.

An answer that cannot satisfy the schema -- a status outside `2xx`, a body that
is not a JSON object, or one that does not validate -- is returned as an error
result carrying the usual envelope. MCP requires a tool that declares an
`outputSchema` to produce structured content unless the result is an error, so
returning the envelope on its own would leave a client that enforces the schema
with a protocol error instead of a readable failure.

The body is validated the way the client will validate it: the MCP SDKs check
structured content with a validator that does not assert `format`, so asserting
it here would reject bodies the client would have accepted.
…y point

`base_url` accepts variables, so a route can be written whose upstream host
comes from the request -- `http://${http_x_backend}` is a documented
configuration. Nothing constrained what that resolved to, which makes the
gateway a usable relay to any host reachable from it.

`allowed_hosts` takes exact host names and `*.example.com` wildcards matching
one or more leading labels. When it is set, the host the `base_url` resolves to
is checked before the document is fetched and before the API is called, and a
`base_url` that is not an `http` or `https` URL is rejected as well; both
answer 400. Neither the URL nor the host appears in the rejection or in the log
line, because a resolved `base_url` can carry values taken from the request.

Leaving the attribute unset keeps the previous behaviour.
YAML reads an unquoted version as a number: `swagger: 2.0` and `openapi: 3.1`
both come back as one, and only a version with two dots such as `3.0.0` is a
string. The document check accepted a string alone, so it turned away the
form Swagger 2.0 documents are usually written in. JSON documents were never
affected, since JSON quotes the value.

The check now takes either, and the loader tests cover all four shapes.
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.

1 participant