Repository navigation
feat(openapi-to-mcp): advertise outputSchema and restrict base_url with allowed_base_url_hosts - #13957
Draft
AlinsRan wants to merge 9 commits into
Draft
feat(openapi-to-mcp): advertise outputSchema and restrict base_url with allowed_base_url_hosts#13957AlinsRan wants to merge 9 commits into
AlinsRan wants to merge 9 commits into
Conversation
…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.
…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.
…se_url may point `base_url` accepts variables, so a route can be written whose upstream host comes from the request. Nothing constrained what that resolved to, which makes the gateway a usable relay to any host reachable from it. `allowed_base_url_hosts` takes entries in the same form as `allowed_ref_hosts` and is matched with the same code: a hostname or `*.example.com` wildcard, optionally followed by `:port`. When it is set, the resolved `base_url` is checked in before_proxy, after the Origin check, and a request whose host is outside the list, or whose `base_url` is not an http(s) URL, is answered with 400. Neither the URL nor the host appears in the rejection or in the log line. Leaving the attribute unset keeps the previous behaviour.
The first commit (apache#13956) and the YAML version fix were merged to master separately; this keeps only the output schema and base_url host work on top of master. allowed_hosts is renamed to allowed_base_url_hosts and shares the entry format and matcher of allowed_ref_hosts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent changes to
openapi-to-mcp, 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.This branch was stacked on #13956. That PR and the YAML version fix are now on
master, so the branch has been merged withmasterand carries only the two changes below (git diff master...shows just them).1.
outputSchemaandstructuredContentAn 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 asstructuredContent, with the same body as the text block.Which response the schema comes from, most specific first:
200,201, any other explicit2xx, then2XX. Only anapplication/jsonschema that is an object with properties qualifies: an array, adefaultresponse, a baretype: objectand 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
outputSchemato return structured content unless the result is an error, so an answer that cannot satisfy the schema (a status outside2xx, 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 leaves an SDK client withMCP error -32600: Tool ... has an output schema but did not return structured content. Tools that advertise nooutputSchemaare untouched.The body is validated the way the client will validate it: the MCP SDKs do not assert
format, so the check schema drops theformatkeyword. It is built once per tool when the tool list is generated, socore.schema.checkcompiles it once instead of on every call, and a property that happens to be namedformatis kept.Tests:
t/plugin/openapi-to-mcp-tools-generator.t: response selection, the cases that qualify or do not, and the check schema.t/plugin/openapi-to-mcp.t: only qualifying operations advertise a schema, the advertised schema is the declared one, a matching answer comes back as structured content, a 404 comes back as an error result.t/plugin/openapi-to-mcp-interop.spec.ts: the official SDK client accepts the structured content and gets an error result for the 404. Returning the envelope withoutisErrormakes this case fail with the-32600above.2.
allowed_base_url_hostsbase_urlaccepts variables, so a route can be written whose upstream host comes from the request. The security section of the docs warns against that, but 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_base_url_hostsattribute uses the same entry format asallowed_ref_hostsand the same matcher (openapi/ref.lua): a hostname or*.example.comwildcard, optionally followed by:port; without a port it matches any port. When it is set, the resolvedbase_urlis checked inbefore_proxy, after the Origin check, so lower-priority access plugins still run first. A host outside the list, or abase_urlthat is not anhttp(s)URL, is answered with 400. Neither the URL nor the host appears in the response or in the log line, because a resolvedbase_urlcan carry values taken from the request. Leaving the attribute unset keeps today's behaviour.Tests: a host outside the list, or on another port, rejected; a
host:portentry served; a wildcard entry matching a sub-domain but not the bare domain; a non-http URL rejected without being logged; the schema rejecting an empty list.Both changes are documented in
docs/en/latest/plugins/openapi-to-mcp.mdand the Chinese page.