From 1602b6f94a27014149b2982bba11bcaef3c0522f Mon Sep 17 00:00:00 2001 From: Tsukilc <3168078770@qq.com> Date: Fri, 31 Oct 2025 15:46:14 +0800 Subject: [PATCH] feat: add higress api mcp server (#2923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 澄潭 Co-authored-by: Se7en --- plugins/golang-filter/mcp-server/config.go | 1 + .../mcp-server/servers/higress/client.go | 49 +- .../servers/higress/higress-api/README.md | 80 ++- .../servers/higress/higress-api/README_en.md | 80 ++- .../servers/higress/higress-api/server.go | 24 +- .../higress/higress-api/tools/ai_provider.go | 366 +++++++++++ .../higress/higress-api/tools/ai_route.go | 601 +++++++++++++++++ .../higress/higress-api/tools/mcp_server.go | 610 ++++++++++++++++++ .../higress-api/tools/plugins/common.go | 4 +- .../tools/plugins/custom-response.go | 178 +++++ .../tools/plugins/request-block.go | 4 +- .../higress/higress-api/tools/route.go | 53 +- .../higress/higress-api/tools/service.go | 106 ++- .../servers/higress/higress-ops/.keep | 0 .../servers/higress/higress-ops/README.md | 197 ++++++ .../servers/higress/higress-ops/README_en.md | 197 ++++++ .../servers/higress/higress-ops/client.go | 147 +++++ .../servers/higress/higress-ops/server.go | 81 +++ .../higress/higress-ops/tools/client.go | 29 + .../higress/higress-ops/tools/envoy.go | 266 ++++++++ .../higress/higress-ops/tools/istiod.go | 131 ++++ .../higress/higress-ops/tools/utils.go | 103 +++ .../golang-filter/mcp-session/common/auth.go | 53 ++ .../golang-filter/mcp-session/common/sse.go | 12 + 24 files changed, 3298 insertions(+), 74 deletions(-) create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_provider.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_route.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/mcp_server.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/custom-response.go delete mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/.keep create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/README.md create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/README_en.md create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/client.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/server.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/client.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/envoy.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/istiod.go create mode 100644 plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/utils.go create mode 100644 plugins/golang-filter/mcp-session/common/auth.go diff --git a/plugins/golang-filter/mcp-server/config.go b/plugins/golang-filter/mcp-server/config.go index d18c11025..ae955aed3 100644 --- a/plugins/golang-filter/mcp-server/config.go +++ b/plugins/golang-filter/mcp-server/config.go @@ -6,6 +6,7 @@ import ( _ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos" _ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm" _ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api" + _ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-ops" _ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/nginx-migration" _ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/rag" mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session" diff --git a/plugins/golang-filter/mcp-server/servers/higress/client.go b/plugins/golang-filter/mcp-server/servers/higress/client.go index e425bed57..d4018ede0 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/client.go +++ b/plugins/golang-filter/mcp-server/servers/higress/client.go @@ -9,6 +9,7 @@ import ( "net/http" "time" + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" "github.com/envoyproxy/envoy/contrib/golang/common/go/api" ) @@ -20,11 +21,9 @@ type HigressClient struct { httpClient *http.Client } -func NewHigressClient(baseURL, username, password string) *HigressClient { +func NewHigressClient(baseURL string) *HigressClient { client := &HigressClient{ - baseURL: baseURL, - username: username, - password: password, + baseURL: baseURL, httpClient: &http.Client{ Timeout: 30 * time.Second, }, @@ -35,22 +34,28 @@ func NewHigressClient(baseURL, username, password string) *HigressClient { return client } -func (c *HigressClient) Get(path string) ([]byte, error) { - return c.request("GET", path, nil) +func (c *HigressClient) Get(ctx context.Context, path string) ([]byte, error) { + return c.request(ctx, "GET", path, nil) } -func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) { - return c.request("POST", path, data) +func (c *HigressClient) Post(ctx context.Context, path string, data interface{}) ([]byte, error) { + return c.request(ctx, "POST", path, data) } -func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) { - return c.request("PUT", path, data) +func (c *HigressClient) Put(ctx context.Context, path string, data interface{}) ([]byte, error) { + return c.request(ctx, "PUT", path, data) } -func (c *HigressClient) Delete(path string) ([]byte, error) { - return c.request("DELETE", path, nil) +func (c *HigressClient) Delete(ctx context.Context, path string) ([]byte, error) { + return c.request(ctx, "DELETE", path, nil) } -func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) { + +// DeleteWithBody performs a DELETE request with a request body +func (c *HigressClient) DeleteWithBody(ctx context.Context, path string, data interface{}) ([]byte, error) { + return c.request(ctx, "DELETE", path, data) +} + +func (c *HigressClient) request(ctx context.Context, method, path string, data interface{}) ([]byte, error) { url := c.baseURL + path var body io.Reader @@ -65,15 +70,27 @@ func (c *HigressClient) request(method, path string, data interface{}) ([]byte, api.LogDebugf("Higress API %s %s", method, url) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // Create context with timeout if not already set + if ctx == nil { + ctx = context.Background() + } + reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - req, err := http.NewRequestWithContext(ctx, method, url, body) + req, err := http.NewRequestWithContext(reqCtx, method, url, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } - req.SetBasicAuth(c.username, c.password) + // Try to get Authorization header from context first (passthrough from MCP client) + if authHeader, ok := common.GetAuthHeader(ctx); ok && authHeader != "" { + req.Header.Set("Authorization", authHeader) + api.LogDebugf("Higress API request: Using Authorization header from context for %s %s", method, path) + } else { + api.LogWarnf("Higress API request: No authentication credentials available for %s %s", method, path) + return nil, fmt.Errorf("no authentication credentials available for %s %s", method, path) + } + req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/README.md b/plugins/golang-filter/mcp-server/servers/higress/higress-api/README.md index 9c1960ad5..e08e5065d 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/README.md +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/README.md @@ -1,6 +1,6 @@ # Higress API MCP Server -Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源和插件等资源。 +Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源、AI路由、AI提供商、MCP服务器和插件等资源。 ## 功能特性 @@ -9,12 +9,38 @@ Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来 - `get-route`: 获取路由 - `add-route`: 添加路由 - `update-route`: 更新路由 +- `delete-route`: 删除路由 + +### AI路由管理 +- `list-ai-routes`: 列出AI路由 +- `get-ai-route`: 获取AI路由 +- `add-ai-route`: 添加AI路由 +- `update-ai-route`: 更新AI路由 +- `delete-ai-route`: 删除AI路由 ### 服务来源管理 - `list-service-sources`: 列出服务来源 - `get-service-source`: 获取服务来源 - `add-service-source`: 添加服务来源 - `update-service-source`: 更新服务来源 +- `delete-service-source`: 删除服务来源 + +### AI提供商管理 +- `list-ai-providers`: 列出LLM提供商 +- `get-ai-provider`: 获取LLM提供商 +- `add-ai-provider`: 添加LLM提供商 +- `update-ai-provider`: 更新LLM提供商 +- `delete-ai-provider`: 删除LLM提供商 + +### MCP服务器管理 +- `list-mcp-servers`: 列出MCP服务器 +- `get-mcp-server`: 获取MCP服务器详情 +- `add-or-update-mcp-server`: 添加或更新MCP服务器 +- `delete-mcp-server`: 删除MCP服务器 +- `list-mcp-server-consumers`: 列出MCP服务器允许的消费者 +- `add-mcp-server-consumers`: 添加MCP服务器允许的消费者 +- `delete-mcp-server-consumers`: 删除MCP服务器允许的消费者 +- `swagger-to-mcp-config`: 将Swagger内容转换为MCP配置 ### 插件管理 - `list-plugin-instances`: 列出特定作用域下的所有插件实例(支持全局、域名、服务、路由级别) @@ -27,8 +53,6 @@ Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来 | 参数 | 类型 | 必需 | 说明 | |------|------|------|------| | `higressURL` | string | 必填 | Higress Console 的 URL 地址 | -| `username` | string | 必填 | Higress Console 登录用户名 | -| `password` | string | 必填 | Higress Console 登录密码 | | `description` | string | 可选 | 服务器描述信息 | 配置示例: @@ -73,6 +97,52 @@ data: type: higress-api # 类型和 RegisterServer 一致 config: higressURL: http://higress-console.higress-system.svc.cluster.local:8080 - username: admin - password: admin ``` + +## 鉴权配置 + +Higress API MCP Server 使用 HTTP Basic Authentication 进行鉴权。客户端需要在请求头中携带 `Authorization` 头。 + +### 配置示例 + +```json +{ + "mcpServers": { + "higress_api_mcp": { + "url": "http://127.0.0.1:80/higress-api/sse", + "headers": { + "Authorization": "Basic YWRtaW46YWRtaW4=" + } + } + } +} +``` + +**说明:** +- `Authorization` 头使用 Basic Authentication 格式:`Basic base64(username:password)` +- 示例中的 `YWRtaW46YWRtaW4=` 是 `admin:admin` 的 Base64 编码 +- 您需要根据实际的 Higress Console 用户名和密码生成相应的 Base64 编码 + +### 生成 Authorization 头 + +使用以下命令生成 Basic Auth 的 Authorization 头: + +```bash +echo -n "username:password" | base64 +``` + +将 `username` 和 `password` 替换为您的 Higress Console 实际凭证。 + +## 演示 + +1. create openapi-mcp-server + +https://private-user-images.githubusercontent.com/153273766/507768507-42077ff3-731e-42fe-8b10-ccae0d1b3378.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY4NTA3LTQyMDc3ZmYzLTczMWUtNDJmZS04YjEwLWNjYWUwZDFiMzM3OC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xODVlY2QzYTBmODY0YzRlMzFjNWI1NGE3MGIyZDAxMGRmZjczNTNhMDZmNjdhMGYxMjM2NzVjMjEyYzdlNWFkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.qzpx2W52Zl9WuWidgEMTYP1sMfrqcgsXtNbNvYK39wE + +2. create ai-route + +https://private-user-images.githubusercontent.com/153273766/507769175-96b6002f-389d-46e8-b696-c5bcf518a1c6.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTc1LTk2YjYwMDJmLTM4OWQtNDZlOC1iNjk2LWM1YmNmNTE4YTFjNi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mYTFiZjY0Zjg0NWVhYzA3NzhiODc2NzUwMDg3MDZiYjI4ZTQ4YWRkNmIwMzEyMWI5ZjE0MTQ3NTZlZmU5NTEwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.XW6eJxjCpcblQCCtidYoNCwn2yUkXt3d9zuDYxDIF8Q + +3. create http-bin + custom response + +https://private-user-images.githubusercontent.com/153273766/507769227-73b624d5-70b8-4c94-aa87-42b3ff8b094d.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MjI3LTczYjYyNGQ1LTcwYjgtNGM5NC1hYTg3LTQyYjNmZjhiMDk0ZC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1jMjc1N2MyZTE2N2RlYjJkZThhZWMwZTc5YWM1ODI3ODgyYjM1Yzk3Mzk1ZjVlMDljZGM4NGJhM2MwZTE5N2E5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.R4h7AmTKadKxd6qr7m-i8JPsxoJHcrN49eVbB0ixYyU \ No newline at end of file diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/README_en.md b/plugins/golang-filter/mcp-server/servers/higress/higress-api/README_en.md index 340dae1bc..302cd14ad 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/README_en.md +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/README_en.md @@ -1,6 +1,6 @@ # Higress API MCP Server -Higress API MCP Server provides MCP tools to manage Higress routes, service sources, plugins and other resources. +Higress API MCP Server provides MCP tools to manage Higress routes, service sources, AI routes, AI providers, MCP servers, plugins and other resources. ## Features @@ -9,12 +9,38 @@ Higress API MCP Server provides MCP tools to manage Higress routes, service sour - `get-route`: Get route - `add-route`: Add route - `update-route`: Update route +- `delete-route`: Delete route + +### AI Route Management +- `list-ai-routes`: List AI routes +- `get-ai-route`: Get AI route +- `add-ai-route`: Add AI route +- `update-ai-route`: Update AI route +- `delete-ai-route`: Delete AI route ### Service Source Management - `list-service-sources`: List service sources - `get-service-source`: Get service source - `add-service-source`: Add service source - `update-service-source`: Update service source +- `delete-service-source`: Delete service source + +### AI Provider Management +- `list-ai-providers`: List LLM providers +- `get-ai-provider`: Get LLM provider +- `add-ai-provider`: Add LLM provider +- `update-ai-provider`: Update LLM provider +- `delete-ai-provider`: Delete LLM provider + +### MCP Server Management +- `list-mcp-servers`: List MCP servers +- `get-mcp-server`: Get MCP server details +- `add-or-update-mcp-server`: Add or update MCP server +- `delete-mcp-server`: Delete MCP server +- `list-mcp-server-consumers`: List MCP server allowed consumers +- `add-mcp-server-consumers`: Add MCP server allowed consumers +- `delete-mcp-server-consumers`: Delete MCP server allowed consumers +- `swagger-to-mcp-config`: Convert Swagger content to MCP configuration ### Plugin Management - `list-plugin-instances`: List all plugin instances for a specific scope (supports global, domain, service, and route levels) @@ -27,8 +53,6 @@ Higress API MCP Server provides MCP tools to manage Higress routes, service sour | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `higressURL` | string | Required | Higress Console URL address | -| `username` | string | Required | Higress Console login username | -| `password` | string | Required | Higress Console login password | | `description` | string | Optional | MCP Server description | Configuration Example: @@ -73,6 +97,52 @@ data: type: higress-api # Type defined in RegisterServer function config: higressURL: http://higress-console.higress-system.svc.cluster.local:8080 - username: admin - password: admin ``` + +## Authentication Configuration + +Higress API MCP Server uses HTTP Basic Authentication for authorization. Clients need to include an `Authorization` header in their requests. + +### Configuration Example + +```json +{ + "mcpServers": { + "higress_api_mcp": { + "url": "http://127.0.0.1:80/higress-api/sse", + "headers": { + "Authorization": "Basic YWRtaW46YWRtaW4=" + } + } + } +} +``` + +**Notes:** +- The `Authorization` header uses Basic Authentication format: `Basic base64(username:password)` +- The example `YWRtaW46YWRtaW4=` is the Base64 encoding of `admin:admin` +- You need to generate the appropriate Base64 encoding based on your actual Higress Console username and password + +### Generating Authorization Header + +Use the following command to generate the Basic Auth Authorization header: + +```bash +echo -n "username:password" | base64 +``` + +Replace `username` and `password` with your actual Higress Console credentials. + +## Demo + +1. create openapi-mcp-server + +https://private-user-images.githubusercontent.com/153273766/507768507-42077ff3-731e-42fe-8b10-ccae0d1b3378.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY4NTA3LTQyMDc3ZmYzLTczMWUtNDJmZS04YjEwLWNjYWUwZDFiMzM3OC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0xODVlY2QzYTBmODY0YzRlMzFjNWI1NGE3MGIyZDAxMGRmZjczNTNhMDZmNjdhMGYxMjM2NzVjMjEyYzdlNWFkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.qzpx2W52Zl9WuWidgEMTYP1sMfrqcgsXtNbNvYK39wE + +2. create ai-route + +https://private-user-images.githubusercontent.com/153273766/507769175-96b6002f-389d-46e8-b696-c5bcf518a1c6.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTc1LTk2YjYwMDJmLTM4OWQtNDZlOC1iNjk2LWM1YmNmNTE4YTFjNi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mYTFiZjY0Zjg0NWVhYzA3NzhiODc2NzUwMDg3MDZiYjI4ZTQ4YWRkNmIwMzEyMWI5ZjE0MTQ3NTZlZmU5NTEwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.XW6eJxjCpcblQCCtidYoNCwn2yUkXt3d9zuDYxDIF8Q + +3. create http-bin + custom response + +https://private-user-images.githubusercontent.com/153273766/507769227-73b624d5-70b8-4c94-aa87-42b3ff8b094d.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MjI3LTczYjYyNGQ1LTcwYjgtNGM5NC1hYTg3LTQyYjNmZjhiMDk0ZC5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1jMjc1N2MyZTE2N2RlYjJkZThhZWMwZTc5YWM1ODI3ODgyYjM1Yzk3Mzk1ZjVlMDljZGM4NGJhM2MwZTE5N2E5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.R4h7AmTKadKxd6qr7m-i8JPsxoJHcrN49eVbB0ixYyU diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/server.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/server.go index 88930f506..dbf890d65 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/server.go +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/server.go @@ -18,8 +18,6 @@ func init() { type HigressConfig struct { higressURL string - username string - password string description string } @@ -30,26 +28,14 @@ func (c *HigressConfig) ParseConfig(config map[string]interface{}) error { } c.higressURL = higressURL - username, ok := config["username"].(string) - if !ok { - return errors.New("missing username") - } - c.username = username - - password, ok := config["password"].(string) - if !ok { - return errors.New("missing password") - } - c.password = password - if desc, ok := config["description"].(string); ok { c.description = desc } else { c.description = "Higress API MCP Server, which invokes Higress Console APIs to manage resources such as routes, services, and plugins." } - api.LogDebugf("HigressConfig ParseConfig: higressURL=%s, username=%s, description=%s", - c.higressURL, c.username, c.description) + api.LogInfof("Higress MCP Server configuration parsed successfully. URL: %s", + c.higressURL) return nil } @@ -62,13 +48,17 @@ func (c *HigressConfig) NewServer(serverName string) (*common.MCPServer, error) ) // Initialize Higress API client - client := higress.NewHigressClient(c.higressURL, c.username, c.password) + client := higress.NewHigressClient(c.higressURL) // Register all tools tools.RegisterRouteTools(mcpServer, client) tools.RegisterServiceTools(mcpServer, client) + tools.RegisterAiRouteTools(mcpServer, client) + tools.RegisterAiProviderTools(mcpServer, client) + tools.RegisterMcpServerTools(mcpServer, client) plugins.RegisterCommonPluginTools(mcpServer, client) plugins.RegisterRequestBlockPluginTools(mcpServer, client) + plugins.RegisterCustomResponsePluginTools(mcpServer, client) api.LogInfof("Higress MCP Server initialized: %s", serverName) diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_provider.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_provider.go new file mode 100644 index 000000000..609f12a46 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_provider.go @@ -0,0 +1,366 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress" + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/mark3labs/mcp-go/mcp" +) + +// LlmProvider represents an LLM provider configuration +type LlmProvider struct { + Name string `json:"name"` + Type string `json:"type"` + Protocol string `json:"protocol"` + Tokens []string `json:"tokens,omitempty"` + TokenFailoverConfig *TokenFailoverConfig `json:"tokenFailoverConfig,omitempty"` + RawConfigs map[string]interface{} `json:"rawConfigs,omitempty"` +} + +// TokenFailoverConfig represents token failover configuration +type TokenFailoverConfig struct { + Enabled bool `json:"enabled,omitempty"` + FailureThreshold int `json:"failureThreshold,omitempty"` + SuccessThreshold int `json:"successThreshold,omitempty"` + HealthCheckInterval int `json:"healthCheckInterval,omitempty"` + HealthCheckTimeout int `json:"healthCheckTimeout,omitempty"` + HealthCheckModel string `json:"healthCheckModel,omitempty"` +} + +// LlmProviderResponse represents the API response for LLM provider operations +type LlmProviderResponse = higress.APIResponse[LlmProvider] + +// RegisterAiProviderTools registers all AI provider management tools +func RegisterAiProviderTools(mcpServer *common.MCPServer, client *higress.HigressClient) { + // List all LLM providers + mcpServer.AddTool( + mcp.NewToolWithRawSchema("list-ai-providers", "List all available LLM providers", listAiProvidersSchema()), + handleListAiProviders(client), + ) + + // Get specific LLM provider + mcpServer.AddTool( + mcp.NewToolWithRawSchema("get-ai-provider", "Get detailed information about a specific LLM provider", getAiProviderSchema()), + handleGetAiProvider(client), + ) + + // Add new LLM provider + mcpServer.AddTool( + mcp.NewToolWithRawSchema("add-ai-provider", "Add a new LLM provider", getAddAiProviderSchema()), + handleAddAiProvider(client), + ) + + // Update existing LLM provider + mcpServer.AddTool( + mcp.NewToolWithRawSchema("update-ai-provider", "Update an existing LLM provider", getUpdateAiProviderSchema()), + handleUpdateAiProvider(client), + ) + + // Delete existing LLM provider + mcpServer.AddTool( + mcp.NewToolWithRawSchema("delete-ai-provider", "Delete an existing LLM provider", getAiProviderSchema()), + handleDeleteAiProvider(client), + ) +} + +func handleListAiProviders(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + respBody, err := client.Get(ctx, "/v1/ai/providers") + if err != nil { + return nil, fmt.Errorf("failed to list LLM providers: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleGetAiProvider(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + respBody, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to get LLM provider '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleAddAiProvider(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Validate required fields + if _, ok := configurations["name"]; !ok { + return nil, fmt.Errorf("missing required field 'name' in configurations") + } + if _, ok := configurations["type"]; !ok { + return nil, fmt.Errorf("missing required field 'type' in configurations") + } + if _, ok := configurations["protocol"]; !ok { + return nil, fmt.Errorf("missing required field 'protocol' in configurations") + } + + respBody, err := client.Post(ctx, "/v1/ai/providers", configurations) + if err != nil { + return nil, fmt.Errorf("failed to add LLM provider: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleUpdateAiProvider(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Get current LLM provider configuration to merge with updates + currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to get current LLM provider configuration: %w", err) + } + + var response LlmProviderResponse + if err := json.Unmarshal(currentBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse current LLM provider response: %w", err) + } + + currentConfig := response.Data + + // Update configurations using JSON marshal/unmarshal for type conversion + configBytes, err := json.Marshal(configurations) + if err != nil { + return nil, fmt.Errorf("failed to marshal configurations: %w", err) + } + + var newConfig LlmProvider + if err := json.Unmarshal(configBytes, &newConfig); err != nil { + return nil, fmt.Errorf("failed to parse LLM provider configurations: %w", err) + } + + // Merge configurations (overwrite with new values where provided) + if newConfig.Type != "" { + currentConfig.Type = newConfig.Type + } + if newConfig.Protocol != "" { + currentConfig.Protocol = newConfig.Protocol + } + if newConfig.Tokens != nil { + currentConfig.Tokens = newConfig.Tokens + } + if newConfig.TokenFailoverConfig != nil { + currentConfig.TokenFailoverConfig = newConfig.TokenFailoverConfig + } + if newConfig.RawConfigs != nil { + currentConfig.RawConfigs = newConfig.RawConfigs + } + + respBody, err := client.Put(ctx, fmt.Sprintf("/v1/ai/providers/%s", name), currentConfig) + if err != nil { + return nil, fmt.Errorf("failed to update LLM provider '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleDeleteAiProvider(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/ai/providers/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to delete LLM provider '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func listAiProvidersSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }`) +} + +func getAiProviderSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the LLM provider" + } + }, + "required": ["name"], + "additionalProperties": false + }`) +} + +func getAddAiProviderSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "configurations": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Provider name" + }, + "type": { + "type": "string", + "enum": ["qwen", "openai", "moonshot", "azure", "ai360", "github", "groq", "baichuan", "yi", "deepseek", "zhipuai", "ollama", "claude", "baidu", "hunyuan", "stepfun", "minimax", "cloudflare", "spark", "gemini", "deepl", "mistral", "cohere", "doubao", "coze", "together-ai"], + "description": "LLM Service Provider Type" + }, + "protocol": { + "type": "string", + "enum": ["openai/v1", "original"], + "description": "LLM Service Provider Protocol" + }, + "tokens": { + "type": "array", + "items": {"type": "string"}, + "description": "Tokens used to request the provider" + }, + "tokenFailoverConfig": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "description": "Whether token failover is enabled"}, + "failureThreshold": {"type": "integer", "description": "Failure threshold"}, + "successThreshold": {"type": "integer", "description": "Success threshold"}, + "healthCheckInterval": {"type": "integer", "description": "Health check interval"}, + "healthCheckTimeout": {"type": "integer", "description": "Health check timeout"}, + "healthCheckModel": {"type": "string", "description": "Health check model"} + }, + "description": "Token Failover Config" + }, + "rawConfigs": { + "type": "object", + "additionalProperties": true, + "description": "Raw configuration key-value pairs used by ai-proxy plugin" + } + }, + "required": ["name", "type", "protocol"], + "additionalProperties": false + } + }, + "required": ["configurations"], + "additionalProperties": false + }`) +} + +func getUpdateAiProviderSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the LLM provider" + }, + "configurations": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["qwen", "openai", "moonshot", "azure", "ai360", "github", "groq", "baichuan", "yi", "deepseek", "zhipuai", "ollama", "claude", "baidu", "hunyuan", "stepfun", "minimax", "cloudflare", "spark", "gemini", "deepl", "mistral", "cohere", "doubao", "coze", "together-ai"], + "description": "LLM Service Provider Type" + }, + "protocol": { + "type": "string", + "enum": ["openai/v1", "original"], + "description": "LLM Service Provider Protocol" + }, + "tokens": { + "type": "array", + "items": {"type": "string"}, + "description": "Tokens used to request the provider" + }, + "tokenFailoverConfig": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "description": "Whether token failover is enabled"}, + "failureThreshold": {"type": "integer", "description": "Failure threshold"}, + "successThreshold": {"type": "integer", "description": "Success threshold"}, + "healthCheckInterval": {"type": "integer", "description": "Health check interval"}, + "healthCheckTimeout": {"type": "integer", "description": "Health check timeout"}, + "healthCheckModel": {"type": "string", "description": "Health check model"} + }, + "description": "Token Failover Config" + }, + "rawConfigs": { + "type": "object", + "additionalProperties": true, + "description": "Raw configuration key-value pairs used by ai-proxy plugin" + } + }, + "additionalProperties": false + } + }, + "required": ["name", "configurations"], + "additionalProperties": false + }`) +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_route.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_route.go new file mode 100644 index 000000000..078cf3712 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/ai_route.go @@ -0,0 +1,601 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress" + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/mark3labs/mcp-go/mcp" +) + +// AiRoute represents an AI route configuration +type AiRoute struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` + Domains []string `json:"domains,omitempty"` + PathPredicate *AiRoutePredicate `json:"pathPredicate,omitempty"` + HeaderPredicates []AiKeyedRoutePredicate `json:"headerPredicates,omitempty"` + URLParamPredicates []AiKeyedRoutePredicate `json:"urlParamPredicates,omitempty"` + Upstreams []AiUpstream `json:"upstreams,omitempty"` + ModelPredicates []AiModelPredicate `json:"modelPredicates,omitempty"` + AuthConfig *RouteAuthConfig `json:"authConfig,omitempty"` + FallbackConfig *AiRouteFallbackConfig `json:"fallbackConfig,omitempty"` +} + +// AiRoutePredicate represents an AI route predicate +type AiRoutePredicate struct { + MatchType string `json:"matchType"` + MatchValue string `json:"matchValue"` + CaseSensitive bool `json:"caseSensitive,omitempty"` +} + +// AiKeyedRoutePredicate represents an AI route predicate with a key +type AiKeyedRoutePredicate struct { + Key string `json:"key"` + MatchType string `json:"matchType"` + MatchValue string `json:"matchValue"` + CaseSensitive bool `json:"caseSensitive,omitempty"` +} + +// AiUpstream represents an AI upstream configuration +type AiUpstream struct { + Provider string `json:"provider"` + Weight int `json:"weight"` + ModelMapping map[string]string `json:"modelMapping,omitempty"` +} + +// AiModelPredicate represents an AI model predicate +type AiModelPredicate struct { + MatchType string `json:"matchType"` + MatchValue string `json:"matchValue"` + CaseSensitive bool `json:"caseSensitive,omitempty"` +} + +// AiRouteFallbackConfig represents AI route fallback configuration +type AiRouteFallbackConfig struct { + Enabled bool `json:"enabled"` + Upstreams []AiUpstream `json:"upstreams,omitempty"` + FallbackStrategy string `json:"fallbackStrategy,omitempty"` + ResponseCodes []string `json:"responseCodes,omitempty"` +} + +// AiRouteResponse represents the API response for AI route operations +type AiRouteResponse = higress.APIResponse[AiRoute] + +// RegisterAiRouteTools registers all AI route management tools +func RegisterAiRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) { + // List all AI routes + mcpServer.AddTool( + mcp.NewToolWithRawSchema("list-ai-routes", "List all available AI routes", listAiRoutesSchema()), + handleListAiRoutes(client), + ) + + // Get specific AI route + mcpServer.AddTool( + mcp.NewToolWithRawSchema("get-ai-route", "Get detailed information about a specific AI route", getAiRouteSchema()), + handleGetAiRoute(client), + ) + + // Add new AI route + mcpServer.AddTool( + mcp.NewToolWithRawSchema("add-ai-route", "Add a new AI route", getAddAiRouteSchema()), + handleAddAiRoute(client), + ) + + // Update existing AI route + mcpServer.AddTool( + mcp.NewToolWithRawSchema("update-ai-route", "Update an existing AI route", getUpdateAiRouteSchema()), + handleUpdateAiRoute(client), + ) + + // Delete existing AI route + mcpServer.AddTool( + mcp.NewToolWithRawSchema("delete-ai-route", "Delete an existing AI route", getAiRouteSchema()), + handleDeleteAiRoute(client), + ) +} + +func handleListAiRoutes(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + respBody, err := client.Get(ctx, "/v1/ai/routes") + if err != nil { + return nil, fmt.Errorf("failed to list AI routes: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleGetAiRoute(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + respBody, err := client.Get(ctx, fmt.Sprintf("/v1/ai/routes/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to get AI route '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleAddAiRoute(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Validate required fields + if _, ok := configurations["name"]; !ok { + return nil, fmt.Errorf("missing required field 'name' in configurations") + } + if _, ok := configurations["upstreams"]; !ok { + return nil, fmt.Errorf("missing required field 'upstreams' in configurations") + } + + // Validate AI providers exist in upstreams + if upstreams, ok := configurations["upstreams"].([]interface{}); ok && len(upstreams) > 0 { + for _, upstream := range upstreams { + if upstreamMap, ok := upstream.(map[string]interface{}); ok { + if providerName, ok := upstreamMap["provider"].(string); ok { + // Check if AI provider exists + _, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", providerName)) + if err != nil { + return nil, fmt.Errorf("Please create the AI provider '%s' first and then create the AI route", providerName) + } + } + } + } + } + + // Validate AI providers exist in fallback upstreams + if fallbackConfig, ok := configurations["fallbackConfig"].(map[string]interface{}); ok { + if fallbackUpstreams, ok := fallbackConfig["upstreams"].([]interface{}); ok && len(fallbackUpstreams) > 0 { + for _, upstream := range fallbackUpstreams { + if upstreamMap, ok := upstream.(map[string]interface{}); ok { + if providerName, ok := upstreamMap["provider"].(string); ok { + // Check if AI provider exists + _, err := client.Get(ctx, fmt.Sprintf("/v1/ai/providers/%s", providerName)) + if err != nil { + return nil, fmt.Errorf("Please create the AI provider '%s' first and then create the AI route", providerName) + } + } + } + } + } + } + + respBody, err := client.Post(ctx, "/v1/ai/routes", configurations) + if err != nil { + return nil, fmt.Errorf("failed to add AI route: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleUpdateAiRoute(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Get current AI route configuration to merge with updates + currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/ai/routes/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to get current AI route configuration: %w", err) + } + + var response AiRouteResponse + if err := json.Unmarshal(currentBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse current AI route response: %w", err) + } + + currentConfig := response.Data + + // Update configurations using JSON marshal/unmarshal for type conversion + configBytes, err := json.Marshal(configurations) + if err != nil { + return nil, fmt.Errorf("failed to marshal configurations: %w", err) + } + + var newConfig AiRoute + if err := json.Unmarshal(configBytes, &newConfig); err != nil { + return nil, fmt.Errorf("failed to parse AI route configurations: %w", err) + } + + // Merge configurations (overwrite with new values where provided) + if newConfig.Domains != nil { + currentConfig.Domains = newConfig.Domains + } + if newConfig.PathPredicate != nil { + currentConfig.PathPredicate = newConfig.PathPredicate + } + if newConfig.HeaderPredicates != nil { + currentConfig.HeaderPredicates = newConfig.HeaderPredicates + } + if newConfig.URLParamPredicates != nil { + currentConfig.URLParamPredicates = newConfig.URLParamPredicates + } + if newConfig.Upstreams != nil { + currentConfig.Upstreams = newConfig.Upstreams + } + if newConfig.ModelPredicates != nil { + currentConfig.ModelPredicates = newConfig.ModelPredicates + } + if newConfig.AuthConfig != nil { + currentConfig.AuthConfig = newConfig.AuthConfig + } + if newConfig.FallbackConfig != nil { + currentConfig.FallbackConfig = newConfig.FallbackConfig + } + + respBody, err := client.Put(ctx, fmt.Sprintf("/v1/ai/routes/%s", name), currentConfig) + if err != nil { + return nil, fmt.Errorf("failed to update AI route '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleDeleteAiRoute(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/ai/routes/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to delete AI route '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func listAiRoutesSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }`) +} + +func getAiRouteSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the AI route" + } + }, + "required": ["name"], + "additionalProperties": false + }`) +} + +func getAddAiRouteSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "configurations": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "AI route name" + }, + "domains": { + "type": "array", + "items": {"type": "string"}, + "description": "Domains that the route applies to. If empty, the route applies to all domains." + }, + "pathPredicate": { + "type": "object", + "properties": { + "matchType": {"type": "string", "enum": ["PRE"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["matchType", "matchValue"], + "description": "Path predicate" + }, + "headerPredicates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": {"type": "string", "description": "Header key"}, + "matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["key", "matchType", "matchValue"] + }, + "description": "Header predicates" + }, + "urlParamPredicates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": {"type": "string", "description": "URL parameter key"}, + "matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["key", "matchType", "matchValue"] + }, + "description": "URL parameter predicates" + }, + "upstreams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": {"type": "string", "description": "LLM provider name"}, + "weight": {"type": "integer", "description": "Weight of the upstream,The sum of upstream weights must be 100"}, + "modelMapping": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Model mapping" + } + }, + "required": ["provider", "weight"] + }, + "description": "Route upstreams" + }, + "modelPredicates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "matchType": {"type": "string", "enum": ["EQUAL", "PRE"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["matchType", "matchValue"] + }, + "description": "Model predicates" + }, + "authConfig": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "description": "Whether auth is enabled"}, + "allowedConsumers": { + "type": "array", + "items": {"type": "string"}, + "description": "Allowed consumer names" + } + }, + "description": "Route auth configuration" + }, + "fallbackConfig": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "description": "Whether fallback is enabled"}, + "upstreams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": {"type": "string", "description": "LLM provider name"}, + "weight": {"type": "integer", "description": "Weight of the upstream"}, + "modelMapping": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Model mapping" + } + }, + "required": ["provider", "weight"] + }, + "description": "Fallback upstreams. Only one upstream is allowed when fallbackStrategy is SEQ." + }, + "fallbackStrategy": {"type": "string", "enum": ["RAND", "SEQ"], "description": "Fallback strategy"}, + "responseCodes": { + "type": "array", + "items": {"type": "string"}, + "description": "Response codes that need fallback" + } + }, + "description": "AI Route fallback configuration" + } + }, + "required": ["name", "upstreams"], + "additionalProperties": false + } + }, + "required": ["configurations"], + "additionalProperties": false + }`) +} + +func getUpdateAiRouteSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the AI route" + }, + "configurations": { + "type": "object", + "properties": { + "domains": { + "type": "array", + "items": {"type": "string"}, + "description": "Domains that the route applies to. If empty, the route applies to all domains." + }, + "pathPredicate": { + "type": "object", + "properties": { + "matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["matchType", "matchValue"], + "description": "Path predicate" + }, + "headerPredicates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": {"type": "string", "description": "Header key"}, + "matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["key", "matchType", "matchValue"] + }, + "description": "Header predicates" + }, + "urlParamPredicates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": {"type": "string", "description": "URL parameter key"}, + "matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["key", "matchType", "matchValue"] + }, + "description": "URL parameter predicates" + }, + "upstreams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": {"type": "string", "description": "LLM provider name"}, + "weight": {"type": "integer", "description": "Weight of the upstream"}, + "modelMapping": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Model mapping" + } + }, + "required": ["provider", "weight"] + }, + "description": "Route upstreams" + }, + "modelPredicates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "matchType": {"type": "string", "enum": ["EQUAL", "PRE", "REGULAR"], "description": "Match type"}, + "matchValue": {"type": "string", "description": "The value to match against"}, + "caseSensitive": {"type": "boolean", "description": "Whether to match the value case-sensitively"} + }, + "required": ["matchType", "matchValue"] + }, + "description": "Model predicates" + }, + "authConfig": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "description": "Whether auth is enabled"}, + "allowedConsumers": { + "type": "array", + "items": {"type": "string"}, + "description": "Allowed consumer names" + } + }, + "description": "Route auth configuration" + }, + "fallbackConfig": { + "type": "object", + "properties": { + "enabled": {"type": "boolean", "description": "Whether fallback is enabled"}, + "upstreams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": {"type": "string", "description": "LLM provider name"}, + "weight": {"type": "integer", "description": "Weight of the upstream"}, + "modelMapping": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Model mapping" + } + }, + "required": ["provider", "weight"] + }, + "description": "Fallback upstreams. Only one upstream is allowed when fallbackStrategy is SEQ." + }, + "fallbackStrategy": {"type": "string", "enum": ["RAND", "SEQ"], "description": "Fallback strategy"}, + "responseCodes": { + "type": "array", + "items": {"type": "string"}, + "description": "Response codes that need fallback" + } + }, + "description": "AI Route fallback configuration" + } + }, + "additionalProperties": false + } + }, + "required": ["name", "configurations"], + "additionalProperties": false + }`) +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/mcp_server.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/mcp_server.go new file mode 100644 index 000000000..edeea1f99 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/mcp_server.go @@ -0,0 +1,610 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress" + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/mark3labs/mcp-go/mcp" +) + +// McpServer represents an MCP server configuration +type McpServer struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Domains []string `json:"domains,omitempty"` + Services []McpUpstreamService `json:"services,omitempty"` + Type string `json:"type"` + ConsumerAuthInfo *ConsumerAuthInfo `json:"consumerAuthInfo,omitempty"` + RawConfigurations string `json:"rawConfigurations,omitempty"` + DSN string `json:"dsn,omitempty"` + DBType string `json:"dbType,omitempty"` + UpstreamPathPrefix string `json:"upstreamPathPrefix,omitempty"` + McpServerName string `json:"mcpServerName,omitempty"` +} + +// McpUpstreamService represents a service in MCP server +type McpUpstreamService struct { + Name string `json:"name"` + Port int `json:"port"` + Version string `json:"version,omitempty"` + Weight int `json:"weight"` +} + +// ConsumerAuthInfo represents consumer authentication information +type ConsumerAuthInfo struct { + Type string `json:"type,omitempty"` + Enable bool `json:"enable,omitempty"` + AllowedConsumers []string `json:"allowedConsumers,omitempty"` +} + +// McpServerConsumers represents MCP server consumers configuration +type McpServerConsumers struct { + McpServerName string `json:"mcpServerName"` + Consumers []string `json:"consumers"` +} + +// McpServerConsumerDetail represents detailed consumer information +type McpServerConsumerDetail struct { + McpServerName string `json:"mcpServerName"` + ConsumerName string `json:"consumerName"` + Type string `json:"type,omitempty"` +} + +// SwaggerContent represents swagger content for conversion +type SwaggerContent struct { + Content string `json:"content"` +} + +// McpServerResponse represents the API response for MCP server operations +type McpServerResponse = higress.APIResponse[McpServer] + +// McpServerConsumerDetailResponse represents the API response for MCP server consumer operations +type McpServerConsumerDetailResponse = higress.APIResponse[[]McpServerConsumerDetail] + +// RegisterMcpServerTools registers all MCP server management tools +func RegisterMcpServerTools(mcpServer *common.MCPServer, client *higress.HigressClient) { + // List MCP servers + mcpServer.AddTool( + mcp.NewToolWithRawSchema("list-mcp-servers", "List all MCP servers", listMcpServersSchema()), + handleListMcpServers(client), + ) + + // Get specific MCP server + mcpServer.AddTool( + mcp.NewToolWithRawSchema("get-mcp-server", "Get detailed information about a specific MCP server", getMcpServerSchema()), + handleGetMcpServer(client), + ) + + // Add or update MCP server + mcpServer.AddTool( + mcp.NewToolWithRawSchema("add-or-update-mcp-server", "Add or update an MCP server instance", getAddOrUpdateMcpServerSchema()), + handleAddOrUpdateMcpServer(client), + ) + + // Delete MCP server + mcpServer.AddTool( + mcp.NewToolWithRawSchema("delete-mcp-server", "Delete an MCP server", getMcpServerSchema()), + handleDeleteMcpServer(client), + ) + + // List MCP server consumers + mcpServer.AddTool( + mcp.NewToolWithRawSchema("list-mcp-server-consumers", "List MCP server allowed consumers", listMcpServerConsumersSchema()), + handleListMcpServerConsumers(client), + ) + + // Add MCP server consumers + mcpServer.AddTool( + mcp.NewToolWithRawSchema("add-mcp-server-consumers", "Add MCP server allowed consumers", getMcpServerConsumersSchema()), + handleAddMcpServerConsumers(client), + ) + + // Delete MCP server consumers + mcpServer.AddTool( + mcp.NewToolWithRawSchema("delete-mcp-server-consumers", "Delete MCP server allowed consumers", getMcpServerConsumersSchema()), + handleDeleteMcpServerConsumers(client), + ) + + // Convert Swagger to MCP config + mcpServer.AddTool( + mcp.NewToolWithRawSchema("swagger-to-mcp-config", "Convert Swagger content to MCP configuration", getSwaggerToMcpConfigSchema()), + handleSwaggerToMcpConfig(client), + ) +} + +func handleListMcpServers(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + + // Build query parameters + queryParams := "" + if mcpServerName, ok := arguments["mcpServerName"].(string); ok && mcpServerName != "" { + queryParams += "?mcpServerName=" + mcpServerName + } + if mcpType, ok := arguments["type"].(string); ok && mcpType != "" { + if queryParams == "" { + queryParams += "?type=" + mcpType + } else { + queryParams += "&type=" + mcpType + } + } + if pageNum, ok := arguments["pageNum"].(string); ok && pageNum != "" { + if queryParams == "" { + queryParams += "?pageNum=" + pageNum + } else { + queryParams += "&pageNum=" + pageNum + } + } + if pageSize, ok := arguments["pageSize"].(string); ok && pageSize != "" { + if queryParams == "" { + queryParams += "?pageSize=" + pageSize + } else { + queryParams += "&pageSize=" + pageSize + } + } + + respBody, err := client.Get(ctx, "/v1/mcpServer"+queryParams) + if err != nil { + return nil, fmt.Errorf("failed to list MCP servers: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleGetMcpServer(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + respBody, err := client.Get(ctx, fmt.Sprintf("/v1/mcpServer/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to get MCP server '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleAddOrUpdateMcpServer(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Validate required fields + if _, ok := configurations["name"]; !ok { + return nil, fmt.Errorf("missing required field 'name' in configurations") + } + if _, ok := configurations["type"]; !ok { + return nil, fmt.Errorf("missing required field 'type' in configurations") + } + + // Validate service sources exist + if services, ok := configurations["services"].([]interface{}); ok && len(services) > 0 { + for _, svc := range services { + if serviceMap, ok := svc.(map[string]interface{}); ok { + if serviceName, ok := serviceMap["name"].(string); ok { + // Extract service source name from "serviceName.serviceType" format + var serviceSourceName string + for i := len(serviceName) - 1; i >= 0; i-- { + if serviceName[i] == '.' { + serviceSourceName = serviceName[:i] + break + } + } + + if serviceSourceName == "" { + return nil, fmt.Errorf("invalid service name format '%s', expected 'serviceName.serviceType'", serviceName) + } + + // Check if service source exists + _, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", serviceSourceName)) + if err != nil { + return nil, fmt.Errorf("Please create the service source '%s' first and then create the mcpserver", serviceSourceName) + } + } + } + } + } + + respBody, err := client.Put(ctx, "/v1/mcpServer", configurations) + if err != nil { + return nil, fmt.Errorf("failed to add or update MCP server: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleDeleteMcpServer(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + name, ok := arguments["name"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'name' argument") + } + + respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/mcpServer/%s", name)) + if err != nil { + return nil, fmt.Errorf("failed to delete MCP server '%s': %w", name, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleListMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + + // Build query parameters + queryParams := "" + if mcpServerName, ok := arguments["mcpServerName"].(string); ok && mcpServerName != "" { + queryParams += "?mcpServerName=" + mcpServerName + } + if consumerName, ok := arguments["consumerName"].(string); ok && consumerName != "" { + if queryParams == "" { + queryParams += "?consumerName=" + consumerName + } else { + queryParams += "&consumerName=" + consumerName + } + } + if pageNum, ok := arguments["pageNum"].(string); ok && pageNum != "" { + if queryParams == "" { + queryParams += "?pageNum=" + pageNum + } else { + queryParams += "&pageNum=" + pageNum + } + } + if pageSize, ok := arguments["pageSize"].(string); ok && pageSize != "" { + if queryParams == "" { + queryParams += "?pageSize=" + pageSize + } else { + queryParams += "&pageSize=" + pageSize + } + } + + respBody, err := client.Get(ctx, "/v1/mcpServer/consumers"+queryParams) + if err != nil { + return nil, fmt.Errorf("failed to list MCP server consumers: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleAddMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Validate required fields + if _, ok := configurations["mcpServerName"]; !ok { + return nil, fmt.Errorf("missing required field 'mcpServerName' in configurations") + } + if _, ok := configurations["consumers"]; !ok { + return nil, fmt.Errorf("missing required field 'consumers' in configurations") + } + + respBody, err := client.Put(ctx, "/v1/mcpServer/consumers", configurations) + if err != nil { + return nil, fmt.Errorf("failed to add MCP server consumers: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleDeleteMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Validate required fields + if _, ok := configurations["mcpServerName"]; !ok { + return nil, fmt.Errorf("missing required field 'mcpServerName' in configurations") + } + if _, ok := configurations["consumers"]; !ok { + return nil, fmt.Errorf("missing required field 'consumers' in configurations") + } + + respBody, err := client.DeleteWithBody(ctx, "/v1/mcpServer/consumers", configurations) + if err != nil { + return nil, fmt.Errorf("failed to delete MCP server consumers: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func handleSwaggerToMcpConfig(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + configurations, ok := arguments["configurations"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid 'configurations' argument") + } + + // Validate required fields + if _, ok := configurations["content"]; !ok { + return nil, fmt.Errorf("missing required field 'content' in configurations") + } + + respBody, err := client.Post(ctx, "/v1/mcpServer/swaggerToMcpConfig", configurations) + if err != nil { + return nil, fmt.Errorf("failed to convert swagger to MCP config: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +// Schema definitions + +func listMcpServersSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "mcpServerName": { + "type": "string", + "description": "McpServer name associated with route" + }, + "type": { + "type": "string", + "description": "Mcp server type" + }, + "pageNum": { + "type": "string", + "description": "Page number, starting from 1. If omitted, all items will be returned" + }, + "pageSize": { + "type": "string", + "description": "Number of items per page. If omitted, all items will be returned" + } + }, + "required": [], + "additionalProperties": false + }`) +} + +func getMcpServerSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the MCP server" + } + }, + "required": ["name"], + "additionalProperties": false + }`) +} + +func getAddOrUpdateMcpServerSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "configurations": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Mcp server name" + }, + "description": { + "type": "string", + "description": "Mcp server description" + }, + "domains": { + "type": "array", + "items": {"type": "string"}, + "description": "Domains that the mcp server applies to" + }, + "services": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "must be service name + service type, such as:daxt-mcp.static .which must be real exist service"}, + "port": {"type": "integer", "description": "Service port"}, + "version": {"type": "string", "description": "Service version"}, + "weight": {"type": "integer", "description": "Service weight"} + }, + "required": ["name", "port", "weight"] + }, + "description": "Mcp server upstream services" + }, + "type": { + "type": "string", + "enum": ["OPEN_API", "DATABASE", "DIRECT_ROUTE"], + "description": "Mcp Server Type" + }, + "consumerAuthInfo": { + "type": "object", + "properties": { + "type": {"type": "string", "description": "Consumer auth type,if not enable, it value must be API_KEY "}, + "enable": {"type": "boolean", "description": "Whether consumer auth is enabled"}, + "allowedConsumers": { + "type": "array", + "items": {"type": "string"}, + "description": "Allowed consumer names" + } + }, + "description": "Mcp server consumer auth info" + }, + "rawConfigurations": { + "type": "string", + "description": "Raw configurations in YAML format" + }, + "dsn": { + "type": "string", + "description": "Data Source Name. For DB type server, it is required such as username:passwd@tcp(ip:port)/Database?charset=utf8mb4&parseTime=True&loc=Local .For other, it can be empty." + }, + "dbType": { + "type": "string", + "enum": ["MYSQL", "POSTGRESQL", "SQLITE", "CLICKHOUSE"], + "description": "Mcp Server DB Type,only if type is DATABASE, it is required" + }, + "upstreamPathPrefix": { + "type": "string", + "description": "The upstream MCP server will redirect requests based on the path prefix" + }, + "mcpServerName": { + "type": "string", + "description": "Mcp server name (usually same as 'name' field)" + } + }, + "required": ["name", "type", "dsn", "services"], + "additionalProperties": false + } + }, + "required": ["configurations"], + "additionalProperties": false + }`) +} + +func listMcpServerConsumersSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "mcpServerName": { + "type": "string", + "description": "McpServer name associated with route" + }, + "consumerName": { + "type": "string", + "description": "Consumer name for search" + }, + "pageNum": { + "type": "string", + "description": "Page number, starting from 1. If omitted, all items will be returned" + }, + "pageSize": { + "type": "string", + "description": "Number of items per page. If omitted, all items will be returned" + } + }, + "required": [], + "additionalProperties": false + }`) +} + +func getMcpServerConsumersSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "configurations": { + "type": "object", + "properties": { + "mcpServerName": { + "type": "string", + "description": "Mcp server route name" + }, + "consumers": { + "type": "array", + "items": {"type": "string"}, + "description": "Consumer names" + } + }, + "required": ["mcpServerName", "consumers"], + "additionalProperties": false + } + }, + "required": ["configurations"], + "additionalProperties": false + }`) +} + +func getSwaggerToMcpConfigSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "configurations": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Swagger content" + } + }, + "required": ["content"], + "additionalProperties": false + } + }, + "required": ["configurations"], + "additionalProperties": false + }`) +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/common.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/common.go index c504d758f..013aff0c3 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/common.go +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/common.go @@ -116,7 +116,7 @@ func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc // Build API path and make request path := BuildPluginPath(pluginName, scope, resourceName) - respBody, err := client.Get(path) + respBody, err := client.Get(ctx, path) if err != nil { return nil, fmt.Errorf("failed to get plugin config for '%s' at scope '%s': %w", pluginName, scope, err) } @@ -162,7 +162,7 @@ func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerF // Build API path and make request path := BuildPluginPath(pluginName, scope, resourceName) - respBody, err := client.Delete(path) + respBody, err := client.Delete(ctx, path) if err != nil { return nil, fmt.Errorf("failed to delete plugin config for '%s' at scope '%s': %w", pluginName, scope, err) } diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/custom-response.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/custom-response.go new file mode 100644 index 000000000..78e5f7f91 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/custom-response.go @@ -0,0 +1,178 @@ +package plugins + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress" + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/mark3labs/mcp-go/mcp" +) + +const CustomResponsePluginName = "custom-response" + +// CustomResponseConfig represents the configuration for custom-response plugin +type CustomResponseConfig struct { + Body string `json:"body,omitempty"` + Headers []string `json:"headers,omitempty"` + StatusCode int `json:"status_code,omitempty"` + EnableOnStatus []int `json:"enable_on_status,omitempty"` +} + +// CustomResponseInstance represents a custom-response plugin instance +type CustomResponseInstance = PluginInstance[CustomResponseConfig] + +// CustomResponseResponse represents the API response for custom-response plugin +type CustomResponseResponse = higress.APIResponse[CustomResponseInstance] + +// RegisterCustomResponsePluginTools registers all custom response plugin management tools +func RegisterCustomResponsePluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) { + // Update custom response configuration + mcpServer.AddTool( + mcp.NewToolWithRawSchema(fmt.Sprintf("update-%s-plugin", CustomResponsePluginName), "Update custom response plugin configuration", getAddOrUpdateCustomResponseConfigSchema()), + handleAddOrUpdateCustomResponseConfig(client), + ) +} + +func handleAddOrUpdateCustomResponseConfig(client *higress.HigressClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + + // Parse required parameters + scope, ok := arguments["scope"].(string) + if !ok { + return nil, fmt.Errorf("missing or invalid 'scope' argument") + } + + if !IsValidScope(scope) { + return nil, fmt.Errorf("invalid scope '%s', must be one of: %v", scope, ValidScopes) + } + + enabled, ok := arguments["enabled"].(bool) + if !ok { + return nil, fmt.Errorf("missing or invalid 'enabled' argument") + } + + configurations, ok := arguments["configurations"] + if !ok { + return nil, fmt.Errorf("missing 'configurations' argument") + } + + // Parse resource_name for non-global scopes + var resourceName string + if scope != ScopeGlobal { + // Validate and get resource_name + resourceName, ok = arguments["resource_name"].(string) + if !ok || resourceName == "" { + return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope) + } + } + + // Build API path + path := BuildPluginPath(CustomResponsePluginName, scope, resourceName) + + // Get current custom response configuration to merge with updates + currentBody, err := client.Get(ctx, path) + if err != nil { + return nil, fmt.Errorf("failed to get current custom response configuration: %w", err) + } + + var response CustomResponseResponse + if err := json.Unmarshal(currentBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse current custom response response: %w", err) + } + + currentConfig := response.Data + currentConfig.Enabled = enabled + currentConfig.Scope = scope + + // Convert the input configurations to CustomResponseConfig and merge + configBytes, err := json.Marshal(configurations) + if err != nil { + return nil, fmt.Errorf("failed to marshal configurations: %w", err) + } + + var newConfig CustomResponseConfig + if err := json.Unmarshal(configBytes, &newConfig); err != nil { + return nil, fmt.Errorf("failed to parse custom response configurations: %w", err) + } + + // Update configurations (overwrite with new values where provided) + if newConfig.Body != "" { + currentConfig.Configurations.Body = newConfig.Body + } + if newConfig.Headers != nil { + currentConfig.Configurations.Headers = newConfig.Headers + } + if newConfig.StatusCode != 0 { + currentConfig.Configurations.StatusCode = newConfig.StatusCode + } + if newConfig.EnableOnStatus != nil { + currentConfig.Configurations.EnableOnStatus = newConfig.EnableOnStatus + } + + respBody, err := client.Put(ctx, path, currentConfig) + if err != nil { + return nil, fmt.Errorf("failed to update custom response config at scope '%s': %w", scope, err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(respBody), + }, + }, + }, nil + } +} + +func getAddOrUpdateCustomResponseConfigSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": { + "scope": { + "type": "string", + "enum": ["GLOBAL", "DOMAIN", "SERVICE", "ROUTE"], + "description": "The scope at which the plugin is applied" + }, + "resource_name": { + "type": "string", + "description": "The name of the resource (required for DOMAIN, SERVICE, ROUTE scopes)" + }, + "enabled": { + "type": "boolean", + "description": "Whether the plugin is enabled" + }, + "configurations": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "Custom response body content" + }, + "headers": { + "type": "array", + "items": {"type": "string"}, + "description": "List of custom response headers in the format 'Header-Name=value'" + }, + "status_code": { + "type": "integer", + "minimum": 100, + "maximum": 599, + "description": "HTTP status code to return in the custom response" + }, + "enable_on_status": { + "type": "array", + "items": {"type": "integer"}, + "description": "List of upstream status codes that trigger this custom response" + } + }, + "additionalProperties": false + } + }, + "required": ["scope", "enabled", "configurations"], + "additionalProperties": false + }`) +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/request-block.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/request-block.go index 5d9450a5e..58deab96d 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/request-block.go +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins/request-block.go @@ -74,7 +74,7 @@ func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.T path := BuildPluginPath(RequestBlockPluginName, scope, resourceName) // Get current request block configuration to merge with updates - currentBody, err := client.Get(path) + currentBody, err := client.Get(ctx, path) if err != nil { return nil, fmt.Errorf("failed to get current request block configuration: %w", err) } @@ -114,7 +114,7 @@ func handleAddOrUpdateRequestBlockConfig(client *higress.HigressClient) common.T } currentConfig.Configurations.CaseSensitive = newConfig.CaseSensitive - respBody, err := client.Put(path, currentConfig) + respBody, err := client.Put(ctx, path, currentConfig) if err != nil { return nil, fmt.Errorf("failed to update request block config at scope '%s': %w", scope, err) } diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/route.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/route.go index 7e5d15094..63c382f15 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/route.go +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/route.go @@ -58,7 +58,7 @@ type RouteResponse = higress.APIResponse[Route] func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) { // List all routes mcpServer.AddTool( - mcp.NewTool("list-routes", mcp.WithDescription("List all available routes")), + mcp.NewToolWithRawSchema("list-routes", "List all available routes", listRouteSchema()), handleListRoutes(client), ) @@ -89,7 +89,7 @@ func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClie func handleListRoutes(client *higress.HigressClient) common.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - respBody, err := client.Get("/v1/routes") + respBody, err := client.Get(ctx, "/v1/routes") if err != nil { return nil, fmt.Errorf("failed to list routes: %w", err) } @@ -113,7 +113,7 @@ func handleGetRoute(client *higress.HigressClient) common.ToolHandlerFunc { return nil, fmt.Errorf("missing or invalid 'name' argument") } - respBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name)) + respBody, err := client.Get(ctx, fmt.Sprintf("/v1/routes/%s", name)) if err != nil { return nil, fmt.Errorf("failed to get route '%s': %w", name, err) } @@ -148,7 +148,35 @@ func handleAddRoute(client *higress.HigressClient) common.ToolHandlerFunc { return nil, fmt.Errorf("missing required field 'services' in configurations") } - respBody, err := client.Post("/v1/routes", configurations) + // Validate service sources exist + if services, ok := configurations["services"].([]interface{}); ok && len(services) > 0 { + for _, svc := range services { + if serviceMap, ok := svc.(map[string]interface{}); ok { + if serviceName, ok := serviceMap["name"].(string); ok { + // Extract service source name from "serviceName.serviceType" format + var serviceSourceName string + for i := len(serviceName) - 1; i >= 0; i-- { + if serviceName[i] == '.' { + serviceSourceName = serviceName[:i] + break + } + } + + if serviceSourceName == "" { + return nil, fmt.Errorf("invalid service name format '%s', expected 'serviceName.serviceType'", serviceName) + } + + // Check if service source exists + _, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", serviceSourceName)) + if err != nil { + return nil, fmt.Errorf("Please create the service source '%s' first and then create the route", serviceSourceName) + } + } + } + } + } + + respBody, err := client.Post(ctx, "/v1/routes", configurations) if err != nil { return nil, fmt.Errorf("failed to add route: %w", err) } @@ -178,7 +206,7 @@ func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc { } // Get current route configuration to merge with updates - currentBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name)) + currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/routes/%s", name)) if err != nil { return nil, fmt.Errorf("failed to get current route configuration: %w", err) } @@ -227,7 +255,7 @@ func handleUpdateRoute(client *higress.HigressClient) common.ToolHandlerFunc { currentConfig.CustomConfigs = newConfig.CustomConfigs } - respBody, err := client.Put(fmt.Sprintf("/v1/routes/%s", name), currentConfig) + respBody, err := client.Put(ctx, fmt.Sprintf("/v1/routes/%s", name), currentConfig) if err != nil { return nil, fmt.Errorf("failed to update route '%s': %w", name, err) } @@ -251,7 +279,7 @@ func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc { return nil, fmt.Errorf("missing or invalid 'name' argument") } - respBody, err := client.Delete(fmt.Sprintf("/v1/routes/%s", name)) + respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/routes/%s", name)) if err != nil { return nil, fmt.Errorf("failed to delete route '%s': %w", name, err) } @@ -267,6 +295,15 @@ func handleDeleteRoute(client *higress.HigressClient) common.ToolHandlerFunc { } } +func listRouteSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }`) +} + func getRouteSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", @@ -295,7 +332,7 @@ func getAddRouteSchema() json.RawMessage { "domains": { "type": "array", "items": {"type": "string"}, - "description": "List of domain names, but only one domain is allowed" + "description": "List of domain names, but only one domain is allowed,Do not fill in the code to match all" }, "path": { "type": "object", diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/service.go b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/service.go index 1f9e6b1d8..43af07457 100644 --- a/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/service.go +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net" "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress" "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" @@ -37,7 +38,7 @@ type ServiceSourceResponse = higress.APIResponse[ServiceSource] func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressClient) { // List all service sources mcpServer.AddTool( - mcp.NewTool("list-service-sources", mcp.WithDescription("List all available service sources")), + mcp.NewToolWithRawSchema("list-service-sources", "List all available service sources", listServiceSourcesSchema()), handleListServiceSources(client), ) @@ -68,7 +69,7 @@ func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressCl func handleListServiceSources(client *higress.HigressClient) common.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - respBody, err := client.Get("/v1/service-sources") + respBody, err := client.Get(ctx, "/v1/service-sources") if err != nil { return nil, fmt.Errorf("failed to list service sources: %w", err) } @@ -92,7 +93,7 @@ func handleGetServiceSource(client *higress.HigressClient) common.ToolHandlerFun return nil, fmt.Errorf("missing or invalid 'name' argument") } - respBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name)) + respBody, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", name)) if err != nil { return nil, fmt.Errorf("failed to get service source '%s': %w", name, err) } @@ -129,8 +130,28 @@ func handleAddServiceSource(client *higress.HigressClient) common.ToolHandlerFun if _, ok := configurations["port"]; !ok { return nil, fmt.Errorf("missing required field 'port' in configurations") } + if t, ok := configurations["type"].(string); ok && t == "static" { + if d, ok := configurations["domain"].(string); ok { + host, port, err := net.SplitHostPort(d) + if err != nil || host == "" || port == "" { + return nil, fmt.Errorf("invalid 'domain' format for static type, expected ip:port, got '%s'", d) + } + } else { + return nil, fmt.Errorf("invalid 'domain' field type, expected string") + } + } + if t, ok := configurations["type"].(string); ok && t != "static" { + if d, ok := configurations["domain"].(string); ok { + host, _, err := net.SplitHostPort(d) + if err == nil && host != "" { + configurations["domain"] = host + } + } + } - respBody, err := client.Post("/v1/service-sources", configurations) + // valid protocol,sni,properties,auth + + respBody, err := client.Post(ctx, "/v1/service-sources", configurations) if err != nil { return nil, fmt.Errorf("failed to add service source: %w", err) } @@ -160,7 +181,7 @@ func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandler } // Get current service source configuration to merge with updates - currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name)) + currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", name)) if err != nil { return nil, fmt.Errorf("failed to get current service source configuration: %w", err) } @@ -209,7 +230,7 @@ func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandler currentConfig.AuthN = newConfig.AuthN } - respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig) + respBody, err := client.Put(ctx, fmt.Sprintf("/v1/service-sources/%s", name), currentConfig) if err != nil { return nil, fmt.Errorf("failed to update service source '%s': %w", name, err) } @@ -233,7 +254,7 @@ func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandler return nil, fmt.Errorf("missing or invalid 'name' argument") } - respBody, err := client.Delete(fmt.Sprintf("/v1/service-sources/%s", name)) + respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/service-sources/%s", name)) if err != nil { return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err) } @@ -249,6 +270,15 @@ func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandler } } +func listServiceSourcesSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }`) +} + func getServiceSourceSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", @@ -263,7 +293,6 @@ func getServiceSourceSchema() json.RawMessage { }`) } -// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka. func getAddServiceSourceSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", @@ -277,12 +306,12 @@ func getAddServiceSourceSchema() json.RawMessage { }, "type": { "type": "string", - "enum": ["static", "dns"], - "description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution" + "enum": ["static", "dns", "consul", "nacos3","nacos2","nacos1", "eureka", "zookeeper"], + "description": "The type of service source. Supported types: 'static' (static IP), 'dns' (DNS resolution), 'consul' (Consul registry), 'nacos3' (Nacos 3.x), 'eureka' (Eureka registry), 'zookeeper' (ZooKeeper registry)" }, "domain": { "type": "string", - "description": "The domain name or IP address (required)" + "description": "The domain name or IP address + port(such as: 127.0.0.1:8080) (required). For dns, use domain name (e.g., 'xxx.com')" }, "port": { "type": "integer", @@ -292,12 +321,32 @@ func getAddServiceSourceSchema() json.RawMessage { }, "protocol": { "type": "string", - "enum": ["http", "https"], - "description": "The protocol to use (optional, defaults to http)" + "enum": ["http", "https", ""], + "description": "The protocol to use (optional, defaults to http, can be empty string for null)" }, "sni": { "type": "string", "description": "Server Name Indication for HTTPS connections (optional)" + }, + "properties": { + "type": "object", + "additionalProperties": true, + "description": "Type-specific configuration properties. Required fields by type: consul: 'consulDatacenter' (string), 'consulServiceTag' (string, format: 'key=value'); nacos3: 'nacosNamespaceId' (string, optional), 'nacosGroups' (array of strings), 'enableMCPServer' (boolean, optional), 'mcpServerBaseUrl' (string, required if enableMCPServer is true, e.g., '/mcp'), 'mcpServerExportDomains' (array of strings, required if enableMCPServer is true, e.g., ['xxx.com']); zookeeper: 'zkServicesPath' (array of strings); static/dns/eureka: no additional properties needed" + }, + "authN": { + "type": "object", + "description": "Authentication configuration", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether authentication is enabled" + }, + "properties": { + "type": "object", + "additionalProperties": true, + "description": "Authentication properties by type. consul: 'consulToken' (string); nacos3: 'nacosUsername' (string), 'nacosPassword' (string)" + } + } } }, "required": ["name", "type", "domain", "port"], @@ -309,7 +358,6 @@ func getAddServiceSourceSchema() json.RawMessage { }`) } -// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka. func getUpdateServiceSourceSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", @@ -323,12 +371,12 @@ func getUpdateServiceSourceSchema() json.RawMessage { "properties": { "type": { "type": "string", - "enum": ["static", "dns"], - "description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution" + "enum": ["static", "dns", "consul", "nacos3", "eureka", "zookeeper"], + "description": "The type of service source. Supported types: 'static' (static IP), 'dns' (DNS resolution), 'consul' (Consul registry), 'nacos3' (Nacos 3.x), 'eureka' (Eureka registry), 'zookeeper' (ZooKeeper registry)" }, "domain": { "type": "string", - "description": "The domain name or IP address" + "description": "The domain name or IP address + port(such as: 127.0.0.1:8080) (required). For dns, use domain name (e.g., 'xxx.com')" }, "port": { "type": "integer", @@ -338,12 +386,32 @@ func getUpdateServiceSourceSchema() json.RawMessage { }, "protocol": { "type": "string", - "enum": ["http", "https"], - "description": "The protocol to use (optional, defaults to http)" + "enum": ["http", "https", ""], + "description": "The protocol to use (optional, can be empty string for null)" }, "sni": { "type": "string", "description": "Server Name Indication for HTTPS connections" + }, + "properties": { + "type": "object", + "additionalProperties": true, + "description": "Type-specific configuration properties. Required fields by type: consul: 'consulDatacenter' (string), 'consulServiceTag' (string, format: 'key=value'); nacos3: 'nacosNamespaceId' (string, optional), 'nacosGroups' (array of strings), 'enableMCPServer' (boolean, optional), 'mcpServerBaseUrl' (string, required if enableMCPServer is true, e.g., '/mcp'), 'mcpServerExportDomains' (array of strings, required if enableMCPServer is true, e.g., ['xxx.com']); zookeeper: 'zkServicesPath' (array of strings); static/dns/eureka: no additional properties needed" + }, + "authN": { + "type": "object", + "description": "Authentication configuration", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether authentication is enabled" + }, + "properties": { + "type": "object", + "additionalProperties": true, + "description": "Authentication properties by type. consul: 'consulToken' (string); nacos: 'nacosUsername' (string), 'nacosPassword' (string)" + } + } } }, "additionalProperties": false diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/.keep b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/README.md b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/README.md new file mode 100644 index 000000000..dda382715 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/README.md @@ -0,0 +1,197 @@ +# Higress Ops MCP Server + +Higress Ops MCP Server 提供了 MCP 工具来调试和监控 Istio 和 Envoy 组件,帮助运维人员进行故障诊断和性能分析。 + +## 功能特性 + +### Istiod 调试接口 + +#### 配置相关 +- `get-istiod-configz`: 获取 Istiod 的配置状态和错误信息 + +#### 服务发现相关 +- `get-istiod-endpointz`: 获取 Istiod 发现的所有服务端点信息 +- `get-istiod-clusters`: 获取 Istiod 发现的所有集群信息 +- `get-istiod-registryz`: 获取 Istiod 的服务注册表信息 + +#### 状态监控相关 +- `get-istiod-syncz`: 获取 Istiod 与 Envoy 代理的同步状态信息 +- `get-istiod-metrics`: 获取 Istiod 的 Prometheus 指标数据 + +#### 系统信息相关 +- `get-istiod-version`: 获取 Istiod 的版本信息 +- `get-istiod-debug-vars`: 获取 Istiod 的调试变量信息 + +### Envoy 调试接口 + +#### 配置相关 +- `get-envoy-config-dump`: 获取 Envoy 的完整配置快照,支持资源过滤和敏感信息掩码 +- `get-envoy-listeners`: 获取 Envoy 的所有监听器信息 +- `get-envoy-clusters`: 获取 Envoy 的所有集群信息和健康状态 + +#### 运行时相关 +- `get-envoy-stats`: 获取 Envoy 的统计信息,支持过滤器和多种输出格式 +- `get-envoy-runtime`: 获取 Envoy 的运行时配置信息 +- `get-envoy-memory`: 获取 Envoy 的内存使用情况 + +#### 状态检查相关 +- `get-envoy-server-info`: 获取 Envoy 服务器的基本信息 +- `get-envoy-ready`: 检查 Envoy 是否准备就绪 +- `get-envoy-hot-restart-version`: 获取 Envoy 热重启版本信息 + +#### 安全相关 +- `get-envoy-certs`: 获取 Envoy 的证书信息 + +## 配置参数 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| `istiodURL` | string | 必填 | Istiod 调试接口的 URL 地址 | +| `envoyAdminURL` | string | 必填 | Envoy Admin 接口的 URL 地址 | +| `namespace` | string | 可选 | Kubernetes 命名空间,默认为 `higress-system` | +| `description` | string | 可选 | 服务器描述信息,默认为 "Higress Ops MCP Server, which provides debug interfaces for Istio and Envoy components." | + +## 配置示例 + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: higress-config + namespace: higress-system +data: + higress: | + mcpServer: + sse_path_suffix: /sse # SSE 连接的路径后缀 + enable: true # 启用 MCP Server + redis: + address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis服务地址 + username: "" # Redis用户名(可选) + password: "" # Redis密码(可选) + db: 0 # Redis数据库(可选) + match_list: # MCP Server 会话保持路由规则 + - match_rule_domain: "*" + match_rule_path: /higress-ops + match_rule_type: "prefix" + servers: + - name: higress-ops-mcp-server + path: /higress-ops + type: higress-ops + config: + istiodURL: http://higress-controller.higress-system.svc.cluster.local:15014 # istiod url + envoyAdminURL: http://127.0.0.1:15000 # envoy url 填127.0.0.1就行,和 gateway 于同一容器 + namespace: higress-system + description: "Higress Ops MCP Server for Istio and Envoy debugging" +``` + +## 鉴权配置 + +Higress Ops MCP Server 使用自定义 HTTP Header 进行鉴权。客户端需要在请求头中携带 Istiod 认证 Token。 + +### Token 生成方式 + +使用以下命令生成长期有效的 Istiod 认证 Token: + +```bash +kubectl create token higress-gateway -n higress-system --audience istio-ca --duration 87600h +``` + +**参数说明:** +- `higress-gateway`: ServiceAccount 名称(与 Higress Gateway Pod 使用的 ServiceAccount 一致) +- `-n higress-system`: 命名空间(需要与配置参数 `namespace` 一致) +- `--audience istio-ca`: Token 的受众,必须为 `istio-ca` +- `--duration 87600h`: Token 有效期(87600小时 ≈ 10年) + +### 配置示例 + +```json +{ + "mcpServers": { + "higress_ops_mcp": { + "url": "http://127.0.0.1:80/higress-ops/sse", + "headers": { + "X-Istiod-Token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1IUlI0Z01ISUNBNVlZbDBHcVVBMjFhMklwQ3hFaHIxSlVlamtzTFRLOTQifQ..." + } + } + } +} +``` + +**说明:** +- `X-Istiod-Token` 头用于携带 Istiod 认证 Token +- Token 值由上述 `kubectl create token` 命令生成 +- 如果未配置 Token,跨 Pod 访问 Istiod 接口时会遇到 401 认证错误 + +## 演示 + +1. get envoy route information + +https://private-user-images.githubusercontent.com/153273766/507769115-d8e20b70-db1a-4a82-b89a-9eefeb3c8982.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTE1LWQ4ZTIwYjcwLWRiMWEtNGE4Mi1iODlhLTllZWZlYjNjODk4Mi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kYzg1Y2FiOTdiN2FiOTNkMmQ0OTc1NzEyZGMyMTlkNDQ4YjQ0NGYyOGUwNTlhYzYyYzA1ODJhOWM0M2Y3ZTQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Uz-HfM9tOzl7zrhGsPP1suunGg_K9ZbUN1BzAU5Oquo + +2. get istiod cluster information + +https://private-user-images.githubusercontent.com/153273766/507769013-9f598593-1251-4304-8e41-8bf4d1588897.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MDEzLTlmNTk4NTkzLTEyNTEtNDMwNC04ZTQxLThiZjRkMTU4ODg5Ny5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hZDQwYWE3MjM5OTU1NGNkMDcwNTgzNDMzZGI4NDRkYzdiNWRlNGJhODMwNjFlYjZiZjUzNzM3YWFhYzIyMjBjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.g19-rxOHSLIIszdGYAI7CmRzLTlrbA1fJ0hB6duuDBI + + + +## 使用场景 + +### 1. 故障诊断 +- 使用 `get-istiod-syncz` 检查配置同步状态 +- 使用 `get-envoy-clusters` 检查集群健康状态 +- 使用 `get-envoy-listeners` 检查监听器配置 + +### 2. 性能分析 +- 使用 `get-istiod-metrics` 获取 Istiod 性能指标 +- 使用 `get-envoy-stats` 获取 Envoy 统计信息 +- 使用 `get-envoy-memory` 监控内存使用 + +### 3. 配置验证 +- 使用 `get-istiod-configz` 验证 Istiod 配置状态 +- 使用 `get-envoy-config-dump` 验证 Envoy 配置 + +### 4. 安全审计 +- 使用 `get-envoy-certs` 检查证书状态 +- 使用 `get-istiod-debug-vars` 查看调试变量 + +## 工具参数示例 + +### Istiod 工具示例 + +```bash +# 获取配置状态 +get-istiod-configz + +# 获取同步状态 +get-istiod-syncz + +# 获取端点信息 +get-istiod-endpointz +``` + +### Envoy 工具示例 + +```bash +# 获取配置快照,过滤监听器配置 +get-envoy-config-dump --resource="listeners" + +# 获取集群信息,JSON 格式输出 +get-envoy-clusters --format="json" + +# 获取统计信息,只显示包含 "cluster" 的统计项 +get-envoy-stats --filter="cluster.*" --format="json" +``` + +## 常见问题 + +### Q: 如何获取特定集群的详细信息? +A: 使用 `get-envoy-clusters` 工具,然后使用 `get-envoy-config-dump --resource="clusters"` 获取详细配置。 + +### Q: 如何监控配置同步状态? +A: 使用 `get-istiod-syncz` 查看整体同步状态,使用 `get-istiod-configz` 查看配置状态和错误信息。 + +### Q: 如何排查路由问题? +A: 使用 `get-envoy-config-dump` 获取详细路由信息。 + +### Q: 支持哪些输出格式? +A: 大部分工具支持 text 和 json 格式,统计信息还支持 prometheus 格式。 diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/README_en.md b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/README_en.md new file mode 100644 index 000000000..258eef704 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/README_en.md @@ -0,0 +1,197 @@ +# Higress Ops MCP Server + +Higress Ops MCP Server provides MCP tools for debugging and monitoring Istio and Envoy components, helping operations teams with troubleshooting and performance analysis. + +## Features + +### Istiod Debug Interfaces + +#### Configuration +- `get-istiod-configz`: Get Istiod configuration status and error information + +#### Service Discovery +- `get-istiod-endpointz`: Get all service endpoints discovered by Istiod +- `get-istiod-clusters`: Get all clusters discovered by Istiod +- `get-istiod-registryz`: Get Istiod service registry information + +#### Status Monitoring +- `get-istiod-syncz`: Get synchronization status between Istiod and Envoy proxies +- `get-istiod-metrics`: Get Prometheus metrics from Istiod + +#### System Information +- `get-istiod-version`: Get Istiod version information +- `get-istiod-debug-vars`: Get Istiod debug variables + +### Envoy Debug Interfaces + +#### Configuration +- `get-envoy-config-dump`: Get complete Envoy configuration snapshot with resource filtering and sensitive data masking +- `get-envoy-listeners`: Get all Envoy listener information +- `get-envoy-clusters`: Get all Envoy cluster information and health status + +#### Runtime +- `get-envoy-stats`: Get Envoy statistics with filtering and multiple output formats +- `get-envoy-runtime`: Get Envoy runtime configuration +- `get-envoy-memory`: Get Envoy memory usage + +#### Status Check +- `get-envoy-server-info`: Get Envoy server basic information +- `get-envoy-ready`: Check if Envoy is ready +- `get-envoy-hot-restart-version`: Get Envoy hot restart version + +#### Security +- `get-envoy-certs`: Get Envoy certificate information + +## Configuration Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `istiodURL` | string | Yes | URL address of Istiod debug interface | +| `envoyAdminURL` | string | Yes | URL address of Envoy Admin interface | +| `namespace` | string | Optional | Kubernetes namespace, defaults to `higress-system` | +| `description` | string | Optional | Server description, defaults to "Higress Ops MCP Server, which provides debug interfaces for Istio and Envoy components." | + +## Configuration Example + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: higress-config + namespace: higress-system +data: + higress: | + mcpServer: + sse_path_suffix: /sse # SSE connection path suffix + enable: true # Enable MCP Server + redis: + address: redis-stack-server.higress-system.svc.cluster.local:6379 # Redis service address + username: "" # Redis username (optional) + password: "" # Redis password (optional) + db: 0 # Redis database (optional) + match_list: # MCP Server session persistence routing rules + - match_rule_domain: "*" + match_rule_path: /higress-ops + match_rule_type: "prefix" + servers: + - name: higress-ops-mcp-server + path: /higress-ops + type: higress-ops + config: + istiodURL: http://higress-controller.higress-system.svc.cluster.local:15014 # istiod url + envoyAdminURL: http://127.0.0.1:15000 # envoy url, use 127.0.0.1 as it's in the same container as gateway + namespace: higress-system + description: "Higress Ops MCP Server for Istio and Envoy debugging" +``` + +## Authentication Configuration + +Higress Ops MCP Server uses custom HTTP headers for authentication. Clients need to include an Istiod authentication token in their request headers. + +### Token Generation + +Generate a long-lived Istiod authentication token with the following command: + +```bash +kubectl create token higress-gateway -n higress-system --audience istio-ca --duration 87600h +``` + +**Parameter Description:** +- `higress-gateway`: ServiceAccount name (must match the ServiceAccount used by Higress Gateway Pod) +- `-n higress-system`: Namespace (must match the `namespace` configuration parameter) +- `--audience istio-ca`: Token audience, must be `istio-ca` +- `--duration 87600h`: Token validity period (87600 hours ≈ 10 years) + +### Configuration Example + +Add the following to your MCP client configuration file (e.g., `~/.cursor/mcp.json` or Claude Desktop config): + +```json +{ + "mcpServers": { + "higress_ops_mcp": { + "url": "http://127.0.0.1:80/higress-ops/sse", + "headers": { + "X-Istiod-Token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im1IUlI0Z01ISUNBNVlZbDBHcVVBMjFhMklwQ3hFaHIxSlVlamtzTFRLOTQifQ..." + } + } + } +} +``` + +**Notes:** +- The `X-Istiod-Token` header is used to carry the Istiod authentication token +- The token value is generated by the above `kubectl create token` command +- If the token is not configured, accessing Istiod interfaces across pods will result in 401 authentication errors + +## Demo + +1. get envoy route information + +https://private-user-images.githubusercontent.com/153273766/507769115-d8e20b70-db1a-4a82-b89a-9eefeb3c8982.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MTE1LWQ4ZTIwYjcwLWRiMWEtNGE4Mi1iODlhLTllZWZlYjNjODk4Mi5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kYzg1Y2FiOTdiN2FiOTNkMmQ0OTc1NzEyZGMyMTlkNDQ4YjQ0NGYyOGUwNTlhYzYyYzA1ODJhOWM0M2Y3ZTQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Uz-HfM9tOzl7zrhGsPP1suunGg_K9ZbUN1BzAU5Oquo + +2. get istiod cluster information + +https://private-user-images.githubusercontent.com/153273766/507769013-9f598593-1251-4304-8e41-8bf4d1588897.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjE4Nzg4NjAsIm5iZiI6MTc2MTg3ODU2MCwicGF0aCI6Ii8xNTMyNzM3NjYvNTA3NzY5MDEzLTlmNTk4NTkzLTEyNTEtNDMwNC04ZTQxLThiZjRkMTU4ODg5Ny5tb3Y_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMDMxJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTAzMVQwMjQyNDBaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hZDQwYWE3MjM5OTU1NGNkMDcwNTgzNDMzZGI4NDRkYzdiNWRlNGJhODMwNjFlYjZiZjUzNzM3YWFhYzIyMjBjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.g19-rxOHSLIIszdGYAI7CmRzLTlrbA1fJ0hB6duuDBI + +## Use Cases + +### 1. Troubleshooting +- Use `get-istiod-syncz` to check configuration sync status +- Use `get-envoy-clusters` to check cluster health status +- Use `get-envoy-listeners` to check listener configuration + +### 2. Performance Analysis +- Use `get-istiod-metrics` to get Istiod performance metrics +- Use `get-envoy-stats` to get Envoy statistics +- Use `get-envoy-memory` to monitor memory usage + +### 3. Configuration Validation +- Use `get-istiod-config-dump` to validate Istiod configuration +- Use `get-envoy-config-dump` to validate Envoy configuration + +### 4. Security Audit +- Use `get-envoy-certs` to check certificate status +- Use `get-istiod-debug-vars` to view debug variables + +## Tool Parameter Examples + +### Istiod Tool Examples + +```bash +# Get specific proxy status +get-istiod-proxy-status --proxy="gateway-proxy.istio-system" + +# Get configuration dump +get-istiod-config-dump + +# Get sync status +get-istiod-syncz +``` + +### Envoy Tool Examples + +```bash +# Get config dump, filter listeners +get-envoy-config-dump --resource="listeners" + +# Get cluster info in JSON format +get-envoy-clusters --format="json" + +# Get stats containing "cluster", JSON format +get-envoy-stats --filter="cluster.*" --format="json" +``` + +## FAQ + +### Q: How to get detailed information for a specific cluster? +A: Use `get-envoy-clusters` tool, then use `get-envoy-config-dump --resource="clusters"` for detailed configuration. + +### Q: How to monitor configuration sync status? +A: Use `get-istiod-syncz` for overall sync status, use `get-istiod-proxy-status` for specific proxy status. + +### Q: How to troubleshoot routing issues? +A: Use `get-envoy-config-dump` for detailed route information. + +### Q: What output formats are supported? +A: Most tools support text and json formats, statistics also support prometheus format. diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/client.go b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/client.go new file mode 100644 index 000000000..c29854c0d --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/client.go @@ -0,0 +1,147 @@ +package higress_ops + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/envoyproxy/envoy/contrib/golang/common/go/api" +) + +// OpsClient handles Istio/Envoy debug API connections and operations +type OpsClient struct { + istiodURL string + envoyAdminURL string + namespace string + istiodToken string // Istiod authentication token (audience: istio-ca) + httpClient *http.Client +} + +// NewOpsClient creates a new ops client for Istio/Envoy debug interfaces +func NewOpsClient(istiodURL, envoyAdminURL, namespace string) *OpsClient { + if namespace == "" { + namespace = "higress-system" + } + + client := &OpsClient{ + istiodURL: istiodURL, + envoyAdminURL: envoyAdminURL, + namespace: namespace, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + return client +} + +// GetIstiodDebug calls Istiod debug endpoints +func (c *OpsClient) GetIstiodDebug(ctx context.Context, path string) ([]byte, error) { + return c.request(ctx, c.istiodURL, path) +} + +// GetEnvoyAdmin calls Envoy admin endpoints +func (c *OpsClient) GetEnvoyAdmin(ctx context.Context, path string) ([]byte, error) { + return c.request(ctx, c.envoyAdminURL, path) +} + +// GetIstiodDebugWithParams calls Istiod debug endpoints with query parameters +func (c *OpsClient) GetIstiodDebugWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error) { + return c.requestWithParams(ctx, c.istiodURL, path, params) +} + +// GetEnvoyAdminWithParams calls Envoy admin endpoints with query parameters +func (c *OpsClient) GetEnvoyAdminWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error) { + return c.requestWithParams(ctx, c.envoyAdminURL, path, params) +} + +func (c *OpsClient) request(ctx context.Context, baseURL, path string) ([]byte, error) { + return c.requestWithParams(ctx, baseURL, path, nil) +} + +func (c *OpsClient) requestWithParams(ctx context.Context, baseURL, path string, params map[string]string) ([]byte, error) { + fullURL := baseURL + path + + // Add query parameters if provided + if len(params) > 0 { + u, err := url.Parse(fullURL) + if err != nil { + return nil, fmt.Errorf("failed to parse URL %s: %w", fullURL, err) + } + + q := u.Query() + for key, value := range params { + q.Set(key, value) + } + u.RawQuery = q.Encode() + fullURL = u.String() + } + + api.LogDebugf("Ops API GET %s", fullURL) + + // Use the provided context, or create a new one if nil + if ctx == nil { + ctx = context.Background() + } + reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(reqCtx, "GET", fullURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + // Try to get Istiod token from context first (passthrough from MCP client) + // This is only applied for Istiod requests, not Envoy admin + if c.isBaseURL(baseURL, c.istiodURL) { + if istiodToken, ok := common.GetIstiodToken(ctx); ok && istiodToken != "" { + req.Header.Set("Authorization", "Bearer "+istiodToken) + api.LogInfof("Istiod API request: Using X-Istiod-Token from context for %s", path) + } else { + api.LogWarnf("Istiod API request: No authentication token available for %s. Request may fail with 401", path) + } + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body)) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + return respBody, nil +} + +// GetNamespace returns the configured namespace +func (c *OpsClient) GetNamespace() string { + return c.namespace +} + +// GetIstiodURL returns the Istiod URL +func (c *OpsClient) GetIstiodURL() string { + return c.istiodURL +} + +// GetEnvoyAdminURL returns the Envoy admin URL +func (c *OpsClient) GetEnvoyAdminURL() string { + return c.envoyAdminURL +} + +// isBaseURL checks if the baseURL matches the targetURL (for determining if token is needed) +func (c *OpsClient) isBaseURL(baseURL, targetURL string) bool { + return baseURL == targetURL +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/server.go b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/server.go new file mode 100644 index 000000000..28cf0cb38 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/server.go @@ -0,0 +1,81 @@ +package higress_ops + +import ( + "errors" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools" + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/envoyproxy/envoy/contrib/golang/common/go/api" +) + +const Version = "1.0.0" + +func init() { + common.GlobalRegistry.RegisterServer("higress-ops", &HigressOpsConfig{}) +} + +type HigressOpsConfig struct { + istiodURL string + envoyAdminURL string + namespace string + istiodToken string + description string +} + +func (c *HigressOpsConfig) ParseConfig(config map[string]interface{}) error { + istiodURL, ok := config["istiodURL"].(string) + if !ok { + return errors.New("missing istiodURL") + } + c.istiodURL = istiodURL + + envoyAdminURL, ok := config["envoyAdminURL"].(string) + if !ok { + return errors.New("missing envoyAdminURL") + } + c.envoyAdminURL = envoyAdminURL + + if namespace, ok := config["namespace"].(string); ok { + c.namespace = namespace + } else { + c.namespace = "higress-system" + } + + // Optional: Istiod authentication token (required for cross-pod access) + if istiodToken, ok := config["istiodToken"].(string); ok { + c.istiodToken = istiodToken + api.LogInfof("Istiod authentication token configured") + } else { + api.LogWarnf("No istiodToken configured. Cross-pod Istiod API requests may fail with 401 errors.") + } + + if desc, ok := config["description"].(string); ok { + c.description = desc + } else { + c.description = "Higress Ops MCP Server, which provides debug interfaces for Istio and Envoy components." + } + + api.LogInfof("Higress Ops MCP Server configuration parsed successfully. IstiodURL: %s, EnvoyAdminURL: %s, Namespace: %s, Description: %s", + c.istiodURL, c.envoyAdminURL, c.namespace, c.description) + + return nil +} + +func (c *HigressOpsConfig) NewServer(serverName string) (*common.MCPServer, error) { + mcpServer := common.NewMCPServer( + serverName, + Version, + common.WithInstructions("This is a Higress Ops MCP Server that provides debug interfaces for Istio and Envoy components"), + ) + + // Initialize Ops client with istiodToken + client := NewOpsClient(c.istiodURL, c.envoyAdminURL, c.namespace) + + // Register all tools with the client as an interface + tools.RegisterIstiodTools(mcpServer, tools.OpsClient(client)) + tools.RegisterEnvoyTools(mcpServer, tools.OpsClient(client)) + + api.LogInfof("Higress Ops MCP Server initialized: %s", serverName) + + return mcpServer, nil +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/client.go b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/client.go new file mode 100644 index 000000000..1054bc6b6 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/client.go @@ -0,0 +1,29 @@ +package tools + +import ( + "context" +) + +// OpsClient defines the interface for operations client +type OpsClient interface { + // GetIstiodDebug calls Istiod debug endpoints + GetIstiodDebug(ctx context.Context, path string) ([]byte, error) + + // GetEnvoyAdmin calls Envoy admin endpoints + GetEnvoyAdmin(ctx context.Context, path string) ([]byte, error) + + // GetIstiodDebugWithParams calls Istiod debug endpoints with query parameters + GetIstiodDebugWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error) + + // GetEnvoyAdminWithParams calls Envoy admin endpoints with query parameters + GetEnvoyAdminWithParams(ctx context.Context, path string, params map[string]string) ([]byte, error) + + // GetNamespace returns the configured namespace + GetNamespace() string + + // GetIstiodURL returns the Istiod URL + GetIstiodURL() string + + // GetEnvoyAdminURL returns the Envoy admin URL + GetEnvoyAdminURL() string +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/envoy.go b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/envoy.go new file mode 100644 index 000000000..abb0de706 --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/envoy.go @@ -0,0 +1,266 @@ +package tools + +import ( + "context" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/mark3labs/mcp-go/mcp" +) + +// RegisterEnvoyTools registers all Envoy admin tools +func RegisterEnvoyTools(mcpServer *common.MCPServer, client OpsClient) { + // Config dump tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-config-dump", + "Get complete Envoy configuration snapshot, including all listeners, clusters, routes, etc.", + CreateSimpleSchema(), + ), + handleEnvoyConfigDump(client), + ) + + // Clusters info tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-clusters", + "Get all Envoy cluster information and health status", + CreateParameterSchema( + map[string]interface{}{ + "format": map[string]interface{}{ + "type": "string", + "description": "Output format: json or text (default text)", + "enum": []string{"json", "text"}, + }, + }, + []string{}, + ), + ), + handleEnvoyClusters(client), + ) + + // Listeners info tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-listeners", + "Get all Envoy listener information", + CreateParameterSchema( + map[string]interface{}{ + "format": map[string]interface{}{ + "type": "string", + "description": "Output format: json or text (default text)", + "enum": []string{"json", "text"}, + }, + }, + []string{}, + ), + ), + handleEnvoyListeners(client), + ) + + // Stats tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-stats", + "Get Envoy statistics information", + CreateParameterSchema( + map[string]interface{}{ + "filter": map[string]interface{}{ + "type": "string", + "description": "Statistics filter, supports regular expressions (optional)", + }, + "format": map[string]interface{}{ + "type": "string", + "description": "Output format: json, prometheus or text (default text)", + "enum": []string{"json", "prometheus", "text"}, + }, + }, + []string{}, + ), + ), + handleEnvoyStats(client), + ) + + // Server info tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-server-info", + "Get Envoy server basic information", + CreateSimpleSchema(), + ), + handleEnvoyServerInfo(client), + ) + + // Ready check tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-ready", + "Check if Envoy is ready", + CreateSimpleSchema(), + ), + handleEnvoyReady(client), + ) + + // Hot restart epoch tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-hot-restart-version", + "Get Envoy hot restart version information", + CreateSimpleSchema(), + ), + handleEnvoyHotRestartVersion(client), + ) + + // Certs info tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-envoy-certs", + "Get Envoy certificate information", + CreateSimpleSchema(), + ), + handleEnvoyCerts(client), + ) +} + +func handleEnvoyConfigDump(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Get complete config dump without any filters + data, err := client.GetEnvoyAdmin(ctx, "/config_dump") + if err != nil { + return CreateErrorResult("failed to get Envoy config dump: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleEnvoyClusters(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + format := GetStringParam(arguments, "format", "text") + + path := "/clusters" + params := make(map[string]string) + + if format == "json" { + params["format"] = "json" + } + + var data []byte + var err error + + if len(params) > 0 { + data, err = client.GetEnvoyAdminWithParams(ctx, path, params) + } else { + data, err = client.GetEnvoyAdmin(ctx, path) + } + + if err != nil { + return CreateErrorResult("failed to get Envoy clusters: " + err.Error()) + } + return CreateToolResult(data, format) + } +} + +func handleEnvoyListeners(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + format := GetStringParam(arguments, "format", "text") + + path := "/listeners" + params := make(map[string]string) + + if format == "json" { + params["format"] = "json" + } + + var data []byte + var err error + + if len(params) > 0 { + data, err = client.GetEnvoyAdminWithParams(ctx, path, params) + } else { + data, err = client.GetEnvoyAdmin(ctx, path) + } + + if err != nil { + return CreateErrorResult("failed to get Envoy listeners: " + err.Error()) + } + return CreateToolResult(data, format) + } +} + +func handleEnvoyStats(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + arguments := request.Params.Arguments + filter := GetStringParam(arguments, "filter", "") + format := GetStringParam(arguments, "format", "text") + + var path string + switch format { + case "json": + path = "/stats?format=json" + case "prometheus": + path = "/stats/prometheus" + default: + path = "/stats" + } + + params := make(map[string]string) + if filter != "" { + params["filter"] = filter + } + + var data []byte + var err error + + if len(params) > 0 { + data, err = client.GetEnvoyAdminWithParams(ctx, path, params) + } else { + data, err = client.GetEnvoyAdmin(ctx, path) + } + + if err != nil { + return CreateErrorResult("failed to get Envoy stats: " + err.Error()) + } + return CreateToolResult(data, format) + } +} + +func handleEnvoyServerInfo(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetEnvoyAdmin(ctx, "/server_info") + if err != nil { + return CreateErrorResult("failed to get Envoy server info: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleEnvoyReady(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetEnvoyAdmin(ctx, "/ready") + if err != nil { + return CreateErrorResult("failed to get Envoy ready status: " + err.Error()) + } + return CreateToolResult(data, "text") + } +} + +func handleEnvoyHotRestartVersion(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetEnvoyAdmin(ctx, "/hot_restart_version") + if err != nil { + return CreateErrorResult("failed to get Envoy hot restart version: " + err.Error()) + } + return CreateToolResult(data, "text") + } +} + +func handleEnvoyCerts(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetEnvoyAdmin(ctx, "/certs") + if err != nil { + return CreateErrorResult("failed to get Envoy certs: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/istiod.go b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/istiod.go new file mode 100644 index 000000000..14617076e --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/istiod.go @@ -0,0 +1,131 @@ +package tools + +import ( + "context" + + "github.com/alibaba/higress/plugins/golang-filter/mcp-session/common" + "github.com/mark3labs/mcp-go/mcp" +) + +// RegisterIstiodTools registers all Istiod debug tools +func RegisterIstiodTools(mcpServer *common.MCPServer, client OpsClient) { + // Sync status tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-istiod-syncz", + "Get synchronization status information between Istiod and Envoy proxies", + CreateSimpleSchema(), + ), + handleIstiodSyncz(client), + ) + + // Endpoints debug tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-istiod-endpointz", + "Get all service endpoint information discovered by Istiod", + CreateSimpleSchema(), + ), + handleIstiodEndpointz(client), + ) + + // Config status tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-istiod-configz", + "Get Istiod configuration status and error information", + CreateSimpleSchema(), + ), + handleIstiodConfigz(client), + ) + + // Clusters debug tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-istiod-clusters", + "Get all cluster information discovered by Istiod", + CreateSimpleSchema(), + ), + handleIstiodClusters(client), + ) + + // Version info tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-istiod-version", + "Get Istiod version information", + CreateSimpleSchema(), + ), + handleIstiodVersion(client), + ) + + // Registry info tool + mcpServer.AddTool( + mcp.NewToolWithRawSchema( + "get-istiod-registryz", + "Get Istiod service registry information", + CreateSimpleSchema(), + ), + handleIstiodRegistryz(client), + ) +} + +func handleIstiodSyncz(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetIstiodDebug(ctx, "/debug/syncz") + if err != nil { + return CreateErrorResult("failed to get Istiod sync status: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleIstiodEndpointz(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetIstiodDebug(ctx, "/debug/endpointz") + if err != nil { + return CreateErrorResult("failed to get Istiod endpoints: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleIstiodConfigz(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetIstiodDebug(ctx, "/debug/configz") + if err != nil { + return CreateErrorResult("failed to get Istiod config status: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleIstiodClusters(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetIstiodDebug(ctx, "/debug/clusterz") + if err != nil { + return CreateErrorResult("failed to get Istiod clusters: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleIstiodVersion(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetIstiodDebug(ctx, "/version") + if err != nil { + return CreateErrorResult("failed to get Istiod version: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} + +func handleIstiodRegistryz(client OpsClient) common.ToolHandlerFunc { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data, err := client.GetIstiodDebug(ctx, "/debug/registryz") + if err != nil { + return CreateErrorResult("failed to get Istiod registry: " + err.Error()) + } + return CreateToolResult(data, "json") + } +} diff --git a/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/utils.go b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/utils.go new file mode 100644 index 000000000..46286a2dc --- /dev/null +++ b/plugins/golang-filter/mcp-server/servers/higress/higress-ops/tools/utils.go @@ -0,0 +1,103 @@ +package tools + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/mark3labs/mcp-go/mcp" +) + +// FormatJSONResponse formats a JSON response for better readability +func FormatJSONResponse(data []byte) (string, error) { + var jsonData interface{} + if err := json.Unmarshal(data, &jsonData); err != nil { + // If not valid JSON, return as-is + return string(data), nil + } + + formatted, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + return string(data), nil + } + + return string(formatted), nil +} + +// CreateToolResult creates a standardized tool result with formatted content +func CreateToolResult(data []byte, contentType string) (*mcp.CallToolResult, error) { + var content string + var err error + + if contentType == "json" || strings.Contains(string(data), "{") { + content, err = FormatJSONResponse(data) + if err != nil { + return nil, fmt.Errorf("failed to format JSON response: %w", err) + } + } else { + content = string(data) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: content, + }, + }, + }, nil +} + +// CreateErrorResult creates an error result for tool calls +func CreateErrorResult(message string) (*mcp.CallToolResult, error) { + return nil, fmt.Errorf(message) +} + +// GetStringParam safely extracts a string parameter from arguments +func GetStringParam(arguments map[string]interface{}, key string, defaultValue string) string { + if value, ok := arguments[key].(string); ok { + return value + } + return defaultValue +} + +// GetBoolParam safely extracts a boolean parameter from arguments +func GetBoolParam(arguments map[string]interface{}, key string, defaultValue bool) bool { + if value, ok := arguments[key].(bool); ok { + return value + } + return defaultValue +} + +// ValidateRequiredParams validates that required parameters are present +func ValidateRequiredParams(arguments map[string]interface{}, requiredParams []string) error { + for _, param := range requiredParams { + if _, ok := arguments[param]; !ok { + return fmt.Errorf("missing required parameter: %s", param) + } + } + return nil +} + +// CreateSimpleSchema creates a simple JSON schema for tools with no parameters +func CreateSimpleSchema() json.RawMessage { + return json.RawMessage(`{ + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + }`) +} + +// CreateParameterSchema creates a JSON schema for tools with specific parameters +func CreateParameterSchema(properties map[string]interface{}, required []string) json.RawMessage { + schema := map[string]interface{}{ + "type": "object", + "properties": properties, + "required": required, + "additionalProperties": false, + } + + schemaBytes, _ := json.Marshal(schema) + return json.RawMessage(schemaBytes) +} diff --git a/plugins/golang-filter/mcp-session/common/auth.go b/plugins/golang-filter/mcp-session/common/auth.go new file mode 100644 index 000000000..84d30f119 --- /dev/null +++ b/plugins/golang-filter/mcp-session/common/auth.go @@ -0,0 +1,53 @@ +package common + +import ( + "context" +) + +// contextKey is the type for context keys to avoid collisions +type authContextKey string + +const ( + // authHeaderKey stores the Authorization header value (for API authentication) + authHeaderKey authContextKey = "auth_header" + // istiodTokenKey stores the Istiod token value (for Istio debug API authentication) + istiodTokenKey authContextKey = "istiod_token" +) + +// WithAuthHeader adds the Authorization header to context +// This is typically used for authenticating with Higress Console API +func WithAuthHeader(ctx context.Context, authHeader string) context.Context { + if authHeader == "" { + return ctx + } + return context.WithValue(ctx, authHeaderKey, authHeader) +} + +// GetAuthHeader retrieves the Authorization header from context +// Returns the header value and true if found, empty string and false otherwise +func GetAuthHeader(ctx context.Context) (string, bool) { + if ctx == nil { + return "", false + } + authHeader, ok := ctx.Value(authHeaderKey).(string) + return authHeader, ok +} + +// WithIstiodToken adds the Istiod authentication token to context +// This is typically used for authenticating with Istiod debug endpoints +func WithIstiodToken(ctx context.Context, token string) context.Context { + if token == "" { + return ctx + } + return context.WithValue(ctx, istiodTokenKey, token) +} + +// GetIstiodToken retrieves the Istiod token from context +// Returns the token value and true if found, empty string and false otherwise +func GetIstiodToken(ctx context.Context) (string, bool) { + if ctx == nil { + return "", false + } + token, ok := ctx.Value(istiodTokenKey).(string) + return token, ok +} diff --git a/plugins/golang-filter/mcp-session/common/sse.go b/plugins/golang-filter/mcp-session/common/sse.go index f845b156a..5a6085c2b 100644 --- a/plugins/golang-filter/mcp-session/common/sse.go +++ b/plugins/golang-filter/mcp-session/common/sse.go @@ -201,6 +201,18 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j SessionID: sessionID, }) + // Extract Authorization header from HTTP request and add to context + // This is used for Higress Console API authentication + if authHeader := r.Header.Get("Authorization"); authHeader != "" { + ctx = WithAuthHeader(ctx, authHeader) + } + + // Extract X-Istiod-Token header from HTTP request and add to context + // This is used for Istiod debug API authentication + if istiodToken := r.Header.Get("X-Istiod-Token"); istiodToken != "" { + ctx = WithIstiodToken(ctx, istiodToken) + } + //TODO: check session id // _, ok := s.sessions.Load(sessionID) // if !ok {