mirror of
https://github.com/alibaba/higress.git
synced 2026-03-19 17:57:31 +08:00
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:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}`)
|
||||
}
|
||||
@@ -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
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// McpServer represents an MCP server configuration
|
||||
type McpServer struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Services []McpUpstreamService `json:"services,omitempty"`
|
||||
Type string `json:"type"`
|
||||
ConsumerAuthInfo *ConsumerAuthInfo `json:"consumerAuthInfo,omitempty"`
|
||||
RawConfigurations string `json:"rawConfigurations,omitempty"`
|
||||
DSN string `json:"dsn,omitempty"`
|
||||
DBType string `json:"dbType,omitempty"`
|
||||
UpstreamPathPrefix string `json:"upstreamPathPrefix,omitempty"`
|
||||
McpServerName string `json:"mcpServerName,omitempty"`
|
||||
}
|
||||
|
||||
// McpUpstreamService represents a service in MCP server
|
||||
type McpUpstreamService struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
// ConsumerAuthInfo represents consumer authentication information
|
||||
type ConsumerAuthInfo struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Enable bool `json:"enable,omitempty"`
|
||||
AllowedConsumers []string `json:"allowedConsumers,omitempty"`
|
||||
}
|
||||
|
||||
// McpServerConsumers represents MCP server consumers configuration
|
||||
type McpServerConsumers struct {
|
||||
McpServerName string `json:"mcpServerName"`
|
||||
Consumers []string `json:"consumers"`
|
||||
}
|
||||
|
||||
// McpServerConsumerDetail represents detailed consumer information
|
||||
type McpServerConsumerDetail struct {
|
||||
McpServerName string `json:"mcpServerName"`
|
||||
ConsumerName string `json:"consumerName"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// SwaggerContent represents swagger content for conversion
|
||||
type SwaggerContent struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// McpServerResponse represents the API response for MCP server operations
|
||||
type McpServerResponse = higress.APIResponse[McpServer]
|
||||
|
||||
// McpServerConsumerDetailResponse represents the API response for MCP server consumer operations
|
||||
type McpServerConsumerDetailResponse = higress.APIResponse[[]McpServerConsumerDetail]
|
||||
|
||||
// RegisterMcpServerTools registers all MCP server management tools
|
||||
func RegisterMcpServerTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List MCP servers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-mcp-servers", "List all MCP servers", listMcpServersSchema()),
|
||||
handleListMcpServers(client),
|
||||
)
|
||||
|
||||
// Get specific MCP server
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-mcp-server", "Get detailed information about a specific MCP server", getMcpServerSchema()),
|
||||
handleGetMcpServer(client),
|
||||
)
|
||||
|
||||
// Add or update MCP server
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-or-update-mcp-server", "Add or update an MCP server instance", getAddOrUpdateMcpServerSchema()),
|
||||
handleAddOrUpdateMcpServer(client),
|
||||
)
|
||||
|
||||
// Delete MCP server
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-mcp-server", "Delete an MCP server", getMcpServerSchema()),
|
||||
handleDeleteMcpServer(client),
|
||||
)
|
||||
|
||||
// List MCP server consumers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("list-mcp-server-consumers", "List MCP server allowed consumers", listMcpServerConsumersSchema()),
|
||||
handleListMcpServerConsumers(client),
|
||||
)
|
||||
|
||||
// Add MCP server consumers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-mcp-server-consumers", "Add MCP server allowed consumers", getMcpServerConsumersSchema()),
|
||||
handleAddMcpServerConsumers(client),
|
||||
)
|
||||
|
||||
// Delete MCP server consumers
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-mcp-server-consumers", "Delete MCP server allowed consumers", getMcpServerConsumersSchema()),
|
||||
handleDeleteMcpServerConsumers(client),
|
||||
)
|
||||
|
||||
// Convert Swagger to MCP config
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("swagger-to-mcp-config", "Convert Swagger content to MCP configuration", getSwaggerToMcpConfigSchema()),
|
||||
handleSwaggerToMcpConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListMcpServers(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Build query parameters
|
||||
queryParams := ""
|
||||
if mcpServerName, ok := arguments["mcpServerName"].(string); ok && mcpServerName != "" {
|
||||
queryParams += "?mcpServerName=" + mcpServerName
|
||||
}
|
||||
if mcpType, ok := arguments["type"].(string); ok && mcpType != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?type=" + mcpType
|
||||
} else {
|
||||
queryParams += "&type=" + mcpType
|
||||
}
|
||||
}
|
||||
if pageNum, ok := arguments["pageNum"].(string); ok && pageNum != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageNum=" + pageNum
|
||||
} else {
|
||||
queryParams += "&pageNum=" + pageNum
|
||||
}
|
||||
}
|
||||
if pageSize, ok := arguments["pageSize"].(string); ok && pageSize != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageSize=" + pageSize
|
||||
} else {
|
||||
queryParams += "&pageSize=" + pageSize
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Get(ctx, "/v1/mcpServer"+queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list MCP servers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetMcpServer(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(ctx, fmt.Sprintf("/v1/mcpServer/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get MCP server '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddOrUpdateMcpServer(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["name"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'name' in configurations")
|
||||
}
|
||||
if _, ok := configurations["type"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'type' in configurations")
|
||||
}
|
||||
|
||||
// Validate service sources exist
|
||||
if services, ok := configurations["services"].([]interface{}); ok && len(services) > 0 {
|
||||
for _, svc := range services {
|
||||
if serviceMap, ok := svc.(map[string]interface{}); ok {
|
||||
if serviceName, ok := serviceMap["name"].(string); ok {
|
||||
// Extract service source name from "serviceName.serviceType" format
|
||||
var serviceSourceName string
|
||||
for i := len(serviceName) - 1; i >= 0; i-- {
|
||||
if serviceName[i] == '.' {
|
||||
serviceSourceName = serviceName[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if serviceSourceName == "" {
|
||||
return nil, fmt.Errorf("invalid service name format '%s', expected 'serviceName.serviceType'", serviceName)
|
||||
}
|
||||
|
||||
// Check if service source exists
|
||||
_, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", serviceSourceName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Please create the service source '%s' first and then create the mcpserver", serviceSourceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, "/v1/mcpServer", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add or update MCP server: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteMcpServer(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
name, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/mcpServer/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete MCP server '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleListMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Build query parameters
|
||||
queryParams := ""
|
||||
if mcpServerName, ok := arguments["mcpServerName"].(string); ok && mcpServerName != "" {
|
||||
queryParams += "?mcpServerName=" + mcpServerName
|
||||
}
|
||||
if consumerName, ok := arguments["consumerName"].(string); ok && consumerName != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?consumerName=" + consumerName
|
||||
} else {
|
||||
queryParams += "&consumerName=" + consumerName
|
||||
}
|
||||
}
|
||||
if pageNum, ok := arguments["pageNum"].(string); ok && pageNum != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageNum=" + pageNum
|
||||
} else {
|
||||
queryParams += "&pageNum=" + pageNum
|
||||
}
|
||||
}
|
||||
if pageSize, ok := arguments["pageSize"].(string); ok && pageSize != "" {
|
||||
if queryParams == "" {
|
||||
queryParams += "?pageSize=" + pageSize
|
||||
} else {
|
||||
queryParams += "&pageSize=" + pageSize
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Get(ctx, "/v1/mcpServer/consumers"+queryParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list MCP server consumers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["mcpServerName"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'mcpServerName' in configurations")
|
||||
}
|
||||
if _, ok := configurations["consumers"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'consumers' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Put(ctx, "/v1/mcpServer/consumers", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add MCP server consumers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteMcpServerConsumers(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["mcpServerName"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'mcpServerName' in configurations")
|
||||
}
|
||||
if _, ok := configurations["consumers"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'consumers' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.DeleteWithBody(ctx, "/v1/mcpServer/consumers", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete MCP server consumers: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleSwaggerToMcpConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
configurations, ok := arguments["configurations"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'configurations' argument")
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := configurations["content"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'content' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/mcpServer/swaggerToMcpConfig", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert swagger to MCP config: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Schema definitions
|
||||
|
||||
func listMcpServersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "McpServer name associated with route"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Mcp server type"
|
||||
},
|
||||
"pageNum": {
|
||||
"type": "string",
|
||||
"description": "Page number, starting from 1. If omitted, all items will be returned"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "string",
|
||||
"description": "Number of items per page. If omitted, all items will be returned"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getMcpServerSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the MCP server"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddOrUpdateMcpServerSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Mcp server name"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Mcp server description"
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Domains that the mcp server applies to"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "must be service name + service type, such as:daxt-mcp.static .which must be real exist service"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"version": {"type": "string", "description": "Service version"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "Mcp server upstream services"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["OPEN_API", "DATABASE", "DIRECT_ROUTE"],
|
||||
"description": "Mcp Server Type"
|
||||
},
|
||||
"consumerAuthInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"type": "string", "description": "Consumer auth type,if not enable, it value must be API_KEY "},
|
||||
"enable": {"type": "boolean", "description": "Whether consumer auth is enabled"},
|
||||
"allowedConsumers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Allowed consumer names"
|
||||
}
|
||||
},
|
||||
"description": "Mcp server consumer auth info"
|
||||
},
|
||||
"rawConfigurations": {
|
||||
"type": "string",
|
||||
"description": "Raw configurations in YAML format"
|
||||
},
|
||||
"dsn": {
|
||||
"type": "string",
|
||||
"description": "Data Source Name. For DB type server, it is required such as username:passwd@tcp(ip:port)/Database?charset=utf8mb4&parseTime=True&loc=Local .For other, it can be empty."
|
||||
},
|
||||
"dbType": {
|
||||
"type": "string",
|
||||
"enum": ["MYSQL", "POSTGRESQL", "SQLITE", "CLICKHOUSE"],
|
||||
"description": "Mcp Server DB Type,only if type is DATABASE, it is required"
|
||||
},
|
||||
"upstreamPathPrefix": {
|
||||
"type": "string",
|
||||
"description": "The upstream MCP server will redirect requests based on the path prefix"
|
||||
},
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "Mcp server name (usually same as 'name' field)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "dsn", "services"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func listMcpServerConsumersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "McpServer name associated with route"
|
||||
},
|
||||
"consumerName": {
|
||||
"type": "string",
|
||||
"description": "Consumer name for search"
|
||||
},
|
||||
"pageNum": {
|
||||
"type": "string",
|
||||
"description": "Page number, starting from 1. If omitted, all items will be returned"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "string",
|
||||
"description": "Number of items per page. If omitted, all items will be returned"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getMcpServerConsumersSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcpServerName": {
|
||||
"type": "string",
|
||||
"description": "Mcp server route name"
|
||||
},
|
||||
"consumers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Consumer names"
|
||||
}
|
||||
},
|
||||
"required": ["mcpServerName", "consumers"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getSwaggerToMcpConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Swagger content"
|
||||
}
|
||||
},
|
||||
"required": ["content"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}`)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
@@ -37,7 +38,7 @@ type ServiceSourceResponse = higress.APIResponse[ServiceSource]
|
||||
func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all service sources
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-service-sources", mcp.WithDescription("List all available service sources")),
|
||||
mcp.NewToolWithRawSchema("list-service-sources", "List all available service sources", listServiceSourcesSchema()),
|
||||
handleListServiceSources(client),
|
||||
)
|
||||
|
||||
@@ -68,7 +69,7 @@ func RegisterServiceTools(mcpServer *common.MCPServer, client *higress.HigressCl
|
||||
|
||||
func handleListServiceSources(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/service-sources")
|
||||
respBody, err := client.Get(ctx, "/v1/service-sources")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list service sources: %w", err)
|
||||
}
|
||||
@@ -92,7 +93,7 @@ func handleGetServiceSource(client *higress.HigressClient) common.ToolHandlerFun
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
respBody, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service source '%s': %w", name, err)
|
||||
}
|
||||
@@ -129,8 +130,28 @@ func handleAddServiceSource(client *higress.HigressClient) common.ToolHandlerFun
|
||||
if _, ok := configurations["port"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in configurations")
|
||||
}
|
||||
if t, ok := configurations["type"].(string); ok && t == "static" {
|
||||
if d, ok := configurations["domain"].(string); ok {
|
||||
host, port, err := net.SplitHostPort(d)
|
||||
if err != nil || host == "" || port == "" {
|
||||
return nil, fmt.Errorf("invalid 'domain' format for static type, expected ip:port, got '%s'", d)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid 'domain' field type, expected string")
|
||||
}
|
||||
}
|
||||
if t, ok := configurations["type"].(string); ok && t != "static" {
|
||||
if d, ok := configurations["domain"].(string); ok {
|
||||
host, _, err := net.SplitHostPort(d)
|
||||
if err == nil && host != "" {
|
||||
configurations["domain"] = host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/service-sources", configurations)
|
||||
// valid protocol,sni,properties,auth
|
||||
|
||||
respBody, err := client.Post(ctx, "/v1/service-sources", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add service source: %w", err)
|
||||
}
|
||||
@@ -160,7 +181,7 @@ func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
}
|
||||
|
||||
// Get current service source configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
currentBody, err := client.Get(ctx, fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current service source configuration: %w", err)
|
||||
}
|
||||
@@ -209,7 +230,7 @@ func handleUpdateServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
currentConfig.AuthN = newConfig.AuthN
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
respBody, err := client.Put(ctx, fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update service source '%s': %w", name, err)
|
||||
}
|
||||
@@ -233,7 +254,7 @@ func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
respBody, err := client.Delete(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
respBody, err := client.Delete(ctx, fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err)
|
||||
}
|
||||
@@ -249,6 +270,15 @@ func handleDeleteServiceSource(client *higress.HigressClient) common.ToolHandler
|
||||
}
|
||||
}
|
||||
|
||||
func listServiceSourcesSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -263,7 +293,6 @@ func getServiceSourceSchema() json.RawMessage {
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getAddServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -277,12 +306,12 @@ func getAddServiceSourceSchema() json.RawMessage {
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
"enum": ["static", "dns", "consul", "nacos3","nacos2","nacos1", "eureka", "zookeeper"],
|
||||
"description": "The type of service source. Supported types: 'static' (static IP), 'dns' (DNS resolution), 'consul' (Consul registry), 'nacos3' (Nacos 3.x), 'eureka' (Eureka registry), 'zookeeper' (ZooKeeper registry)"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address (required)"
|
||||
"description": "The domain name or IP address + port(such as: 127.0.0.1:8080) (required). For dns, use domain name (e.g., 'xxx.com')"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
@@ -292,12 +321,32 @@ func getAddServiceSourceSchema() json.RawMessage {
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
"enum": ["http", "https", ""],
|
||||
"description": "The protocol to use (optional, defaults to http, can be empty string for null)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections (optional)"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Type-specific configuration properties. Required fields by type: consul: 'consulDatacenter' (string), 'consulServiceTag' (string, format: 'key=value'); nacos3: 'nacosNamespaceId' (string, optional), 'nacosGroups' (array of strings), 'enableMCPServer' (boolean, optional), 'mcpServerBaseUrl' (string, required if enableMCPServer is true, e.g., '/mcp'), 'mcpServerExportDomains' (array of strings, required if enableMCPServer is true, e.g., ['xxx.com']); zookeeper: 'zkServicesPath' (array of strings); static/dns/eureka: no additional properties needed"
|
||||
},
|
||||
"authN": {
|
||||
"type": "object",
|
||||
"description": "Authentication configuration",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether authentication is enabled"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Authentication properties by type. consul: 'consulToken' (string); nacos3: 'nacosUsername' (string), 'nacosPassword' (string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "domain", "port"],
|
||||
@@ -309,7 +358,6 @@ func getAddServiceSourceSchema() json.RawMessage {
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
@@ -323,12 +371,12 @@ func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
"enum": ["static", "dns", "consul", "nacos3", "eureka", "zookeeper"],
|
||||
"description": "The type of service source. Supported types: 'static' (static IP), 'dns' (DNS resolution), 'consul' (Consul registry), 'nacos3' (Nacos 3.x), 'eureka' (Eureka registry), 'zookeeper' (ZooKeeper registry)"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address"
|
||||
"description": "The domain name or IP address + port(such as: 127.0.0.1:8080) (required). For dns, use domain name (e.g., 'xxx.com')"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
@@ -338,12 +386,32 @@ func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
"enum": ["http", "https", ""],
|
||||
"description": "The protocol to use (optional, can be empty string for null)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Type-specific configuration properties. Required fields by type: consul: 'consulDatacenter' (string), 'consulServiceTag' (string, format: 'key=value'); nacos3: 'nacosNamespaceId' (string, optional), 'nacosGroups' (array of strings), 'enableMCPServer' (boolean, optional), 'mcpServerBaseUrl' (string, required if enableMCPServer is true, e.g., '/mcp'), 'mcpServerExportDomains' (array of strings, required if enableMCPServer is true, e.g., ['xxx.com']); zookeeper: 'zkServicesPath' (array of strings); static/dns/eureka: no additional properties needed"
|
||||
},
|
||||
"authN": {
|
||||
"type": "object",
|
||||
"description": "Authentication configuration",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether authentication is enabled"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Authentication properties by type. consul: 'consulToken' (string); nacos: 'nacosUsername' (string), 'nacosPassword' (string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -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 格式。
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
53
plugins/golang-filter/mcp-session/common/auth.go
Normal file
53
plugins/golang-filter/mcp-session/common/auth.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user