feat: Add Higress API MCP server (#2517)

This commit is contained in:
Se7en
2025-07-03 10:27:28 +08:00
committed by GitHub
parent 36bcb595d6
commit 45eb76d4cc
16 changed files with 1543 additions and 3 deletions

View File

@@ -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` 将会自动构建并复制到镜像中。

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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