Skip to main content
Version: Next

openapi-to-mcp

Description#

The openapi-to-mcp Plugin exposes an existing HTTP API to Model Context Protocol (MCP) clients, such as LLM agents, without changing the API. It fetches the API's OpenAPI document, generates one MCP tool per operation, and answers the MCP protocol itself. When a client calls a tool, the Plugin sends the corresponding HTTP request to the API and returns the response as the tool result.

The MCP server runs inside APISIX. No additional process or service is required.

The Plugin supports:

  • The Streamable HTTP transport (stateless) and the HTTP+SSE transport.
  • MCP protocol versions 2024-10-07, 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, negotiated during initialize.
  • The initialize, ping, tools/list and tools/call methods.
  • OpenAPI 3.x documents in JSON or YAML. Internal and http(s) $ref references are resolved. Swagger 2.0 documents are read on a best-effort basis: in: body and in: formData parameters are not turned into tool inputs.

Attributes#

NameTypeRequiredDefaultValid valuesDescription
transportstringFalsesse[sse, streamable_http]MCP transport served on the Route.
openapi_urlstringTrueURL of the OpenAPI document. The document is fetched on the first request and the generated tools are cached for an hour.
base_urlstringTrueBase URL of the API the tools call. The path of each operation is appended to it. Supports APISIX variables and NGINX variables, for example http://${http_x_backend}.
headersobjectFalseHeaders added to every request sent to the API. Values support variables, for example "Authorization": "Bearer ${http_x_api_token}".
flatten_parametersbooleanFalsefalseWhen false, the tool input nests parameters under pathParameters, queryParameters and headerParameters. When true, they are placed directly in the input object.

Tool call arguments are validated against the generated input schema before the API is called. A call to an unknown tool, or with invalid arguments, returns a result with isError set to true.

When a tool is called, the Plugin builds the request from the operation:

  • Parameters declared on the Path Item apply to every operation under it; an operation parameter with the same name and location overrides them.
  • Query parameters are serialized according to their style and explode, as defined by the OpenAPI Parameter Object. With the defaults (form, exploded), tags: ["a", "b"] is sent as tags=a&tags=b. spaceDelimited, pipeDelimited and deepObject are supported.
  • A request body is sent with the media type the operation declares, unless headers sets Content-Type.

For the SSE transport, sessions are kept in the mcp-session shared dict, so the stream and the message requests of one session may be handled by different worker processes. Sessions are local to one APISIX instance: when several instances run behind a load balancer, the requests of an SSE session must reach the same instance. The Streamable HTTP transport is stateless and has no such requirement.

Example usage#

The examples below use a Route with the ID mcp. An admin key is required for the Admin API calls:

admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')

Serve an API over Streamable HTTP#

Create a Route that serves the tools of the Swagger Petstore API:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "mcp",
"uri": "/mcp",
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"openapi_url": "https://petstore3.swagger.io/api/v3/openapi.json",
"base_url": "https://petstore3.swagger.io/api/v3"
}
}
}'

List the tools:

curl "http://127.0.0.1:9080/mcp" -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The response is a single SSE event carrying the JSON-RPC result:

event: message
data: {"result":{"tools":[{"name":"updatePet","description":"Update an existing pet by Id", ...}]},"jsonrpc":"2.0","id":1}

Call a tool:

curl "http://127.0.0.1:9080/mcp" -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "findPetsByStatus",
"arguments": { "queryParameters": { "status": "sold" } }
}
}'

The tool result carries the status, status text, headers and body the API returned, as JSON text:

event: message
data: {"result":{"content":[{"type":"text","text":"{\n \"status\": 200,\n \"statusText\": \"OK\", ..."}]},"jsonrpc":"2.0","id":2}

An MCP client connects to http://127.0.0.1:9080/mcp using its Streamable HTTP transport.

Serve an API over SSE#

With transport set to sse, or left unset, a client opens the stream with a GET request. The first event tells it where to send its messages:

curl -N "http://127.0.0.1:9080/mcp"
event: endpoint
data: /mcp?sessionId=4c9b0a4e-1bb0-4f4d-9b0b-2f3c3e0f7a51

The client then POSTs each JSON-RPC message to that endpoint, receives 202 Accepted, and reads the answer from the stream.

Forward credentials to the API#

Pass the caller's token through to the API by reading it from a request header:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "mcp",
"uri": "/mcp",
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"openapi_url": "https://petstore3.swagger.io/api/v3/openapi.json",
"base_url": "https://petstore3.swagger.io/api/v3",
"headers": {
"Authorization": "Bearer ${http_x_api_token}"
}
}
}
}'

Other Plugins on the Route keep working. For example, key-auth or limit-count run before the MCP request is answered, and a request they reject never reaches the tools.

Delete Plugin#

To remove the openapi-to-mcp Plugin, delete it from the Route configuration. APISIX reloads the configuration automatically:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "mcp",
"uri": "/mcp",
"plugins": {},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'