Skip to main content

openapi-to-mcp

The openapi-to-mcp plugin exposes an existing HTTP API to MCP (Model Context Protocol) clients. It generates tools from the API's OpenAPI document, serves the MCP endpoint from the gateway, and calls the corresponding API operations on behalf of clients. The plugin supports custom upstream headers and both Streamable HTTP and Server-Sent Events (SSE) transports.

The openapi-to-mcp plugin is available in both APISIX and API7 Enterprise. In APISIX, it generates tools and invokes APIs inside the gateway. In API7 Enterprise, the gateway plugin forwards MCP requests to a separate OpenAPI-to-MCP service. The two implementations share the basic route configuration but differ in deployment, caching, header forwarding, and supported options.

The following sequence shows the native APISIX request flow:

Examples​

In the tabbed examples, the Admin API commands target APISIX, ADC can target APISIX or API7 Gateway, and the Ingress Controller resources apply to either gateway. Complete Prepare API7 Gateway before using an API7 Gateway workflow.

The Petstore examples use version 1.0.27 of the official Swagger Petstore image. The fixture serves both the OpenAPI document and the API. Its manifest digest is pinned so tool discovery and invocation remain reproducible.

Start Petstore​

Start the fixture for the deployment type used by the gateway:

Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container, then place the gateway and Petstore on a dedicated network. In an API7 Gateway deployment, the OpenAPI-to-MCP service must share the gateway's network namespace as described in Prepare API7 Gateway.

export GATEWAY_CONTAINER=replace-with-gateway-container-name

docker network inspect gateway-petstore-net > /dev/null 2>&1 || \
docker network create gateway-petstore-net

if ! docker inspect "$GATEWAY_CONTAINER" \
--format '{{json .NetworkSettings.Networks}}' \
| grep -q '"gateway-petstore-net"'; then
docker network connect gateway-petstore-net "$GATEWAY_CONTAINER"
fi

Create a Compose file for the pinned Petstore fixture on the shared network:

docker-compose.petstore.yaml
services:
petstore:
image: swaggerapi/petstore3:1.0.27@sha256:749432676c31cd05256f0d9e878302164debb94daa1c1223e193747e1b34e7d1
platform: linux/amd64
networks:
- gateway-petstore-net
ports:
- "18080:8080"

networks:
gateway-petstore-net:
external: true

Start or reconcile the fixture with a fixed Compose project name so the command can be rerun across these guides:

docker compose -p api7-docs-petstore \
-f docker-compose.petstore.yaml up -d

Once the container has started, confirm that the OpenAPI document is available:

curl -fsS "http://127.0.0.1:18080/api/v3/openapi.json" > /dev/null

Prepare API7 Gateway​

API7 Gateway forwards MCP requests to a separate OpenAPI-to-MCP service. Native APISIX runs the MCP implementation inside the gateway and does not require this step.

Starting with API7 Enterprise 3.9.10, the service is no longer bundled in the gateway image. Deploy it in the same network namespace as the gateway:

  • Kubernetes (Helm): Set openapiToMcp.enabled: true in the gateway chart values to run the compatible service image as a sidecar.
  • Docker or bare metal: Run api7/openapi-to-mcp:1.0.6 and share the gateway's network namespace, for example with --network=container:<gateway> or host networking. A shared Docker bridge network is insufficient because the plugin connects to 127.0.0.1:3000 by default.

A refused connection to the service normally returns 502. This also applies to mcp-tools-acl. To use another port, see Static Configurations.

Expose Petstore as MCP Tools​

This example exposes the pinned Petstore fixture through a stateless MCP endpoint. The Admin API tab configures native APISIX; ADC and Ingress Controller can configure APISIX or API7 Gateway.

Create a route with the openapi-to-mcp plugin:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "openapi-to-mcp-route",
"uri": "/mcp",
"methods": ["POST"],
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"base_url": "http://petstore:8080/api/v3",
"headers": {
"Authorization": "special-key"
},
"openapi_url": "http://petstore:8080/api/v3/openapi.json"
}
}
}'

❶ Configure the route to accept POST requests for MCP messages such as tools/list and tools/call.

❷ Configure the transport method to be streamable_http (recommended for production).

❸ Configure the Petstore API address.

❹ Add an illustrative header to requests sent to the Petstore API.

❺ Configure the Petstore OpenAPI document URL.

List the generated tools. Streamable HTTP clients must accept both JSON and event-stream responses:

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

The response should contain a tools list that includes getPetById.

After applying the Admin API, ADC, or Ingress Controller configuration, update your MCP settings with the gateway address and append the previously created route path. For instance:

mcp.json
{
"mcpServers": {
"api7-petstore-mcp": {
"url": "http://123.123.123.123:9080/mcp"
}
}
}

If the configuration is successful, you should see the available tools (external functions or services exposed to AI clients through MCP).

You can now interact with the Petstore service directly from the chat window of your AI client. For example, try asking: "Show me pet 1 from the petstore."

AI client interaction with Petstore

Configure Authentication for MCP Routes​

The following example demonstrates how to expose Petstore APIs through the MCP protocol when the route is protected by an authentication method such as key-auth.

Create a consumer johndoe:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "johndoe"
}'

Configure the key-auth credential for johndoe:

curl "http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-key-auth",
"plugins": {
"key-auth": {
"key": "john-key"
}
}
}'

Create a route with the openapi-to-mcp and key-auth plugins:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "openapi-to-mcp-route",
"uri": "/mcp",
"methods": ["POST"],
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"base_url": "http://petstore:8080/api/v3",
"headers": {
"Authorization": "special-key"
},
"openapi_url": "http://petstore:8080/api/v3/openapi.json"
},
"key-auth": {
"header": "apikey"
}
}
}'

When an MCP server requires authentication, you can specify headers in the mcp.json configuration. Refer to the documentation of your AI client to verify whether headers are supported.

If Headers Are Supported​

After applying the Admin API, ADC, or Ingress Controller configuration, update Cursor with the gateway address and the route path created above. Include the header required by key-auth:

mcp.json
{
"mcpServers": {
"api7-petstore-mcp": {
"url": "http://123.123.123.123:9080/mcp",
"headers": {
"apikey": "john-key"
}
}
}
}

The configured headers will be added to every POST request.

If the configuration is successful, you should see the available tools (external functions or services exposed to AI clients through MCP). You can then interact with Petstore directly from the chat window of your AI client.

If the authentication header is not configured in mcp.json, the AI client will be unable to load tools from the MCP server.

If Headers Are Not Supported​

If your AI client does not support configuring headers in mcp.json, you can include the authentication credential in the MCP URL query, since key-auth supports obtaining credential from the URL query.

Update the key-auth configuration on the route as such:

curl "http://127.0.0.1:9180/apisix/admin/routes/openapi-to-mcp-route" -X PATCH \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"plugins": {
"key-auth": {
"query": "apikey"
}
}
}'

Authenticate every MCP request. Streamable HTTP uses POST for initialization, discovery, and tool calls. Use a client that sends the credential on every request; if it does not retain query credentials, configure a supported authentication header instead.

The example reads apikey from the query. The client must keep that credential on subsequent requests as well.

After applying the Admin API, ADC, or Ingress Controller configuration, include the credential in the gateway address query parameter:

mcp.json
{
"mcpServers": {
"api7-petstore-mcp": {
"url": "http://123.123.123.123:9080/mcp?apikey=john-key"
}
}
}

If the configuration is successful, you should see the available tools (external functions or services exposed to AI clients through MCP). You can then interact with Petstore directly from the chat window of your AI client.

If the authentication credential is not configured in the MCP server URL query, the AI client will be unable to load tools from the MCP server.

Flatten Tool Schema Parameters​

The following example demonstrates how flatten_parameters affects the structure of query and path parameters in the generated MCP tool input schema.

Complete the previous example using Admin API, ADC, or Ingress Controller to set up MCP access to the Petstore APIs. Although the configuration does not explicitly set flatten_parameters, the parameter defaults to false.

In your AI client, such as Cursor, inspect the tool input schema. You should see parameters nested under pathParameters and queryParameters:

{
"operations": {
...,
"getPetById": {
"method": "GET",
"path": "/pet/{petId}",
"pathParameters": {
"type": "object",
"required": ["petId"],
"properties": {
"petId": {
"type": "integer",
"description": "ID of pet to return"
}
},
"additionalProperties": false
}
},
"findPetsByStatus": {
"method": "GET",
"path": "/pet/findByStatus",
"queryParameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["available", "pending", "sold"],
"description": "Status values that need to be considered for filter",
"default": "available"
}
},
"additionalProperties": false
}
}
}
}

Update the plugin to flatten query and path parameters:

curl "http://127.0.0.1:9180/apisix/admin/routes/openapi-to-mcp-route" -X PATCH \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"plugins": {
"openapi-to-mcp": {
"flatten_parameters": true
}
}
}'

In your AI client, such as Cursor, inspect the tool input schema. You should see that parameters like status are no longer nested under pathParameters or queryParameters:

{
"operations": {
...,
"getPetById": {
"parameters": {
"type": "object",
"required": ["petId"],
"properties": {
"petId": {
"type": "integer",
"description": "ID of pet to return"
}
},
"additionalProperties": false
}
},
"findPetsByStatus": {
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["available", "pending", "sold"],
"description": "Status values that need to be considered for filter",
"default": "available"
}
},
"additionalProperties": false
}
}
}
}

Customize MCP Tool Annotations​

Availability

MCP tool annotations were introduced in API7 Enterprise 3.9.7 and APISIX 3.19.0.

The following example demonstrates how to add MCP tool annotations to OpenAPI operations exposed by the openapi-to-mcp plugin.

Without these annotations, AI clients only receive the generated tool name, description, and input schema. They cannot reliably tell whether a tool is read-only, destructive, or idempotent, which makes it harder to rank tools correctly and use them safely.

This is implemented in the bundled OpenAPI-to-MCP converter in two ways:

  1. It infers default tool behavior from the HTTP method.
  2. It reads explicit operation-level configuration from the OpenAPI vendor extension x-mcp-annotations.

When both are present, explicit x-mcp-annotations values override the inferred defaults.

Complete the previous example using Admin API, ADC, or Ingress Controller to expose an OpenAPI document through the openapi-to-mcp plugin, then add annotations to the OpenAPI operations:

The pinned Petstore fixture serves a fixed OpenAPI document. To apply x-mcp-annotations, host a modified copy of the document and update openapi_url to point to it.

openapi.yaml
paths:
/users/{id}:
get:
operationId: getUser
summary: Get user information
x-mcp-annotations:
title: Get User
readOnlyHint: true
openWorldHint: false
delete:
operationId: deleteUser
summary: Delete a user
x-mcp-annotations:
title: Delete User
destructiveHint: true

Supported annotation fields:

  • title
  • readOnlyHint
  • destructiveHint
  • idempotentHint
  • openWorldHint

If x-mcp-annotations is not configured, the converter still applies default inference rules:

  • GET, HEAD, and OPTIONS map to readOnlyHint: true
  • DELETE maps to destructiveHint: true and idempotentHint: true
  • PUT maps to idempotentHint: true

After updating the hosted OpenAPI document, ask your MCP client to list tools. The following snippet shows the result.tools portion of the tools/list response:

{
"tools": [
{
"name": "getUser",
"annotations": {
"title": "Get User",
"readOnlyHint": true,
"openWorldHint": false
}
},
{
"name": "deleteUser",
"annotations": {
"title": "Delete User",
"destructiveHint": true,
"idempotentHint": true
}
}
]
}

Notes:

  • Only operation-level x-mcp-annotations is supported.
  • Invalid values and unsupported fields are ignored.
  • summary and description still control the generated tool description.
  • title is only read from x-mcp-annotations.title.

Enable MCP Access to API7 Enterprise APIs​

The following example demonstrates how to expose API7 Enterprise APIs through the MCP protocol, enabling AI models and clients to interact with your API7 Enterprise configuration.

Create a route with the openapi-to-mcp plugin:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "openapi-to-mcp-route",
"uri": "/mcp",
"methods": ["POST"],
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"base_url": "https://your-dashboard.com",
"headers": {
"X-API-KEY": "<API7_ENTERPRISE_API_KEY>"
},
"openapi_url": "https://run.api7.ai/api7-ee/openapi-latest.json"
}
}
}'

❶ Configure the route to accept POST requests for MCP messages such as tools/list and tools/call.

❷ Configure the transport method to be streamable_http (recommended for production).

❸ Replace with your API7 Enterprise address, where requests will be forwarded.

❹ Replace with your credential in the X-API-KEY header for API7 Enterprise authentication.

❺ Configure the URL of the API7 Enterprise OpenAPI document.

In your AI client, such as Cursor, update the MCP settings with your gateway address and append the previously created route path. For instance:

mcp.json
{
"mcpServers": {
"api7-enterprise-mcp": {
"url": "http://123.123.123.123:9080/mcp"
}
}
}

If the configuration is successful, you should see the available tools (external functions or services exposed to AI clients through MCP).

You can now interact with API7 Enterprise directly from the chat window of your AI client. For example, try asking: "How many gateway groups are there in API7 Enterprise?"

AI client interaction with API7 Enterprise

API7 Gateway Service Configuration​

The following settings apply to the separate OpenAPI-to-MCP service used by API7 Gateway. They do not apply to the native APISIX implementation.

Forward Dynamic Headers with API7 Gateway​

Some upstream APIs require per-request credentials or context, such as user-specific API tokens, tenant identifiers, or session IDs. Pass values that differ between MCP clients to the upstream with the x-openapi2mcp-header-* convention.

The OpenAPI-to-MCP sidecar extracts any request header that matches x-openapi2mcp-header-{name}. It removes the prefix and forwards the value to the upstream API in the {name} header.

For example, a header x-openapi2mcp-header-my-token: abc123 in the client request becomes my-token: abc123 in the upstream API request.

How It Works​

The gateway plugin and the OpenAPI-to-MCP sidecar work together to forward headers:

  1. Plugin-level headers: Headers configured in the plugin headers field are resolved at the gateway and forwarded as x-openapi2mcp-header-{name} to the sidecar. These headers are shared across all clients, but their values can vary per request when using built-in variables.
  2. Client-level headers (dynamic): Headers set by the MCP client in mcp.json using the x-openapi2mcp-header-* prefix are passed through the gateway to the sidecar, then forwarded to the upstream. These can vary per client.

When both static plugin headers and dynamic client headers are present, they are merged. If a client header has the same name as a plugin header, the plugin header takes precedence and the client value is ignored.

Transport-Specific Behavior​

The behavior of dynamic headers depends on the transport method configured in the plugin:

  • streamable_http (recommended): Every MCP request is independent and stateless. The sidecar reads x-openapi2mcp-header-* headers on each request, so dynamic headers are truly per-request. This is the recommended transport for dynamic header passthrough.
  • sse: The x-openapi2mcp-header-* headers are only read during the initial GET request that establishes the SSE connection. Subsequent POST requests within the same session do not re-read these headers. As a result, dynamic headers are fixed for the entire session and cannot be changed mid-session.

If your use case requires different header values across requests (for example, per-user tokens that change), use streamable_http transport.

Configure Client Headers​

If your MCP client supports custom headers (such as Cursor or Claude Desktop), add x-openapi2mcp-header-* entries to the headers field in mcp.json:

mcp.json
{
"mcpServers": {
"my-api-mcp": {
"url": "http://123.123.123.123:9080/mcp",
"headers": {
"x-openapi2mcp-header-authorization": "Bearer <user-token>",
"x-openapi2mcp-header-x-tenant-id": "tenant-42"
}
}
}
}

When the MCP client sends a tools/call request, the sidecar extracts these headers and forwards them to the upstream API as:

authorization: Bearer <user-token>
x-tenant-id: tenant-42

Header Name Mapping​

The sidecar extracts lowercase header names after the x-openapi2mcp-header- prefix for the upstream request. The following table summarizes the mapping:

Client headerUpstream header
x-openapi2mcp-header-authorizationauthorization
x-openapi2mcp-header-x-api-keyx-api-key
x-openapi2mcp-header-my-tokenmy-token
note

The x-openapi2mcp-header-* headers are consumed by the sidecar and are not forwarded to the upstream as-is. Only the extracted header names and values are sent to the upstream.

Security Considerations​

Any x-openapi2mcp-header-* header sent by the MCP client is forwarded to the upstream API after prefix stripping. This means clients can inject arbitrary headers into upstream requests. To mitigate risks:

  • Use gateway-level authentication plugins (such as key-auth or jwt-auth) to restrict access to the MCP route, ensuring only authorized clients can send requests.
  • If the upstream API relies on specific headers for authentication or authorization, ensure those headers are set in the plugin-level headers configuration rather than relying on client-provided values, since plugin-level headers take precedence over client-level headers.

Configure OpenAPI Document Caching​

The Helm chart pins a compatible service image. The cache_enabled and cache_ttl route options require service image 1.0.5 or later.

The service caches each OpenAPI document and its generated tools by the openapi_url string. Entries expire after 3600 seconds by default. Document content and HTTP cache headers do not invalidate an entry, so change the URL, for example with a version query parameter, to load an update immediately.

API7 Enterprise 3.9.21 and 3.10.8 introduced per-route cache_enabled and cache_ttl settings, which default to true and 3600. With service image 1.0.5 or later, route settings take precedence over the container environment variables below. Disable caching on a route while its document changes frequently.

For Docker and bare-metal deployments, configure cache defaults on the OpenAPI-to-MCP container:

VariableDefaultDescription
CACHE_ENABLEDtrueSet to false to disable caching. Every request then downloads and parses the document again, which increases latency.
CACHE_TTL3600Seconds an entry stays in the cache.

The Helm chart does not expose these environment variables. Helm deployments use the defaults or the per-route plugin settings.

Add the Service to Docker Compose​

When you add a gateway instance in the Dashboard, it generates a ready-to-use docker-compose.yaml. Keep the generated gateway service unchanged and add the OpenAPI-to-MCP service:

docker-compose.yaml
services:
openapi-to-mcp:
image: api7/openapi-to-mcp:1.0.6
network_mode: "service:gateway"
restart: always

The network_mode setting makes the service share the gateway service's network stack so the plugin can reach it at 127.0.0.1:3000. If the generated gateway service has another name, replace gateway with that name. A shared Docker bridge network is not sufficient.

Start the services:

docker compose up -d

Troubleshooting​

For native APISIX, inspect the gateway error log. A specification-fetch or parsing failure prevents the plugin from building the tools list. Verify that the URL returns an OpenAPI document containing API paths, rather than an HTML error page or a document containing only components.

The following logging guidance and known issues apply to the API7 Enterprise OpenAPI-to-MCP service.

The separate service writes errors to standard error. For the Docker Compose example, inspect its logs with docker compose logs openapi-to-mcp. For a Helm deployment, inspect the openapi-to-mcp sidecar with kubectl logs <gateway-pod> -c openapi-to-mcp. These logs are separate from the gateway error log.

Known Issues​

  1. The error Cannot use 'in' operator to search for '$ref' in undefined typically occurs when an OpenAPI v2 document is used in openapi_url. The plugin only supports OpenAPI v3 document in openapi_url.

  2. The plugin has a known parsing issue when handling oneOf schemas in OpenAPI v3 document retrieved from openapi_url. In this case, the MCP client will be stuck at tool loading.