mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 12:47:28 +08:00
refactor: migrate MCP SDK to main repo (#3516)
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
module jsonrpc-converter
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.1
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go/pkg/mcp => ../../pkg/mcp
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.4
|
||||
github.com/alibaba/higress/plugins/wasm-go/pkg/mcp v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
@@ -15,6 +19,7 @@ require (
|
||||
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/davecgh/go-spew 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
|
||||
@@ -22,8 +27,10 @@ require (
|
||||
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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // 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
|
||||
|
||||
@@ -20,10 +20,10 @@ 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-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.4 h1:/GqbzCw4oWqJc8UbKEfF94E3/+4CPZGbzxpKo2L3Ldk=
|
||||
github.com/higress-group/wasm-go v1.0.4/go.mod h1:B8C6+OlpnyYyZUBEdUXA7tYZYD+uwZTNjfkE5FywA+A=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2 h1:NY33OrWCJJ+DFiLc+lsBY4Ywor2Ik61ssk6qkGF8Ypo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9 h1:sUuUXZwr50l3W1St7MESlFmxmUAu+QUNNfJXx4P6bas=
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
|
||||
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=
|
||||
@@ -49,6 +49,8 @@ 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/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
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=
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/higress-group/wasm-go/pkg/mcp"
|
||||
"github.com/higress-group/wasm-go/pkg/mcp/utils"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/utils"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTruncateString tests the truncateString function
|
||||
func TestTruncateString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -14,6 +20,8 @@ func TestTruncateString(t *testing.T) {
|
||||
{"Short String", "Higress Is an AI-Native API Gateway", 1000, "Higress Is an AI-Native API Gateway"},
|
||||
{"Exact Length", "Higress Is an AI-Native API Gateway", 35, "Higress Is an AI-Native API Gateway"},
|
||||
{"Truncated String", "Higress Is an AI-Native API Gateway", 20, "Higress Is...(truncated)...PI Gateway"},
|
||||
{"Empty String", "", 10, ""},
|
||||
{"Single Char", "A", 10, "A"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -26,3 +34,248 @@ func TestTruncateString(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsPreRequestStage tests the isPreRequestStage function
|
||||
func TestIsPreRequestStage(t *testing.T) {
|
||||
config := McpConverterConfig{Stage: ProcessRequest}
|
||||
require.True(t, isPreRequestStage(config))
|
||||
|
||||
config = McpConverterConfig{Stage: ProcessResponse}
|
||||
require.False(t, isPreRequestStage(config))
|
||||
}
|
||||
|
||||
// TestIsPreResponseStage tests the isPreResponseStage function
|
||||
func TestIsPreResponseStage(t *testing.T) {
|
||||
config := McpConverterConfig{Stage: ProcessResponse}
|
||||
require.True(t, isPreResponseStage(config))
|
||||
|
||||
config = McpConverterConfig{Stage: ProcessRequest}
|
||||
require.False(t, isPreResponseStage(config))
|
||||
}
|
||||
|
||||
// TestIsMethodAllowed tests the isMethodAllowed function
|
||||
func TestIsMethodAllowed(t *testing.T) {
|
||||
config := McpConverterConfig{AllowedMethods: []string{MethodToolList, MethodToolCall}}
|
||||
|
||||
require.True(t, isMethodAllowed(config, MethodToolList))
|
||||
require.True(t, isMethodAllowed(config, MethodToolCall))
|
||||
require.False(t, isMethodAllowed(config, "invalid/method"))
|
||||
}
|
||||
|
||||
// TestConstants tests the constant values
|
||||
func TestConstants(t *testing.T) {
|
||||
require.Equal(t, "x-envoy-jsonrpc-id", JsonRpcId)
|
||||
require.Equal(t, "x-envoy-jsonrpc-method", JsonRpcMethod)
|
||||
require.Equal(t, "x-envoy-jsonrpc-params", JsonRpcParams)
|
||||
require.Equal(t, "x-envoy-jsonrpc-result", JsonRpcResult)
|
||||
require.Equal(t, "x-envoy-jsonrpc-error", JsonRpcError)
|
||||
require.Equal(t, "x-envoy-mcp-tool-name", McpToolName)
|
||||
require.Equal(t, "x-envoy-mcp-tool-arguments", McpToolArguments)
|
||||
require.Equal(t, "x-envoy-mcp-tool-response", McpToolResponse)
|
||||
require.Equal(t, "x-envoy-mcp-tool-error", McpToolError)
|
||||
require.Equal(t, 4000, DefaultMaxHeaderLength)
|
||||
require.Equal(t, "tools/list", MethodToolList)
|
||||
require.Equal(t, "tools/call", MethodToolCall)
|
||||
require.Equal(t, ProcessStage("request"), ProcessRequest)
|
||||
require.Equal(t, ProcessStage("response"), ProcessResponse)
|
||||
}
|
||||
|
||||
// TestMcpConverterConfigDefaults tests config default values
|
||||
func TestMcpConverterConfigDefaults(t *testing.T) {
|
||||
config := McpConverterConfig{}
|
||||
require.Equal(t, 0, config.MaxHeaderLength)
|
||||
require.Equal(t, ProcessStage(""), config.Stage)
|
||||
require.Nil(t, config.AllowedMethods)
|
||||
}
|
||||
|
||||
// TestProcessStage tests ProcessStage type
|
||||
func TestProcessStage(t *testing.T) {
|
||||
require.Equal(t, ProcessStage("request"), ProcessRequest)
|
||||
require.Equal(t, ProcessStage("response"), ProcessResponse)
|
||||
}
|
||||
|
||||
// TestRemoveJsonRpcHeadersFunction tests removeJsonRpcHeaders function logic
|
||||
func TestRemoveJsonRpcHeadersFunction(t *testing.T) {
|
||||
headersToRemove := []string{
|
||||
JsonRpcId,
|
||||
JsonRpcMethod,
|
||||
JsonRpcParams,
|
||||
JsonRpcResult,
|
||||
McpToolName,
|
||||
McpToolArguments,
|
||||
McpToolResponse,
|
||||
McpToolError,
|
||||
}
|
||||
require.Len(t, headersToRemove, 8)
|
||||
}
|
||||
|
||||
// TestTruncateStringLong tests truncation of very long strings
|
||||
func TestTruncateStringLong(t *testing.T) {
|
||||
longString := ""
|
||||
for i := 0; i < 5000; i++ {
|
||||
longString += "a"
|
||||
}
|
||||
config := McpConverterConfig{MaxHeaderLength: 1000}
|
||||
result := truncateString(longString, config)
|
||||
require.Contains(t, result, "...(truncated)...")
|
||||
require.LessOrEqual(t, len(result), 1020)
|
||||
}
|
||||
|
||||
// TestTruncateStringWithSmallMaxLength tests truncation with small max length
|
||||
func TestTruncateStringWithSmallMaxLength(t *testing.T) {
|
||||
config := McpConverterConfig{MaxHeaderLength: 10}
|
||||
result := truncateString("This is a very long string", config)
|
||||
require.Contains(t, result, "...(truncated)...")
|
||||
}
|
||||
|
||||
// TestPluginInit tests plugin initialization
|
||||
func TestPluginInit(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(McpConverterConfig{
|
||||
Stage: ProcessRequest,
|
||||
MaxHeaderLength: DefaultMaxHeaderLength,
|
||||
AllowedMethods: []string{MethodToolList, MethodToolCall},
|
||||
})
|
||||
|
||||
host, status := test.NewTestHost(configBytes)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
}
|
||||
|
||||
// TestProcessJsonRpcRequest tests processJsonRpcRequest function
|
||||
func TestProcessJsonRpcRequest(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(McpConverterConfig{
|
||||
Stage: ProcessRequest,
|
||||
MaxHeaderLength: DefaultMaxHeaderLength,
|
||||
AllowedMethods: []string{MethodToolList, MethodToolCall},
|
||||
})
|
||||
|
||||
host, status := test.NewTestHost(configBytes)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.InitHttp()
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "mcp-server.example.com"},
|
||||
{":method", "POST"},
|
||||
{":path", "/mcp"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
toolsListRequest := `{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(toolsListRequest))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
}
|
||||
|
||||
// TestProcessToolCallRequest tests processToolCallRequest function
|
||||
func TestProcessToolCallRequest(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(McpConverterConfig{
|
||||
Stage: ProcessRequest,
|
||||
MaxHeaderLength: DefaultMaxHeaderLength,
|
||||
AllowedMethods: []string{MethodToolCall},
|
||||
})
|
||||
|
||||
host, status := test.NewTestHost(configBytes)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.InitHttp()
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "mcp-server.example.com"},
|
||||
{":method", "POST"},
|
||||
{":path", "/mcp"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
toolCallRequest := `{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "test_tool",
|
||||
"arguments": {"arg1": "value1"}
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(toolCallRequest))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
}
|
||||
|
||||
// TestProcessJsonRpcResponse tests processJsonRpcResponse function
|
||||
func TestProcessJsonRpcResponse(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(McpConverterConfig{
|
||||
Stage: ProcessResponse,
|
||||
MaxHeaderLength: DefaultMaxHeaderLength,
|
||||
AllowedMethods: []string{MethodToolList, MethodToolCall},
|
||||
})
|
||||
|
||||
host, status := test.NewTestHost(configBytes)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.InitHttp()
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "mcp-server.example.com"},
|
||||
{":method", "POST"},
|
||||
{":path", "/mcp"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
responseBody := `{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"tools": [{"name": "test_tool"}]
|
||||
}
|
||||
}`
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
host.CompleteHttp()
|
||||
}
|
||||
|
||||
// TestProcessToolListResponse tests processToolListResponse function
|
||||
func TestProcessToolListResponse(t *testing.T) {
|
||||
configBytes, _ := json.Marshal(McpConverterConfig{
|
||||
Stage: ProcessResponse,
|
||||
MaxHeaderLength: DefaultMaxHeaderLength,
|
||||
AllowedMethods: []string{MethodToolList},
|
||||
})
|
||||
|
||||
host, status := test.NewTestHost(configBytes)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.InitHttp()
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "mcp-server.example.com"},
|
||||
{":method", "POST"},
|
||||
{":path", "/mcp"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
responseBody := `{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"tools": [{"name": "test_tool"}]
|
||||
}
|
||||
}`
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
host.CompleteHttp()
|
||||
}
|
||||
|
||||
89
plugins/wasm-go/extensions/mcp-router/README.md
Normal file
89
plugins/wasm-go/extensions/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/extensions/mcp-router/README_ZH.md
Normal file
89
plugins/wasm-go/extensions/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`。
|
||||
38
plugins/wasm-go/extensions/mcp-router/go.mod
Normal file
38
plugins/wasm-go/extensions/mcp-router/go.mod
Normal file
@@ -0,0 +1,38 @@
|
||||
module mcp-router
|
||||
|
||||
go 1.24.1
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go/pkg/mcp => ../../pkg/mcp
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go/pkg/mcp v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9
|
||||
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
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
73
plugins/wasm-go/extensions/mcp-router/go.sum
Normal file
73
plugins/wasm-go/extensions/mcp-router/go.sum
Normal file
@@ -0,0 +1,73 @@
|
||||
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/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-20251103120604-77e9cce339d2 h1:NY33OrWCJJ+DFiLc+lsBY4Ywor2Ik61ssk6qkGF8Ypo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9 h1:sUuUXZwr50l3W1St7MESlFmxmUAu+QUNNfJXx4P6bas=
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
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=
|
||||
162
plugins/wasm-go/extensions/mcp-router/main.go
Normal file
162
plugins/wasm-go/extensions/mcp-router/main.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp/consts"
|
||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
mcp.LoadMCPFilter(
|
||||
mcp.FilterName("mcp-router"),
|
||||
mcp.SetConfigOverrideParser(ParseGlobalConfig, ParseOverrideConfig),
|
||||
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"`
|
||||
}
|
||||
|
||||
// McpRouterGlobalConfig represents the global configuration for the mcp-router filter
|
||||
type McpRouterGlobalConfig struct {
|
||||
Servers []ServerConfig `json:"servers"`
|
||||
}
|
||||
|
||||
type McpRouterConfig struct {
|
||||
global *McpRouterGlobalConfig
|
||||
enable bool
|
||||
}
|
||||
|
||||
func ParseGlobalConfig(configBytes []byte, globalConfig *any) error {
|
||||
var config McpRouterGlobalConfig
|
||||
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)
|
||||
}
|
||||
|
||||
*globalConfig = config
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseOverrideConfig(configBytes []byte, globalConfig any, ruleConfig *any) error {
|
||||
var config McpRouterConfig
|
||||
if globalConfig == nil {
|
||||
config.global = &McpRouterGlobalConfig{}
|
||||
config.enable = false
|
||||
*ruleConfig = config
|
||||
log.Error("globalConfig not found, mcp router will not work")
|
||||
return nil
|
||||
}
|
||||
parent, ok := globalConfig.(McpRouterGlobalConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid globalConfig: %v", globalConfig)
|
||||
}
|
||||
config.global = &parent
|
||||
config.enable = gjson.GetBytes(configBytes, "enable").Bool()
|
||||
*ruleConfig = 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
|
||||
}
|
||||
if !routerConfig.enable {
|
||||
return types.ActionContinue
|
||||
}
|
||||
// Extract server name from tool name (format: "${serverName}___${toolName}")
|
||||
parts := strings.SplitN(toolName, consts.ToolSetNameSplitter, 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.global.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
|
||||
}
|
||||
}
|
||||
|
||||
proxywasm.ReplaceHttpRequestHeader("x-envoy-internal-route", "true")
|
||||
|
||||
// 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
|
||||
}
|
||||
48
plugins/wasm-go/extensions/mcp-server/go.mod
Normal file
48
plugins/wasm-go/extensions/mcp-server/go.mod
Normal file
@@ -0,0 +1,48 @@
|
||||
module mcp-server
|
||||
|
||||
go 1.24.1
|
||||
|
||||
replace (
|
||||
amap-tools => ../../mcp-servers/amap-tools
|
||||
github.com/alibaba/higress/plugins/wasm-go/pkg/mcp => ../../pkg/mcp
|
||||
quark-search => ../../mcp-servers/quark-search
|
||||
)
|
||||
|
||||
require (
|
||||
amap-tools v0.0.0-00010101000000-000000000000
|
||||
github.com/alibaba/higress/plugins/wasm-go/pkg/mcp v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9
|
||||
github.com/stretchr/testify v1.9.0
|
||||
quark-search v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
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/davecgh/go-spew 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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/gjson v1.18.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/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
75
plugins/wasm-go/extensions/mcp-server/go.sum
Normal file
75
plugins/wasm-go/extensions/mcp-server/go.sum
Normal file
@@ -0,0 +1,75 @@
|
||||
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/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-20251103120604-77e9cce339d2 h1:NY33OrWCJJ+DFiLc+lsBY4Ywor2Ik61ssk6qkGF8Ypo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20251103120604-77e9cce339d2/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9 h1:sUuUXZwr50l3W1St7MESlFmxmUAu+QUNNfJXx4P6bas=
|
||||
github.com/higress-group/wasm-go v1.0.10-0.20260115123534-84ef43c39dc9/go.mod h1:uKVYICbRaxTlKqdm8E0dpjbysxM8uCPb9LV26hF3Km8=
|
||||
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/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
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=
|
||||
32
plugins/wasm-go/extensions/mcp-server/main.go
Normal file
32
plugins/wasm-go/extensions/mcp-server/main.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 (
|
||||
amap "amap-tools/tools"
|
||||
quark "quark-search/tools"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/mcp"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
mcp.LoadMCPServer(mcp.AddMCPServer("quark-search",
|
||||
quark.LoadTools(mcp.NewMCPServer())))
|
||||
mcp.LoadMCPServer(mcp.AddMCPServer("amap-tools",
|
||||
amap.LoadTools(mcp.NewMCPServer())))
|
||||
mcp.InitMCPServer()
|
||||
}
|
||||
2766
plugins/wasm-go/extensions/mcp-server/main_test.go
Normal file
2766
plugins/wasm-go/extensions/mcp-server/main_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user