diff --git a/plugins/wasm-go/mcp-filters/Dockerfile b/plugins/wasm-go/mcp-filters/Dockerfile new file mode 100644 index 000000000..eab97b499 --- /dev/null +++ b/plugins/wasm-go/mcp-filters/Dockerfile @@ -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" \ No newline at end of file diff --git a/plugins/wasm-go/mcp-filters/Makefile b/plugins/wasm-go/mcp-filters/Makefile new file mode 100644 index 000000000..ee41eb596 --- /dev/null +++ b/plugins/wasm-go/mcp-filters/Makefile @@ -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})" diff --git a/plugins/wasm-go/mcp-filters/mcp-router/README.md b/plugins/wasm-go/mcp-filters/mcp-router/README.md new file mode 100644 index 000000000..5de0f2cd2 --- /dev/null +++ b/plugins/wasm-go/mcp-filters/mcp-router/README.md @@ -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`. diff --git a/plugins/wasm-go/mcp-filters/mcp-router/README_ZH.md b/plugins/wasm-go/mcp-filters/mcp-router/README_ZH.md new file mode 100644 index 000000000..8ca9a5188 --- /dev/null +++ b/plugins/wasm-go/mcp-filters/mcp-router/README_ZH.md @@ -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`。 diff --git a/plugins/wasm-go/mcp-filters/mcp-router/go.mod b/plugins/wasm-go/mcp-filters/mcp-router/go.mod new file mode 100644 index 000000000..14d2e2f25 --- /dev/null +++ b/plugins/wasm-go/mcp-filters/mcp-router/go.mod @@ -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 +) diff --git a/plugins/wasm-go/mcp-filters/mcp-router/go.sum b/plugins/wasm-go/mcp-filters/mcp-router/go.sum new file mode 100644 index 000000000..86e6a6d1c --- /dev/null +++ b/plugins/wasm-go/mcp-filters/mcp-router/go.sum @@ -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= diff --git a/plugins/wasm-go/mcp-filters/mcp-router/main.go b/plugins/wasm-go/mcp-filters/mcp-router/main.go new file mode 100644 index 000000000..f98d57615 --- /dev/null +++ b/plugins/wasm-go/mcp-filters/mcp-router/main.go @@ -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 +} diff --git a/plugins/wasm-go/mcp-servers/all-in-one/go.mod b/plugins/wasm-go/mcp-servers/all-in-one/go.mod index 5a30b4e5c..720f4d930 100644 --- a/plugins/wasm-go/mcp-servers/all-in-one/go.mod +++ b/plugins/wasm-go/mcp-servers/all-in-one/go.mod @@ -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 ) diff --git a/plugins/wasm-go/mcp-servers/all-in-one/go.sum b/plugins/wasm-go/mcp-servers/all-in-one/go.sum index e33cb0811..8601d2d2b 100644 --- a/plugins/wasm-go/mcp-servers/all-in-one/go.sum +++ b/plugins/wasm-go/mcp-servers/all-in-one/go.sum @@ -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=