Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions apisix/plugins/mcp-bridge.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,33 @@ function _M.check_schema(conf, schema_type)
end


-- build a notifications/stderr JSON-RPC message from a subprocess stderr line.
-- core.json.encode is used so that special characters in the content are
-- escaped and the message stays well-formed. It is exposed on _M so the
-- behaviour can be exercised directly in tests.
function _M.build_stderr_notification(content)
local msg, encode_err = core.json.encode({
jsonrpc = "2.0",
method = "notifications/stderr",
params = {
content = content,
},
})
if not msg then
core.log.error("failed to encode stderr notification: ", encode_err)
-- the fallback content is a plain ASCII string, so it always encodes
msg = core.json.encode({
jsonrpc = "2.0",
method = "notifications/stderr",
params = {
content = "failed to encode stderr content",
},
})
end
return msg
end


local function on_connect(conf, ctx)
return function(additional)
local proc, err = pipe.spawn({conf.command, unpack(conf.args or {})})
Expand Down Expand Up @@ -109,9 +136,9 @@ local function on_connect(conf, ctx)
local line, _
line, _, stderr_partial = proc:stderr_read_line()
if line then
local content = stderr_partial and stderr_partial .. line or line
local ok, err = server.transport:send(
'{"jsonrpc":"2.0","method":"notifications/stderr","params":{"content":"'
.. (stderr_partial and stderr_partial .. line or line) .. '"}}')
_M.build_stderr_notification(content))
if not ok then
core.log.info("session ", server.session_id,
" exit, failed to send response message: ", err)
Expand Down
3 changes: 2 additions & 1 deletion docs/en/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"plugins/ai-prompt-decorator",
"plugins/ai-prompt-template",
"plugins/ai-rag",
"plugins/ai-request-rewrite"
"plugins/ai-request-rewrite",
"plugins/mcp-bridge"
]
},
{
Expand Down
123 changes: 123 additions & 0 deletions docs/en/latest/plugins/mcp-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: mcp-bridge
keywords:
- Apache APISIX
- API Gateway
- Plugin
- mcp-bridge
- MCP
description: This document contains information about the Apache APISIX mcp-bridge Plugin, which bridges a stdio-based MCP (Model Context Protocol) server to HTTP clients over SSE.
---

<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->

<head>
<link rel="canonical" href="https://docs.api7.ai/hub/mcp-bridge" />
</head>

## Description

The `mcp-bridge` Plugin bridges a stdio-based [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server to HTTP clients. APISIX spawns the MCP server as a subprocess and exposes it over the MCP [SSE transport](https://modelcontextprotocol.io/docs/concepts/transports), so that clients can talk to a local MCP server through the gateway without managing the process themselves.

When a client connects, APISIX starts the configured `command` as a subprocess and relays data between them:

- The subprocess's standard output is forwarded to the client as JSON-RPC messages.
- The subprocess's standard error is forwarded to the client as `notifications/stderr` notifications.
- Messages sent by the client are written to the subprocess's standard input.

:::caution

The `mcp-bridge` Plugin is currently experimental and under active development. Its configuration and behavior may change in future releases.

:::

## Attributes

| Name | Type | Required | Default | Description |
|------------|-----------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------|
| command | string | True | | Command used to start the MCP server subprocess, for example `npx`. The command must be available in the `PATH` of the APISIX process. |
| args | array[string] | False | | List of arguments passed to `command`. |
| base_uri | string | False | "" | Base path under which the SSE and message endpoints are exposed. It should match the prefix of the Route's `uri`. |

With a given `base_uri`, the Plugin serves two endpoints:

- `GET <base_uri>/sse`: establishes the SSE stream and advertises the message endpoint to the client.
- `POST <base_uri>/message?sessionId=<id>`: the endpoint to which the client posts JSON-RPC messages.

The Route should therefore be configured with a wildcard `uri` such as `<base_uri>/*` so that both endpoints are matched.

## Example usage

The following example bridges the [filesystem MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) and exposes it under `/mcp`.

Create a Route with the `mcp-bridge` Plugin. The `uri` uses a wildcard so that both `/mcp/sse` and `/mcp/message` are matched, and `base_uri` is set to `/mcp`:

```shell
curl -i "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/mcp/*",
"plugins": {
"mcp-bridge": {
"base_uri": "/mcp",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/path/to/serve"
]
}
}
}'
```

Connect to the SSE endpoint to establish a session:

```shell
curl -N "http://127.0.0.1:9080/mcp/sse"
```

```text
event: endpoint
data: /mcp/message?sessionId=0d9...e3a
```

The `data` field contains the message endpoint, including the `sessionId` to use for this session. Using that endpoint, send JSON-RPC requests to the MCP server. For example, to list the available tools:

```shell
curl "http://127.0.0.1:9080/mcp/message?sessionId=0d9...e3a" -X POST \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

The MCP server's responses are streamed back over the SSE connection opened in the previous step.

## Delete Plugin

To remove the `mcp-bridge` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.

```shell
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/mcp/*",
"plugins": {}
}'
```
3 changes: 2 additions & 1 deletion docs/zh/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
"plugins/ai-prompt-decorator",
"plugins/ai-prompt-template",
"plugins/ai-rag",
"plugins/ai-request-rewrite"
"plugins/ai-request-rewrite",
"plugins/mcp-bridge"
]
},
{
Expand Down
123 changes: 123 additions & 0 deletions docs/zh/latest/plugins/mcp-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: mcp-bridge
keywords:
- Apache APISIX
- API 网关
- Plugin
- mcp-bridge
- MCP
description: 本文档包含有关 Apache APISIX mcp-bridge 插件的信息,该插件通过 SSE 将基于 stdio 的 MCP(Model Context Protocol)服务器桥接给 HTTP 客户端。
---

<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->

<head>
<link rel="canonical" href="https://docs.api7.ai/hub/mcp-bridge" />
</head>

## 描述

`mcp-bridge` 插件将基于 stdio 的 [MCP(Model Context Protocol)](https://modelcontextprotocol.io/) 服务器桥接给 HTTP 客户端。APISIX 会将 MCP 服务器作为子进程启动,并通过 MCP 的 [SSE 传输](https://modelcontextprotocol.io/docs/concepts/transports)对外暴露,使客户端无需自行管理进程,即可通过网关与本地 MCP 服务器通信。

当客户端连接时,APISIX 会以子进程方式启动所配置的 `command`,并在两者之间转发数据:

- 子进程的标准输出会作为 JSON-RPC 消息转发给客户端。
- 子进程的标准错误会作为 `notifications/stderr` 通知转发给客户端。
- 客户端发送的消息会写入子进程的标准输入。

:::caution

`mcp-bridge` 插件目前处于实验阶段,仍在积极开发中。其配置和行为在未来版本中可能会发生变化。

:::

## 属性

| 名称 | 类型 | 必选项 | 默认值 | 描述 |
|------------|-----------------|--------|--------|----------------------------------------------------------------------------------------------------------|
| command | string | 是 | | 用于启动 MCP 服务器子进程的命令,例如 `npx`。该命令必须位于 APISIX 进程的 `PATH` 中。 |
| args | array[string] | 否 | | 传递给 `command` 的参数列表。 |
| base_uri | string | 否 | "" | 暴露 SSE 与 message 端点的基础路径,应与路由 `uri` 的前缀保持一致。 |

对于给定的 `base_uri`,插件会提供两个端点:

- `GET <base_uri>/sse`:建立 SSE 流,并向客户端通告 message 端点。
- `POST <base_uri>/message?sessionId=<id>`:客户端用于发送 JSON-RPC 消息的端点。

因此,路由的 `uri` 应配置为通配形式(例如 `<base_uri>/*`),以便同时匹配上述两个端点。

## 使用示例

以下示例桥接 [filesystem MCP 服务器](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem),并将其暴露在 `/mcp` 路径下。

创建一个启用 `mcp-bridge` 插件的路由。`uri` 使用通配符以同时匹配 `/mcp/sse` 与 `/mcp/message`,并将 `base_uri` 设置为 `/mcp`:

```shell
curl -i "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/mcp/*",
"plugins": {
"mcp-bridge": {
"base_uri": "/mcp",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/path/to/serve"
]
}
}
}'
```

连接 SSE 端点以建立会话:

```shell
curl -N "http://127.0.0.1:9080/mcp/sse"
```

```text
event: endpoint
data: /mcp/message?sessionId=0d9...e3a
```

`data` 字段中包含 message 端点,其中携带了本次会话使用的 `sessionId`。使用该端点向 MCP 服务器发送 JSON-RPC 请求。例如,列出可用的工具:

```shell
curl "http://127.0.0.1:9080/mcp/message?sessionId=0d9...e3a" -X POST \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

MCP 服务器的响应会通过上一步建立的 SSE 连接以流的形式返回。

## 删除插件

当你需要删除 `mcp-bridge` 插件时,可以从插件配置中删除对应的 JSON 配置。APISIX 会自动重新加载,无需重启即可生效。

```shell
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/mcp/*",
"plugins": {}
}'
```
34 changes: 34 additions & 0 deletions t/plugin/mcp-bridge.t
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,37 @@ property "command" is required
property "command" validation failed: wrong type: expected string, got number
done
property "args" validation failed: wrong type: expected array, got string



=== TEST 2: stderr content with JSON special characters keeps the notification well-formed
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local pipe = require("ngx.pipe")
local mcp_bridge = require("apisix.plugins.mcp-bridge")

-- a subprocess line that contains double quotes, a backslash and the
-- "}}" sequence, all of which break naive string concatenation
local payload = [[Error: invalid input "x\y" }}]]

local proc = assert(
pipe.spawn({"sh", "-c", [[printf '%s\n' "$1" 1>&2]], "sh", payload}))
proc:set_timeouts(nil, 1000, 1000)
local line = assert(proc:stderr_read_line())
proc:wait()

-- build the notification through the plugin's own code path
local msg = mcp_bridge.build_stderr_notification(line)

local decoded, err = core.json.decode(msg)
if not decoded then
ngx.say("not valid json: ", err)
return
end
ngx.say("valid json: ", decoded.params.content == payload)
}
}
--- response_body
valid json: true
Loading