Skip to content
Closed
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
29 changes: 26 additions & 3 deletions apisix/plugins/mcp-bridge.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ function _M.check_schema(conf, schema_type)
end


local function build_stderr_notification(content)
local message, err = core.json.encode({
jsonrpc = "2.0",
method = "notifications/stderr",
params = {
content = content,
},
})

if message then
return message
end

core.log.warn("failed to encode MCP stderr notification: ", err)
return '{"jsonrpc":"2.0","method":"notifications/stderr","params":{"content":""}}'
end


-- Exported for testability; not part of the public API.
_M.build_stderr_notification = build_stderr_notification


local function on_connect(conf, ctx)
return function(additional)
local proc, err = pipe.spawn({conf.command, unpack(conf.args or {})})
Expand Down Expand Up @@ -110,15 +132,16 @@ local function on_connect(conf, ctx)
line, _, stderr_partial = proc:stderr_read_line()
if line then
local ok, err = server.transport:send(
'{"jsonrpc":"2.0","method":"notifications/stderr","params":{"content":"'
.. (stderr_partial and stderr_partial .. line or line) .. '"}}')
build_stderr_notification(
stderr_partial and stderr_partial .. line or line
))
if not ok then
core.log.info("session ", server.session_id,
" exit, failed to send response message: ", err)
need_exit = true
break
end
stderr_partial = "" -- luacheck: ignore
stderr_partial = nil -- luacheck: ignore
end
until not line
if need_exit then
Expand Down
1 change: 1 addition & 0 deletions docs/en/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
"plugins/dubbo-proxy",
"plugins/mqtt-proxy",
"plugins/kafka-proxy",
"plugins/mcp-bridge",
"plugins/http-dubbo"
]
}
Expand Down
135 changes: 135 additions & 0 deletions docs/en/latest/plugins/mcp-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: mcp-bridge
keywords:
- Apache APISIX
- API Gateway
- Plugin
- mcp-bridge
- MCP
description: The mcp-bridge Plugin exposes a stdio-based MCP server through HTTP SSE endpoints.
---

<!--
#
# 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.
#
-->

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

## Description

The `mcp-bridge` Plugin exposes a stdio-based Model Context Protocol
(MCP) server through HTTP Server-Sent Events (SSE) endpoints. When a
client connects to the SSE endpoint, APISIX starts the configured MCP
server process, forwards client messages to the process through stdin,
and streams MCP responses back to the client.

For a Route configured with `base_uri: /mcp`, the Plugin exposes:

- `GET /mcp/sse`: the SSE endpoint used by MCP clients.
- `POST /mcp/message?sessionId=<session-id>`: the endpoint used to send
client messages to the MCP session.

Only configure commands that you trust. The configured process runs on
the same host as APISIX and is managed by the APISIX worker handling the
MCP session.

## Attributes

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `base_uri` | string | False | `""` | URI prefix for the generated MCP SSE and message endpoints. |
| `command` | string | True | | Command used to start the stdio-based MCP server. |
| `args` | array[string] | False | | Arguments passed to `command`. |

## Examples

The following example demonstrates how to expose a stdio-based MCP
server through APISIX.

:::note

You can fetch the `admin_key` from `config.yaml` and save to an
environment variable with the following command:

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

:::

### Expose an MCP Server

Create a Route with the `mcp-bridge` Plugin:

<Tabs groupId="api">
<TabItem value="admin-api" label="Admin API">

```shell
curl "http://127.0.0.1:9180/apisix/admin/routes/mcp-bridge" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/mcp/*",
"plugins": {
"mcp-bridge": {
"base_uri": "/mcp",
"command": "node",
"args": [
"path/to/mcp-server.js"
]
}
}
}'
```

</TabItem>
<TabItem value="adc" label="ADC">

```yaml title="adc.yaml"
services:
- name: mcp-bridge-service
routes:
- name: mcp-bridge-route
uris:
- /mcp/*
plugins:
mcp-bridge:
base_uri: /mcp
command: node
args:
- path/to/mcp-server.js
```

Synchronize the configuration to the gateway:

```shell
adc sync -f adc.yaml
```

</TabItem>
</Tabs>

Connect an MCP client to the SSE endpoint:

```text
http://127.0.0.1:9080/mcp/sse
```

The Plugin sends an `endpoint` SSE event that contains the message
endpoint for the session. MCP clients use that endpoint to send
JSON-RPC messages back to APISIX.
1 change: 1 addition & 0 deletions docs/zh/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"items": [
"plugins/dubbo-proxy",
"plugins/mqtt-proxy",
"plugins/mcp-bridge",
"plugins/http-dubbo"
]
}
Expand Down
124 changes: 124 additions & 0 deletions docs/zh/latest/plugins/mcp-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
title: mcp-bridge
keywords:
- Apache APISIX
- API 网关
- Plugin
- mcp-bridge
- MCP
description: mcp-bridge 插件用于通过 HTTP SSE 端点暴露基于 stdio 的 MCP 服务器。
---

<!--
#
# 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.
#
-->

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

## 描述

`mcp-bridge` 插件用于通过 HTTP Server-Sent Events(SSE)端点暴露基于 stdio 的 Model Context Protocol(MCP)服务器。当客户端连接到 SSE 端点时,APISIX 会启动配置的 MCP 服务器进程,通过 stdin 将客户端消息转发给该进程,并将 MCP 响应流式返回给客户端。

如果路由配置了 `base_uri: /mcp`,该插件会暴露以下端点:

- `GET /mcp/sse`:MCP 客户端使用的 SSE 端点。
- `POST /mcp/message?sessionId=<session-id>`:用于向 MCP 会话发送客户端消息的端点。

请仅配置你信任的命令。配置的进程会运行在 APISIX 所在主机上,并由处理 MCP 会话的 APISIX worker 管理。

## 插件属性

| 名称 | 类型 | 必选项 | 默认值 | 描述 |
| --- | --- | --- | --- | --- |
| `base_uri` | string | 否 | `""` | 生成 MCP SSE 和消息端点时使用的 URI 前缀。 |
| `command` | string | 是 | | 用于启动基于 stdio 的 MCP 服务器的命令。 |
| `args` | array[string] | 否 | | 传递给 `command` 的参数。 |

## 使用示例

以下示例演示了如何通过 APISIX 暴露一个基于 stdio 的 MCP 服务器。

:::note

你可以使用以下命令从 `config.yaml` 中获取 `admin_key` 并保存到环境变量中:

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

:::

### 暴露 MCP 服务器

创建一个配置了 `mcp-bridge` 插件的路由:

<Tabs groupId="api">
<TabItem value="admin-api" label="Admin API">

```shell
curl "http://127.0.0.1:9180/apisix/admin/routes/mcp-bridge" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"uri": "/mcp/*",
"plugins": {
"mcp-bridge": {
"base_uri": "/mcp",
"command": "node",
"args": [
"path/to/mcp-server.js"
]
}
}
}'
```

</TabItem>
<TabItem value="adc" label="ADC">

```yaml title="adc.yaml"
services:
- name: mcp-bridge-service
routes:
- name: mcp-bridge-route
uris:
- /mcp/*
plugins:
mcp-bridge:
base_uri: /mcp
command: node
args:
- path/to/mcp-server.js
```

将配置同步到网关:

```shell
adc sync -f adc.yaml
```

</TabItem>
</Tabs>

将 MCP 客户端连接到 SSE 端点:

```text
http://127.0.0.1:9080/mcp/sse
```

插件会发送一个 `endpoint` SSE 事件,该事件包含当前会话的消息端点。MCP 客户端使用该端点将 JSON-RPC 消息发送回 APISIX。
27 changes: 27 additions & 0 deletions t/plugin/mcp-bridge.t
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,30 @@ 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 notification escapes JSON string content
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local plugin = require("apisix.plugins.mcp-bridge")
local content = "quote: \" backslash: \\ newline:\nend"
local message = plugin.build_stderr_notification(content)
local decoded, err = core.json.decode(message)

if not decoded then
ngx.say(err)
return
end

ngx.say(decoded.jsonrpc)
ngx.say(decoded.method)
ngx.say(decoded.params.content == content)
}
}
--- response_body
2.0
notifications/stderr
true
Loading