mirror of
https://github.com/alibaba/higress.git
synced 2026-03-08 02:30:56 +08:00
feat: add mcp-router plugin (#2409)
This commit is contained in:
14
plugins/wasm-go/mcp-filters/Dockerfile
Normal file
14
plugins/wasm-go/mcp-filters/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
# Use a minimal base image as we only need to store the wasm file.
|
||||
FROM scratch
|
||||
|
||||
# Add build argument for the filter name. This will be passed by the Makefile.
|
||||
ARG FILTER_NAME
|
||||
|
||||
# Copy the compiled WASM binary into the image's root directory.
|
||||
# The wasm file will be named after the filter.
|
||||
COPY ${FILTER_NAME}/main.wasm /plugin.wasm
|
||||
|
||||
# Metadata
|
||||
LABEL org.opencontainers.image.title="${FILTER_NAME}"
|
||||
LABEL org.opencontainers.image.description="Higress MCP filter - ${FILTER_NAME}"
|
||||
LABEL org.opencontainers.image.source="https://github.com/alibaba/higress"
|
||||
54
plugins/wasm-go/mcp-filters/Makefile
Normal file
54
plugins/wasm-go/mcp-filters/Makefile
Normal file
@@ -0,0 +1,54 @@
|
||||
# MCP Filter Makefile
|
||||
|
||||
# Variables
|
||||
FILTER_NAME ?= mcp-router
|
||||
REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
|
||||
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
|
||||
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
IMAGE_TAG = $(if $(strip $(FILTER_VERSION)),${FILTER_VERSION},${BUILD_TIME}-${COMMIT_ID})
|
||||
IMG ?= ${REGISTRY}${FILTER_NAME}:${IMAGE_TAG}
|
||||
|
||||
# Default target
|
||||
.DEFAULT: build
|
||||
|
||||
build:
|
||||
@echo "Building WASM binary for filter: ${FILTER_NAME}..."
|
||||
@if [ ! -d "${FILTER_NAME}" ]; then \
|
||||
echo "Error: Filter directory '${FILTER_NAME}' not found."; \
|
||||
exit 1; \
|
||||
fi
|
||||
cd ${FILTER_NAME} && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm main.go
|
||||
@echo ""
|
||||
@echo "Output WASM file: ${FILTER_NAME}/main.wasm"
|
||||
|
||||
# Build Docker image (depends on build target to ensure WASM binary exists)
|
||||
build-image: build
|
||||
@echo "Building Docker image for ${FILTER_NAME}..."
|
||||
docker build -t ${IMG} \
|
||||
--build-arg FILTER_NAME=${FILTER_NAME} \
|
||||
-f Dockerfile .
|
||||
@echo ""
|
||||
@echo "Image: ${IMG}"
|
||||
|
||||
# Build and push Docker image
|
||||
build-push: build-image
|
||||
docker push ${IMG}
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning build artifacts for filter: ${FILTER_NAME}..."
|
||||
rm -f ${FILTER_NAME}/main.wasm
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build WASM binary for a specific filter"
|
||||
@echo " build-image - Build Docker image"
|
||||
@echo " build-push - Build and push Docker image"
|
||||
@echo " clean - Remove build artifacts for a specific filter"
|
||||
@echo ""
|
||||
@echo "Variables:"
|
||||
@echo " FILTER_NAME - Name of the MCP filter to build (default: ${FILTER_NAME})"
|
||||
@echo " REGISTRY - Docker registry (default: ${REGISTRY})"
|
||||
@echo " FILTER_VERSION - Version tag for the image (default: timestamp-commit)"
|
||||
@echo " IMG - Full image name (default: ${IMG})"
|
||||
89
plugins/wasm-go/mcp-filters/mcp-router/README.md
Normal file
89
plugins/wasm-go/mcp-filters/mcp-router/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# MCP Router Plugin
|
||||
|
||||
## Feature Description
|
||||
The `mcp-router` plugin provides a routing capability for MCP (Model Context Protocol) `tools/call` requests. It inspects the tool name in the request payload, and if the name is prefixed with a server identifier (e.g., `server-name/tool-name`), it dynamically reroutes the request to the appropriate backend MCP server.
|
||||
|
||||
This enables the creation of a unified MCP endpoint that can aggregate tools from multiple, distinct MCP servers. A client can make a `tools/call` request to a single endpoint, and the `mcp-router` will ensure it reaches the correct underlying server where the tool is actually hosted.
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
|-----------|---------------|----------|---------------|---------------------------------------------------------------------------------------------------------|
|
||||
| `servers` | array of objects | Yes | - | A list of routing configurations for each backend MCP server. |
|
||||
| `servers[].name` | string | Yes | - | The unique identifier for the MCP server. This must match the prefix used in the `tools/call` request's tool name. |
|
||||
| `servers[].domain` | string | No | - | The domain (authority) of the backend MCP server. If omitted, the original request's domain will be kept. |
|
||||
| `servers[].path` | string | Yes | - | The path of the backend MCP server to which the request will be routed. |
|
||||
|
||||
## How It Works
|
||||
|
||||
When a `tools/call` request is processed by a route with the `mcp-router` plugin enabled, the following occurs:
|
||||
|
||||
1. **Tool Name Parsing**: The plugin inspects the `name` parameter within the `params` object of the JSON-RPC request.
|
||||
2. **Prefix Matching**: It checks if the tool name follows the `server-name/tool-name` format.
|
||||
- If it does not match this format, the plugin takes no action, and the request proceeds normally.
|
||||
- If it matches, the plugin extracts the `server-name` and the actual `tool-name`.
|
||||
3. **Route Lookup**: The extracted `server-name` is used to look up the corresponding routing configuration (domain and path) from the `servers` list in the plugin's configuration.
|
||||
4. **Header Modification**:
|
||||
- The `:authority` request header is replaced with the `domain` from the matched server configuration.
|
||||
- The `:path` request header is replaced with the `path` from the matched server configuration.
|
||||
5. **Request Body Modification**: The `name` parameter in the JSON-RPC request body is updated to be just the `tool-name` (the `server-name/` prefix is removed).
|
||||
6. **Rerouting**: After the headers are modified, the gateway's routing engine processes the request again with the new destination information, sending it to the correct backend MCP server.
|
||||
|
||||
### Example Configuration
|
||||
|
||||
Here is an example of how to configure the `mcp-router` plugin in a `higress-plugins.yaml` file:
|
||||
|
||||
```yaml
|
||||
servers:
|
||||
- name: random-user-server
|
||||
domain: mcp.example.com
|
||||
path: /mcp-servers/mcp-random-user-server
|
||||
- name: rest-amap-server
|
||||
domain: mcp.example.com
|
||||
path: /mcp-servers/mcp-rest-amap-server
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
|
||||
Consider a `tools/call` request sent to an endpoint where the `mcp-router` is active:
|
||||
|
||||
**Original Request:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "rest-amap-server/get-weather",
|
||||
"arguments": {
|
||||
"location": "New York"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Plugin Actions:**
|
||||
|
||||
1. The plugin identifies the tool name as `rest-amap-server/get-weather`.
|
||||
2. It extracts `server-name` as `rest-amap-server` and `tool-name` as `get-weather`.
|
||||
3. It finds the matching configuration: `domain: mcp.example.com`, `path: /mcp-servers/mcp-rest-amap-server`.
|
||||
4. It modifies the request headers to:
|
||||
- `:authority`: `mcp.example.com`
|
||||
- `:path`: `/mcp-servers/mcp-rest-amap-server`
|
||||
5. It modifies the request body to:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "get-weather",
|
||||
"arguments": {
|
||||
"location": "New York"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The request is then rerouted to the `rest-amap-server`.
|
||||
89
plugins/wasm-go/mcp-filters/mcp-router/README_ZH.md
Normal file
89
plugins/wasm-go/mcp-filters/mcp-router/README_ZH.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# MCP Router 插件
|
||||
|
||||
## 功能说明
|
||||
`mcp-router` 插件为 MCP (Model Context Protocol) 的 `tools/call` 请求提供了路由能力。它会检查请求负载中的工具名称,如果名称带有服务器标识符前缀(例如 `server-name/tool-name`),它会动态地将请求重新路由到相应的后端 MCP 服务器。
|
||||
|
||||
这使得创建一个统一的 MCP 端点成为可能,该端点可以聚合来自多个不同 MCP 服务器的工具。客户端可以向单个端点发出 `tools/call` 请求,`mcp-router` 将确保请求到达托管该工具的正确底层服务器。
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|---|---|---|---|---|
|
||||
| `servers` | 对象数组 | 是 | - | 每个后端 MCP 服务器的路由配置列表。 |
|
||||
| `servers[].name` | 字符串 | 是 | - | MCP 服务器的唯一标识符。这必须与 `tools/call` 请求的工具名称中使用的前缀相匹配。 |
|
||||
| `servers[].domain` | 字符串 | 否 | - | 后端 MCP 服务器的域名 (authority)。如果省略,将保留原始请求的域名。 |
|
||||
| `servers[].path` | 字符串 | 是 | - | 请求将被路由到的后端 MCP 服务器的路径。 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
当一个启用了 `mcp-router` 插件的路由处理 `tools/call` 请求时,会发生以下情况:
|
||||
|
||||
1. **工具名称解析**:插件检查 JSON-RPC 请求中 `params` 对象的 `name` 参数。
|
||||
2. **前缀匹配**:它检查工具名称是否遵循 `server-name/tool-name` 格式。
|
||||
- 如果不匹配此格式,插件不执行任何操作,请求将正常继续。
|
||||
- 如果匹配,插件将提取 `server-name` 和实际的 `tool-name`。
|
||||
3. **路由查找**:提取的 `server-name` 用于从插件配置的 `servers` 列表中查找相应的路由配置(domain 和 path)。
|
||||
4. **Header 修改**:
|
||||
- `:authority` 请求头被替换为匹配的服务器配置中的 `domain`。
|
||||
- `:path` 请求头被替换为匹配的服务器配置中的 `path`。
|
||||
5. **请求体修改**:JSON-RPC 请求体中的 `name` 参数被更新为仅包含 `tool-name`(移除了 `server-name/` 前缀)。
|
||||
6. **重新路由**:在 Header 修改后,网关的路由引擎会使用新的目标信息再次处理请求,将其发送到正确的后端 MCP 服务器。
|
||||
|
||||
### 配置示例
|
||||
|
||||
以下是在 `higress-plugins.yaml` 文件中配置 `mcp-router` 插件的示例:
|
||||
|
||||
```yaml
|
||||
servers:
|
||||
- name: random-user-server
|
||||
domain: mcp.example.com
|
||||
path: /mcp-servers/mcp-random-user-server
|
||||
- name: rest-amap-server
|
||||
domain: mcp.example.com
|
||||
path: /mcp-servers/mcp-rest-amap-server
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
假设一个 `tools/call` 请求被发送到激活了 `mcp-router` 的端点:
|
||||
|
||||
**原始请求:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "rest-amap-server/get-weather",
|
||||
"arguments": {
|
||||
"location": "New York"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**插件行为:**
|
||||
|
||||
1. 插件识别出工具名称为 `rest-amap-server/get-weather`。
|
||||
2. 它提取出 `server-name` 为 `rest-amap-server`,`tool-name` 为 `get-weather`。
|
||||
3. 它找到匹配的配置:`domain: mcp.example.com`, `path: /mcp-servers/mcp-rest-amap-server`。
|
||||
4. 它将请求头修改为:
|
||||
- `:authority`: `mcp.example.com`
|
||||
- `:path`: `/mcp-servers/mcp-rest-amap-server`
|
||||
5. 它将请求体修改为:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "get-weather",
|
||||
"arguments": {
|
||||
"location": "New York"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
请求随后被重新路由到 `rest-amap-server`。
|
||||
34
plugins/wasm-go/mcp-filters/mcp-router/go.mod
Normal file
34
plugins/wasm-go/mcp-filters/mcp-router/go.mod
Normal file
@@ -0,0 +1,34 @@
|
||||
module mcp-router
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250612125225-016b165a33c9
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
71
plugins/wasm-go/mcp-filters/mcp-router/go.sum
Normal file
71
plugins/wasm-go/mcp-filters/mcp-router/go.sum
Normal file
@@ -0,0 +1,71 @@
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250612125225-016b165a33c9 h1:MBIjh29Qie+jmPQ9W61wOzyUoulk/lsOjdj6hoYTRpo=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250612125225-016b165a33c9/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b h1:rRI9+ThQbe+nw4jUiYEyOFaREkXCMMW9k1X2gy2d6pE=
|
||||
github.com/higress-group/gjson_template v0.0.0-20250413075336-4c4161ed428b/go.mod h1:rU3M+Tq5VrQOo0dxpKHGb03Ty0sdWIZfAH+YCOACx/Y=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0 h1:Ta+RBsZYML3hjoenbGJoS2L6aWJN+hqlxKoqzj/Y2SY=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250402062734-d50d98c305f0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
133
plugins/wasm-go/mcp-filters/mcp-router/main.go
Normal file
133
plugins/wasm-go/mcp-filters/mcp-router/main.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
mcp.LoadMCPFilter(
|
||||
mcp.FilterName("mcp-router"),
|
||||
mcp.SetConfigParser(ParseConfig),
|
||||
mcp.SetToolCallRequestFilter(ProcessRequest),
|
||||
)
|
||||
mcp.InitMCPFilter()
|
||||
}
|
||||
|
||||
// ServerConfig represents the routing configuration for a single MCP server
|
||||
type ServerConfig struct {
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// McpRouterConfig represents the configuration for the mcp-router filter
|
||||
type McpRouterConfig struct {
|
||||
Servers []ServerConfig `json:"servers"`
|
||||
}
|
||||
|
||||
func ParseConfig(configBytes []byte, filterConfig *any) error {
|
||||
var config McpRouterConfig
|
||||
if err := json.Unmarshal(configBytes, &config); err != nil {
|
||||
return fmt.Errorf("failed to parse mcp-router config: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Parsed mcp-router config with %d servers", len(config.Servers))
|
||||
for _, server := range config.Servers {
|
||||
log.Debugf("Server: %s -> %s%s", server.Name, server.Domain, server.Path)
|
||||
}
|
||||
|
||||
*filterConfig = config
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessRequest(context wrapper.HttpContext, config any, toolName string, toolArgs gjson.Result, rawBody []byte) types.Action {
|
||||
routerConfig, ok := config.(McpRouterConfig)
|
||||
if !ok {
|
||||
log.Errorf("Invalid config type for mcp-router")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
// Extract server name from tool name (format: "serverName/toolName")
|
||||
parts := strings.SplitN(toolName, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Debugf("Tool name '%s' does not contain server prefix, continuing without routing", toolName)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
serverName := parts[0]
|
||||
actualToolName := parts[1]
|
||||
|
||||
log.Debugf("Routing tool call: server=%s, tool=%s", serverName, actualToolName)
|
||||
|
||||
// Find the server configuration
|
||||
var targetServer *ServerConfig
|
||||
for _, server := range routerConfig.Servers {
|
||||
if server.Name == serverName {
|
||||
targetServer = &server
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetServer == nil {
|
||||
log.Warnf("No routing configuration found for server '%s'", serverName)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
log.Infof("Routing to server '%s': domain=[%s], path=[%s]", serverName, targetServer.Domain, targetServer.Path)
|
||||
|
||||
// Modify the :authority header (domain) if it's configured
|
||||
if targetServer.Domain != "" {
|
||||
if err := proxywasm.ReplaceHttpRequestHeader(":authority", targetServer.Domain); err != nil {
|
||||
log.Errorf("Failed to set :authority header to '%s': %v", targetServer.Domain, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
// Modify the :path header
|
||||
if err := proxywasm.ReplaceHttpRequestHeader(":path", targetServer.Path); err != nil {
|
||||
log.Errorf("Failed to set :path header to '%s': %v", targetServer.Path, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
// Create a new JSON with the modified tool name
|
||||
modifiedBody, err := sjson.SetBytes(rawBody, "params.name", actualToolName)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to modify tool name, body: %s, err: %v", rawBody, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
// Replace the request body
|
||||
if err := proxywasm.ReplaceHttpRequestBody([]byte(modifiedBody)); err != nil {
|
||||
log.Errorf("Failed to replace request body: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
log.Infof("Successfully routed request for tool '%s' to server '%s'. New tool name is '%s'.",
|
||||
toolName, serverName, actualToolName)
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -8,7 +8,7 @@ replace amap-tools => ../amap-tools
|
||||
|
||||
require (
|
||||
amap-tools v0.0.0-00010101000000-000000000000
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250528033743-f88b782fe131
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250612122351-913048186a5e
|
||||
quark-search v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
|
||||
@@ -6,16 +6,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a h1:CvTkMBU9+SGIyJEJYFEvg/esoVbLzQP9WVeoZzMHM9E=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250507130917-ed12a186173a/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250513083230-017f47fc2432 h1:Acw2RhWABsw3Mg+agBhKJML+Fk5CbDBJcVhM9HM2lmk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250513083230-017f47fc2432/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250515035738-c8f491db9030 h1:CX3lqAbgKnsrNpLYlfi6xDmnyMKsU8NJcMCCaci8BUI=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250515035738-c8f491db9030/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250526122106-bde03cd884e5 h1:ACvlY5Vu7SN+K1posB3UP3l4G+Iw5+6iMcAEaBKJvH8=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250526122106-bde03cd884e5/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250528033743-f88b782fe131 h1:/efvKhP31Qo4RE48mjJCNC1jpVObgAohNe23bN5hFPA=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250528033743-f88b782fe131/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250612122351-913048186a5e h1:MQapJm3X6I0uRuuKjE2LoAqF3hS2Sb4XxNCllrMLhus=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.4-0.20250612122351-913048186a5e/go.mod h1:yObZXF1xTx/8peEsSbtHIzz7KlTr/tZCrokIHtwF0Jk=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
|
||||
Reference in New Issue
Block a user