feat: add higress api mcp server (#2923)

Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
Co-authored-by: Se7en <chengzw258@163.com>
This commit is contained in:
Tsukilc
2025-10-31 15:46:14 +08:00
committed by GitHub
parent d745bc0d0b
commit 1602b6f94a
24 changed files with 3298 additions and 74 deletions

View File

@@ -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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}`)
}

View File

@@ -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
}`)
}

View File

@@ -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 typeif 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
}`)
}

View File

@@ -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)
}

View File

@@ -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
}`)
}

View File

@@ -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)
}

View File

@@ -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",

View File

@@ -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 + portsuch 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 + portsuch 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

View File

@@ -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 格式。

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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")
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 {