mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
feat: Add Higress API MCP server (#2517)
This commit is contained in:
@@ -54,8 +54,15 @@ http_filters:
|
||||
|
||||
## 快速构建
|
||||
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
如果是 arm64 架构,请设置 `GOARCH=arm64`:
|
||||
|
||||
```bash
|
||||
make build GOARCH=arm64
|
||||
```
|
||||
你也可以直接在 Higress 项目的根目录下执行 `make build-gateway-local` 来构建 Higress Gateway 镜像,`golang-filter.so` 将会自动构建并复制到镜像中。
|
||||
|
||||
@@ -58,4 +58,12 @@ Use the following command to quickly build the golang filter plugin:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
```
|
||||
|
||||
If you are on an arm64 architecture, please set `GOARCH=arm64`:
|
||||
|
||||
```bash
|
||||
make build GOARCH=arm64
|
||||
```
|
||||
|
||||
Alternatively, you can build the Higress Gateway image directly by running `make build-gateway-local` in the root directory of the Higress project. The `golang-filter.so` file will be automatically built and included in the image.
|
||||
|
||||
@@ -5,6 +5,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"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
@@ -99,7 +100,7 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
|
||||
serverInstance, err := server.NewServer(serverName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
|
||||
return nil, fmt.Errorf("failed to initialize MCP Server: %w", err)
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, &SSEServerWrapper{
|
||||
|
||||
95
plugins/golang-filter/mcp-server/servers/higress/client.go
Normal file
95
plugins/golang-filter/mcp-server/servers/higress/client.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package higress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// HigressClient handles Higress Console API connections and operations
|
||||
type HigressClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewHigressClient(baseURL, username, password string) *HigressClient {
|
||||
client := &HigressClient{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
password: password,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
api.LogInfof("Higress Console client initialized: %s", baseURL)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *HigressClient) Get(path string) ([]byte, error) {
|
||||
return c.request("GET", path, nil)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("POST", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) {
|
||||
return c.request("PUT", path, data)
|
||||
}
|
||||
|
||||
func (c *HigressClient) Delete(path string) ([]byte, error) {
|
||||
return c.request("DELETE", path, nil)
|
||||
}
|
||||
func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) {
|
||||
url := c.baseURL + path
|
||||
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request data: %w", err)
|
||||
}
|
||||
body = bytes.NewBuffer(jsonData)
|
||||
api.LogDebugf("Higress API %s %s: %s", method, url, string(jsonData))
|
||||
} else {
|
||||
api.LogDebugf("Higress API %s %s", method, url)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server 提供了 MCP 工具来管理 Higress 路由、服务来源和插件等资源。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 路由管理
|
||||
- `list-routes`: 列出路由
|
||||
- `get-route`: 获取路由
|
||||
- `add-route`: 添加路由
|
||||
- `update-route`: 更新路由
|
||||
|
||||
### 服务来源管理
|
||||
- `list-service-sources`: 列出服务来源
|
||||
- `get-service-source`: 获取服务来源
|
||||
- `add-service-source`: 添加服务来源
|
||||
- `update-service-source`: 更新服务来源
|
||||
|
||||
### 插件管理
|
||||
- `get-plugin`: 获取插件配置
|
||||
- `delete-plugin`: 删除插件
|
||||
- `update-request-block-plguin`: 更新 request-block 插件配置
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `higressURL` | string | 必填 | Higress Console 的 URL 地址 |
|
||||
| `username` | string | 必填 | Higress Console 登录用户名 |
|
||||
| `password` | string | 必填 | Higress Console 登录密码 |
|
||||
| `description` | string | 可选 | 服务器描述信息 |
|
||||
|
||||
配置示例:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: higress
|
||||
meta.helm.sh/release-namespace: higress-system
|
||||
labels:
|
||||
app: higress-gateway
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
app.kubernetes.io/name: higress-gateway
|
||||
app.kubernetes.io/version: 2.1.4
|
||||
helm.sh/chart: higress-core-2.1.4
|
||||
higress: higress-system-higress-gateway
|
||||
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 会话保持路由规则(当匹配下面路径时,将被识别为一个 MCP 会话,通过 SSE 等机制进行会话保持)
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-api
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-api-mcp-server # MCP Server 名称
|
||||
path: /higress-api # 访问路径,需要与 match_list 中的配置匹配
|
||||
type: higress-api # 类型和 RegisterServer 一致
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# Higress API MCP Server
|
||||
|
||||
Higress API MCP Server provides MCP tools to manage Higress routes, service sources, plugins and other resources.
|
||||
|
||||
## Features
|
||||
|
||||
### Route Management
|
||||
- `list-routes`: List routes
|
||||
- `get-route`: Get route
|
||||
- `add-route`: Add route
|
||||
- `update-route`: Update 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
|
||||
|
||||
### Plugin Management
|
||||
- `get-plugin`: Get plugin configuration
|
||||
- `delete-plugin`: Delete plugin
|
||||
- `update-request-block-plugin`: Update request block configuration
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
| 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:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: higress
|
||||
meta.helm.sh/release-namespace: higress-system
|
||||
labels:
|
||||
app: higress-gateway
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
app.kubernetes.io/name: higress-gateway
|
||||
app.kubernetes.io/version: 2.1.4
|
||||
helm.sh/chart: higress-core-2.1.4
|
||||
higress: higress-system-higress-gateway
|
||||
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 (when matching the following paths, it will be recognized as an MCP session and maintained through SSE)
|
||||
- match_rule_domain: "*"
|
||||
match_rule_path: /higress-api
|
||||
match_rule_type: "prefix"
|
||||
servers:
|
||||
- name: higress-api-mcp-server # MCP Server name
|
||||
path: /higress-api # Access path, needs to match the configuration in match_list
|
||||
type: higress-api # Type defined in RegisterServer function
|
||||
config:
|
||||
higressURL: http://higress-console.higress-system.svc.cluster.local:8080
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
package higress_ops
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/higress/higress-api/tools/plugins"
|
||||
"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-api", &HigressConfig{})
|
||||
}
|
||||
|
||||
type HigressConfig struct {
|
||||
higressURL string
|
||||
username string
|
||||
password string
|
||||
description string
|
||||
}
|
||||
|
||||
func (c *HigressConfig) ParseConfig(config map[string]interface{}) error {
|
||||
higressURL, ok := config["higressURL"].(string)
|
||||
if !ok {
|
||||
return errors.New("missing higressURL")
|
||||
}
|
||||
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)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HigressConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
common.WithInstructions("This is a Higress API MCP Server"),
|
||||
)
|
||||
|
||||
// Initialize Higress API client
|
||||
client := higress.NewHigressClient(c.higressURL, c.username, c.password)
|
||||
|
||||
// Register all tools
|
||||
tools.RegisterRouteTools(mcpServer, client)
|
||||
tools.RegisterServiceTools(mcpServer, client)
|
||||
plugins.RegisterCommonPluginTools(mcpServer, client)
|
||||
plugins.RegisterRequestBlockPluginTools(mcpServer, client)
|
||||
|
||||
api.LogInfof("Higress MCP Server initialized: %s", serverName)
|
||||
|
||||
return mcpServer, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// RegisterCommonPluginTools registers all common plugin management tools
|
||||
func RegisterCommonPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Get plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-plugin", "Get configuration for a specific plugin", getPluginConfigSchema()),
|
||||
handleGetPluginConfig(client),
|
||||
)
|
||||
|
||||
// Delete plugin configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-plugin", "Delete configuration for a specific plugin", getPluginConfigSchema()),
|
||||
handleDeletePluginConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleGetPluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
pluginName, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Parse resource_name (required for non-global scopes)
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Get(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeletePluginConfig(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
|
||||
// Parse required parameters
|
||||
pluginName, ok := arguments["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing or invalid 'name' argument")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Parse resource_name (required for non-global scopes)
|
||||
var resourceName string
|
||||
if scope != ScopeGlobal {
|
||||
resourceName, ok = arguments["resource_name"].(string)
|
||||
if !ok || resourceName == "" {
|
||||
return nil, fmt.Errorf("'resource_name' is required for scope '%s'", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API path and make request
|
||||
path := BuildPluginPath(pluginName, scope, resourceName)
|
||||
respBody, err := client.Delete(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete plugin config for '%s' at scope '%s': %w", pluginName, scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginConfigSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the plugin"
|
||||
},
|
||||
"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)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "scope"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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 RequestBlockPluginName = "request-block"
|
||||
|
||||
// RequestBlockConfig represents the configuration for request-block plugin
|
||||
type RequestBlockConfig struct {
|
||||
BlockBodies []string `json:"block_bodies,omitempty"`
|
||||
BlockHeaders []string `json:"block_headers,omitempty"`
|
||||
BlockUrls []string `json:"block_urls,omitempty"`
|
||||
BlockedCode int `json:"blocked_code,omitempty"`
|
||||
CaseSensitive bool `json:"case_sensitive,omitempty"`
|
||||
}
|
||||
|
||||
// RequestBlockInstance represents a request-block plugin instance
|
||||
type RequestBlockInstance = PluginInstance[RequestBlockConfig]
|
||||
|
||||
// RequestBlockResponse represents the API response for request-block plugin
|
||||
type RequestBlockResponse = higress.APIResponse[RequestBlockInstance]
|
||||
|
||||
// RegisterRequestBlockPluginTools registers all request block plugin management tools
|
||||
func RegisterRequestBlockPluginTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// Update request block configuration
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema(fmt.Sprintf("update-%s-plugin", RequestBlockPluginName), "Update request block plugin configuration", getAddOrUpdateRequestBlockConfigSchema()),
|
||||
handleAddOrUpdateRequestBlockConfig(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleAddOrUpdateRequestBlockConfig(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(RequestBlockPluginName, scope, resourceName)
|
||||
|
||||
// Get current request block configuration to merge with updates
|
||||
currentBody, err := client.Get(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current request block configuration: %w", err)
|
||||
}
|
||||
|
||||
var response RequestBlockResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current request block response: %w", err)
|
||||
}
|
||||
|
||||
currentConfig := response.Data
|
||||
currentConfig.Enabled = enabled
|
||||
currentConfig.Scope = scope
|
||||
|
||||
// Convert the input configurations to RequestBlockConfig and merge
|
||||
configBytes, err := json.Marshal(configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal configurations: %w", err)
|
||||
}
|
||||
|
||||
var newConfig RequestBlockConfig
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse request block configurations: %w", err)
|
||||
}
|
||||
|
||||
// Update configurations (overwrite with new values where provided)
|
||||
if newConfig.BlockBodies != nil {
|
||||
currentConfig.Configurations.BlockBodies = newConfig.BlockBodies
|
||||
}
|
||||
if newConfig.BlockHeaders != nil {
|
||||
currentConfig.Configurations.BlockHeaders = newConfig.BlockHeaders
|
||||
}
|
||||
if newConfig.BlockUrls != nil {
|
||||
currentConfig.Configurations.BlockUrls = newConfig.BlockUrls
|
||||
}
|
||||
if newConfig.BlockedCode != 0 {
|
||||
currentConfig.Configurations.BlockedCode = newConfig.BlockedCode
|
||||
}
|
||||
currentConfig.Configurations.CaseSensitive = newConfig.CaseSensitive
|
||||
|
||||
respBody, err := client.Put(path, currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update request block config at scope '%s': %w", scope, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAddOrUpdateRequestBlockConfigSchema() 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": {
|
||||
"block_bodies": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request body content"
|
||||
},
|
||||
"block_headers": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request headers"
|
||||
},
|
||||
"block_urls": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of patterns to match against request URLs"
|
||||
},
|
||||
"blocked_code": {
|
||||
"type": "integer",
|
||||
"minimum": 100,
|
||||
"maximum": 599,
|
||||
"description": "HTTP status code to return when a block is matched"
|
||||
},
|
||||
"case_sensitive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the block matching is case sensitive"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["scope", "enabled", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package plugins
|
||||
|
||||
// PluginTargets represents the targets for different scopes
|
||||
type PluginTargets struct {
|
||||
Domain string `json:"DOMAIN,omitempty"`
|
||||
Service string `json:"SERVICE,omitempty"`
|
||||
Route string `json:"ROUTE,omitempty"`
|
||||
}
|
||||
|
||||
// PluginInstance represents a plugin instance configuration
|
||||
type PluginInstance[T any] struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Targets PluginTargets `json:"targets,omitempty"`
|
||||
PluginName string `json:"pluginName,omitempty"`
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
Internal bool `json:"internal,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RawConfigurations string `json:"rawConfigurations,omitempty"`
|
||||
Configurations T `json:"configurations,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package plugins
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
ScopeGlobal = "GLOBAL"
|
||||
ScopeDomain = "DOMAIN"
|
||||
ScopeService = "SERVICE"
|
||||
ScopeRoute = "ROUTE"
|
||||
)
|
||||
|
||||
// ValidScopes contains all valid plugin scopes
|
||||
var ValidScopes = []string{ScopeGlobal, ScopeDomain, ScopeService, ScopeRoute}
|
||||
|
||||
// IsValidScope checks if the given scope is valid
|
||||
func IsValidScope(scope string) bool {
|
||||
for _, validScope := range ValidScopes {
|
||||
if scope == validScope {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildPluginPath builds the API path for plugin operations based on scope and resource
|
||||
func BuildPluginPath(pluginName, scope, resourceName string) string {
|
||||
switch scope {
|
||||
case ScopeGlobal:
|
||||
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
|
||||
case ScopeDomain:
|
||||
return fmt.Sprintf("/v1/domains/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
case ScopeService:
|
||||
return fmt.Sprintf("/v1/services/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
case ScopeRoute:
|
||||
return fmt.Sprintf("/v1/routes/%s/plugin-instances/%s", resourceName, pluginName)
|
||||
default:
|
||||
return fmt.Sprintf("/v1/global/plugin-instances/%s", pluginName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// Route represents a route configuration
|
||||
type Route struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
Path *RoutePath `json:"path,omitempty"`
|
||||
Methods []string `json:"methods,omitempty"`
|
||||
Headers []RouteMatch `json:"headers,omitempty"`
|
||||
URLParams []RouteMatch `json:"urlParams,omitempty"`
|
||||
Services []RouteService `json:"services,omitempty"`
|
||||
AuthConfig *RouteAuthConfig `json:"authConfig,omitempty"`
|
||||
CustomConfigs map[string]interface{} `json:"customConfigs,omitempty"`
|
||||
}
|
||||
|
||||
// RoutePath represents path matching configuration
|
||||
type RoutePath struct {
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
// RouteMatch represents header or URL parameter matching configuration
|
||||
type RouteMatch struct {
|
||||
Key string `json:"key"`
|
||||
MatchType string `json:"matchType"`
|
||||
MatchValue string `json:"matchValue"`
|
||||
}
|
||||
|
||||
// RouteService represents a service in the route
|
||||
type RouteService struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
// RouteAuthConfig represents authentication configuration for a route
|
||||
type RouteAuthConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedConsumers []string `json:"allowedConsumers,omitempty"`
|
||||
}
|
||||
|
||||
// RouteResponse represents the API response for route operations
|
||||
type RouteResponse = higress.APIResponse[Route]
|
||||
|
||||
// RegisterRouteTools registers all route management tools
|
||||
func RegisterRouteTools(mcpServer *common.MCPServer, client *higress.HigressClient) {
|
||||
// List all routes
|
||||
mcpServer.AddTool(
|
||||
mcp.NewTool("list-routes", mcp.WithDescription("List all available routes")),
|
||||
handleListRoutes(client),
|
||||
)
|
||||
|
||||
// Get specific route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-route", "Get detailed information about a specific route", getRouteSchema()),
|
||||
handleGetRoute(client),
|
||||
)
|
||||
|
||||
// Add new route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-route", "Add a new route", getAddRouteSchema()),
|
||||
handleAddRoute(client),
|
||||
)
|
||||
|
||||
// Update existing route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-route", "Update an existing route", getUpdateRouteSchema()),
|
||||
handleUpdateRoute(client),
|
||||
)
|
||||
|
||||
// Delete existing route
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-route", "Delete an existing route", getRouteSchema()),
|
||||
handleDeleteRoute(client),
|
||||
)
|
||||
}
|
||||
|
||||
func handleListRoutes(client *higress.HigressClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
respBody, err := client.Get("/v1/routes")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list routes: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRoute(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(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddRoute(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["path"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'path' in configurations")
|
||||
}
|
||||
if _, ok := configurations["services"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'services' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/routes", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateRoute(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 route configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current route configuration: %w", err)
|
||||
}
|
||||
|
||||
var response RouteResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current 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 Route
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse route configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Domains != nil {
|
||||
currentConfig.Domains = newConfig.Domains
|
||||
}
|
||||
if newConfig.Path != nil {
|
||||
currentConfig.Path = newConfig.Path
|
||||
}
|
||||
if newConfig.Methods != nil {
|
||||
currentConfig.Methods = newConfig.Methods
|
||||
}
|
||||
if newConfig.Headers != nil {
|
||||
currentConfig.Headers = newConfig.Headers
|
||||
}
|
||||
if newConfig.URLParams != nil {
|
||||
currentConfig.URLParams = newConfig.URLParams
|
||||
}
|
||||
if newConfig.Services != nil {
|
||||
currentConfig.Services = newConfig.Services
|
||||
}
|
||||
if newConfig.AuthConfig != nil {
|
||||
currentConfig.AuthConfig = newConfig.AuthConfig
|
||||
}
|
||||
if newConfig.CustomConfigs != nil {
|
||||
currentConfig.CustomConfigs = newConfig.CustomConfigs
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/routes/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteRoute(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(fmt.Sprintf("/v1/routes/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete route '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getAddRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
},
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed"
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "List of path match conditions"
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
|
||||
"description": "List of HTTP methods"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Header key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of header match conditions"
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Parameter key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of URL parameter match conditions"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Service name"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "List of services for this route"
|
||||
},
|
||||
"customConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Dictionary of custom configurations"
|
||||
}
|
||||
},
|
||||
"required": ["name", "path", "services"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
func getUpdateRouteSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the route"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of domain names, but only one domain is allowed",
|
||||
"maxItems": 1
|
||||
},
|
||||
"path": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of path"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"}
|
||||
},
|
||||
"required": ["matchType", "matchValue"],
|
||||
"description": "The path configuration"
|
||||
},
|
||||
"methods": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"]},
|
||||
"description": "List of HTTP methods"
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of header"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Header key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of header match conditions"
|
||||
},
|
||||
"urlParams": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchType": {"type": "string", "enum": ["PRE", "EQUAL", "REGULAR"], "description": "Match type of URL parameter"},
|
||||
"matchValue": {"type": "string", "description": "Value to match"},
|
||||
"caseSensitive": {"type": "boolean", "description": "Whether matching is case sensitive"},
|
||||
"key": {"type": "string", "description": "Parameter key name"}
|
||||
},
|
||||
"required": ["matchType", "matchValue", "key"]
|
||||
},
|
||||
"description": "List of URL parameter match conditions"
|
||||
},
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Service name"},
|
||||
"port": {"type": "integer", "description": "Service port"},
|
||||
"weight": {"type": "integer", "description": "Service weight"}
|
||||
},
|
||||
"required": ["name", "port", "weight"]
|
||||
},
|
||||
"description": "List of services for this route"
|
||||
},
|
||||
"customConfigs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "Dictionary of custom configurations"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// ServiceSource represents a service source configuration
|
||||
type ServiceSource struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Domain string `json:"domain"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
SNI *string `json:"sni,omitempty"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
AuthN *ServiceSourceAuthN `json:"authN,omitempty"`
|
||||
Valid bool `json:"valid,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSourceAuthN represents authentication configuration for service source
|
||||
type ServiceSourceAuthN struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSourceResponse represents the API response for service source operations
|
||||
type ServiceSourceResponse = higress.APIResponse[ServiceSource]
|
||||
|
||||
// RegisterServiceTools registers all service source management tools
|
||||
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")),
|
||||
handleListServiceSources(client),
|
||||
)
|
||||
|
||||
// Get specific service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("get-service-source", "Get detailed information about a specific service source", getServiceSourceSchema()),
|
||||
handleGetServiceSource(client),
|
||||
)
|
||||
|
||||
// Add new service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("add-service-source", "Add a new service source", getAddServiceSourceSchema()),
|
||||
handleAddServiceSource(client),
|
||||
)
|
||||
|
||||
// Update existing service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("update-service-source", "Update an existing service source", getUpdateServiceSourceSchema()),
|
||||
handleUpdateServiceSource(client),
|
||||
)
|
||||
|
||||
// Delete existing service source
|
||||
mcpServer.AddTool(
|
||||
mcp.NewToolWithRawSchema("delete-service-source", "Delete an existing service source", getServiceSourceSchema()),
|
||||
handleDeleteServiceSource(client),
|
||||
)
|
||||
}
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list service sources: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetServiceSource(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(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddServiceSource(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["domain"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'domain' in configurations")
|
||||
}
|
||||
if _, ok := configurations["port"]; !ok {
|
||||
return nil, fmt.Errorf("missing required field 'port' in configurations")
|
||||
}
|
||||
|
||||
respBody, err := client.Post("/v1/service-sources", configurations)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add service source: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleUpdateServiceSource(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 service source configuration to merge with updates
|
||||
currentBody, err := client.Get(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current service source configuration: %w", err)
|
||||
}
|
||||
|
||||
var response ServiceSourceResponse
|
||||
if err := json.Unmarshal(currentBody, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current service source 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 ServiceSource
|
||||
if err := json.Unmarshal(configBytes, &newConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse service source configurations: %w", err)
|
||||
}
|
||||
|
||||
// Merge configurations (overwrite with new values where provided)
|
||||
if newConfig.Name != "" {
|
||||
currentConfig.Name = newConfig.Name
|
||||
}
|
||||
if newConfig.Type != "" {
|
||||
currentConfig.Type = newConfig.Type
|
||||
}
|
||||
if newConfig.Domain != "" {
|
||||
currentConfig.Domain = newConfig.Domain
|
||||
}
|
||||
if newConfig.Port != 0 {
|
||||
currentConfig.Port = newConfig.Port
|
||||
}
|
||||
if newConfig.Protocol != "" {
|
||||
currentConfig.Protocol = newConfig.Protocol
|
||||
}
|
||||
if newConfig.SNI != nil {
|
||||
currentConfig.SNI = newConfig.SNI
|
||||
}
|
||||
if newConfig.Properties != nil {
|
||||
currentConfig.Properties = newConfig.Properties
|
||||
}
|
||||
if newConfig.AuthN != nil {
|
||||
currentConfig.AuthN = newConfig.AuthN
|
||||
}
|
||||
|
||||
respBody, err := client.Put(fmt.Sprintf("/v1/service-sources/%s", name), currentConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleDeleteServiceSource(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(fmt.Sprintf("/v1/service-sources/%s", name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete service source '%s': %w", name, err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: string(respBody),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source to retrieve"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getAddServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address (required)"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"description": "The port number (required)"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections (optional)"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type", "domain", "port"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TODO: extend other types of service sources, e.g., nacos, zookeeper, euraka.
|
||||
func getUpdateServiceSourceSchema() json.RawMessage {
|
||||
return json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service source to update"
|
||||
},
|
||||
"configurations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["static", "dns"],
|
||||
"description": "The type of service source: 'static' for static IPs, 'dns' for DNS resolution"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The domain name or IP address"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"description": "The port number"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": ["http", "https"],
|
||||
"description": "The protocol to use (optional, defaults to http)"
|
||||
},
|
||||
"sni": {
|
||||
"type": "string",
|
||||
"description": "Server Name Indication for HTTPS connections"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "configurations"],
|
||||
"additionalProperties": false
|
||||
}`)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package higress
|
||||
|
||||
// APIResponse represents the standard Higress API response format
|
||||
type APIResponse[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data T `json:"data,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user