diff --git a/apisix/plugins/mcp-bridge.lua b/apisix/plugins/mcp-bridge.lua index a73d94393d6c..b747da88ff92 100644 --- a/apisix/plugins/mcp-bridge.lua +++ b/apisix/plugins/mcp-bridge.lua @@ -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 {})}) @@ -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 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 7691e45802e9..c6260c164262 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -250,6 +250,7 @@ "plugins/dubbo-proxy", "plugins/mqtt-proxy", "plugins/kafka-proxy", + "plugins/mcp-bridge", "plugins/http-dubbo" ] } diff --git a/docs/en/latest/plugins/mcp-bridge.md b/docs/en/latest/plugins/mcp-bridge.md new file mode 100644 index 000000000000..19075da28078 --- /dev/null +++ b/docs/en/latest/plugins/mcp-bridge.md @@ -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. +--- + + + +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=`: 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: + + + + +```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" + ] + } + } + }' +``` + + + + +```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 +``` + + + + +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. diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 78ab8ad88718..93a68d402ef8 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -236,6 +236,7 @@ "items": [ "plugins/dubbo-proxy", "plugins/mqtt-proxy", + "plugins/mcp-bridge", "plugins/http-dubbo" ] } diff --git a/docs/zh/latest/plugins/mcp-bridge.md b/docs/zh/latest/plugins/mcp-bridge.md new file mode 100644 index 000000000000..8be0669a5aeb --- /dev/null +++ b/docs/zh/latest/plugins/mcp-bridge.md @@ -0,0 +1,124 @@ +--- +title: mcp-bridge +keywords: + - Apache APISIX + - API 网关 + - Plugin + - mcp-bridge + - MCP +description: mcp-bridge 插件用于通过 HTTP SSE 端点暴露基于 stdio 的 MCP 服务器。 +--- + + + +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=`:用于向 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` 插件的路由: + + + + +```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" + ] + } + } + }' +``` + + + + +```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 +``` + + + + +将 MCP 客户端连接到 SSE 端点: + +```text +http://127.0.0.1:9080/mcp/sse +``` + +插件会发送一个 `endpoint` SSE 事件,该事件包含当前会话的消息端点。MCP 客户端使用该端点将 JSON-RPC 消息发送回 APISIX。 diff --git a/t/plugin/mcp-bridge.t b/t/plugin/mcp-bridge.t index 25369f02f821..5d4d13142adc 100644 --- a/t/plugin/mcp-bridge.t +++ b/t/plugin/mcp-bridge.t @@ -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