fix: Refactor MCP Server into MCP Session and MCP Server (#2120)

This commit is contained in:
Jingze
2025-04-28 13:42:14 +08:00
committed by GitHub
parent e381806ba0
commit c382635e7f
29 changed files with 1025 additions and 517 deletions

View File

@@ -61,6 +61,8 @@ type SSEServer struct {
Type string `json:"type,omitempty"`
// Additional Config parameters for the real MCP server implementation
Config map[string]interface{} `json:"config,omitempty"`
// The domain list of the SSE server
DomainList []string `json:"domain_list,omitempty"`
}
// MatchRule defines a rule for matching requests
@@ -179,9 +181,10 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
for i, server := range mcp.Servers {
newServer := &SSEServer{
Name: server.Name,
Path: server.Path,
Type: server.Type,
Name: server.Name,
Path: server.Path,
Type: server.Type,
DomainList: server.DomainList,
}
if server.Config != nil {
newServer.Config = make(map[string]interface{})
@@ -294,73 +297,88 @@ func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error)
return configs, nil
}
mcpStruct := m.constructMcpServerStruct(mcpServer)
if mcpStruct == "" {
return configs, nil
}
config := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: higressMcpServerEnvoyFilterName,
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "envoy.filters.network.http_connection_manager",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
Name: "envoy.filters.http.cors",
// mcp-session envoy filter
mcpSessionStruct := m.constructMcpSessionStruct(mcpServer)
if mcpSessionStruct != "" {
sessionConfig := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: higressMcpServerEnvoyFilterName,
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "envoy.filters.network.http_connection_manager",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
Name: "envoy.filters.http.cors",
},
},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
Value: util.BuildPatchStruct(mcpStruct),
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
Value: util.BuildPatchStruct(mcpSessionStruct),
},
},
},
},
},
}
configs = append(configs, sessionConfig)
}
// mcp-server envoy filter
mcpServerStruct := m.constructMcpServerStruct(mcpServer)
if mcpServerStruct != "" {
serverConfig := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: higressMcpServerEnvoyFilterName + "-server",
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "envoy.filters.network.http_connection_manager",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
Name: "envoy.filters.http.router",
},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
Value: util.BuildPatchStruct(mcpServerStruct),
},
},
},
},
}
configs = append(configs, serverConfig)
}
configs = append(configs, config)
return configs, nil
}
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
// Build servers configuration
servers := "[]"
if len(mcp.Servers) > 0 {
serverConfigs := make([]string, len(mcp.Servers))
for i, server := range mcp.Servers {
serverConfig := fmt.Sprintf(`{
"name": "%s",
"path": "%s",
"type": "%s"`,
server.Name, server.Path, server.Type)
if len(server.Config) > 0 {
config, _ := json.Marshal(server.Config)
serverConfig += fmt.Sprintf(`,
"config": %s`, string(config))
}
serverConfig += "}"
serverConfigs[i] = serverConfig
}
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
}
func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
// Build match_list configuration
matchList := "[]"
if len(mcp.MatchList) > 0 {
@@ -375,7 +393,7 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
}
// 构建 Redis 配置
// Build redis configuration
redisConfig := "null"
if mcp.Redis != nil {
redisConfig = fmt.Sprintf(`{
@@ -386,7 +404,7 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
}
// 构建限流配置
// Build rate limit configuration
rateLimitConfig := "null"
if mcp.Ratelimit != nil {
whiteList := "[]"
@@ -417,7 +435,6 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
"rate_limit": %s,
"sse_path_suffix": "%s",
"match_list": %s,
"servers": %s,
"enable_user_level_server": %t
}
}
@@ -428,6 +445,53 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
rateLimitConfig,
mcp.SsePathSuffix,
matchList,
servers,
mcp.EnableUserLevelServer)
}
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
// Build servers configuration
servers := "[]"
if len(mcp.Servers) > 0 {
serverConfigs := make([]string, len(mcp.Servers))
for i, server := range mcp.Servers {
serverConfig := fmt.Sprintf(`{
"name": "%s",
"path": "%s",
"type": "%s"`,
server.Name, server.Path, server.Type)
if len(server.DomainList) > 0 {
domainList := fmt.Sprintf(`["%s"]`, strings.Join(server.DomainList, `","`))
serverConfig += fmt.Sprintf(`,
"domain_list": %s`, domainList)
}
if len(server.Config) > 0 {
config, _ := json.Marshal(server.Config)
serverConfig += fmt.Sprintf(`,
"config": %s`, string(config))
}
serverConfig += "}"
serverConfigs[i] = serverConfig
}
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
}
// Build complete configuration structure
return fmt.Sprintf(`{
"name": "envoy.filters.http.golang",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"value": {
"library_id": "mcp-server",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-server",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"servers": %s
}
}
}
}
}`, servers)
}

View File

@@ -15,6 +15,7 @@
package configmap
import (
"encoding/json"
"errors"
"testing"
@@ -422,3 +423,311 @@ func TestMcpServerController_AddOrUpdateHigressConfig(t *testing.T) {
})
}
}
func TestMcpServerController_ValidHigressConfig(t *testing.T) {
tests := []struct {
name string
higressConfig *HigressConfig
wantErr error
}{
{
name: "nil config",
higressConfig: nil,
wantErr: nil,
},
{
name: "nil mcp server",
higressConfig: &HigressConfig{
McpServer: nil,
},
wantErr: nil,
},
{
name: "valid config",
higressConfig: &HigressConfig{
McpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
},
wantErr: nil,
},
{
name: "invalid config - user level server without redis",
higressConfig: &HigressConfig{
McpServer: &McpServer{
Enable: true,
EnableUserLevelServer: true,
Redis: nil,
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
},
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
err := m.ValidHigressConfig(tt.higressConfig)
assert.Equal(t, tt.wantErr, err)
})
}
}
func TestMcpServerController_ConstructEnvoyFilters(t *testing.T) {
tests := []struct {
name string
mcpServer *McpServer
wantConfigs int
wantErr error
}{
{
name: "nil mcp server",
mcpServer: nil,
wantConfigs: 0,
wantErr: nil,
},
{
name: "disabled mcp server",
mcpServer: &McpServer{
Enable: false,
},
wantConfigs: 0,
wantErr: nil,
},
{
name: "valid mcp server with redis",
mcpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantConfigs: 2, // Both session and server filters
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
m.mcpServer.Store(tt.mcpServer)
configs, err := m.ConstructEnvoyFilters()
assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.wantConfigs, len(configs))
})
}
}
func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
tests := []struct {
name string
mcp *McpServer
wantJSON string
}{
{
name: "minimal config",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantJSON: `{
"name": "envoy.filters.http.golang",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"value": {
"library_id": "mcp-session",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-session",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"redis": {
"address": "localhost:6379",
"username": "",
"password": "",
"db": 0
},
"rate_limit": null,
"sse_path_suffix": "",
"match_list": [],
"enable_user_level_server": false
}
}
}
}
}`,
},
{
name: "full config",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "user",
Password: "pass",
DB: 1,
},
SsePathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/test",
MatchRuleType: "exact",
},
},
EnableUserLevelServer: true,
Ratelimit: &MCPRatelimitConfig{
Limit: 100,
Window: 3600,
WhiteList: []string{"user1", "user2"},
},
},
wantJSON: `{
"name": "envoy.filters.http.golang",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"value": {
"library_id": "mcp-session",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-session",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"redis": {
"address": "localhost:6379",
"username": "user",
"password": "pass",
"db": 1
},
"rate_limit": {
"limit": 100,
"window": 3600,
"white_list": ["user1","user2"]
},
"sse_path_suffix": "/sse",
"match_list": [{
"match_rule_domain": "*",
"match_rule_path": "/test",
"match_rule_type": "exact"
}],
"enable_user_level_server": true
}
}
}
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
got := m.constructMcpSessionStruct(tt.mcp)
// Normalize JSON strings for comparison
var gotJSON, wantJSON interface{}
json.Unmarshal([]byte(got), &gotJSON)
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
assert.Equal(t, wantJSON, gotJSON)
})
}
}
func TestMcpServerController_constructMcpServerStruct(t *testing.T) {
tests := []struct {
name string
mcp *McpServer
wantJSON string
}{
{
name: "no servers",
mcp: &McpServer{
Servers: []*SSEServer{},
},
wantJSON: `{
"name": "envoy.filters.http.golang",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"value": {
"library_id": "mcp-server",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-server",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"servers": []
}
}
}
}
}`,
},
{
name: "with servers",
mcp: &McpServer{
Servers: []*SSEServer{
{
Name: "test-server",
Path: "/test",
Type: "test",
Config: map[string]interface{}{
"key": "value",
},
DomainList: []string{"example.com"},
},
},
},
wantJSON: `{
"name": "envoy.filters.http.golang",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"value": {
"library_id": "mcp-server",
"library_path": "/var/lib/istio/envoy/golang-filter.so",
"plugin_name": "mcp-server",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"servers": [{
"name": "test-server",
"path": "/test",
"type": "test",
"domain_list": ["example.com"],
"config": {"key":"value"}
}]
}
}
}
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("test-namespace")
got := m.constructMcpServerStruct(tt.mcp)
// Normalize JSON strings for comparison
var gotJSON, wantJSON interface{}
json.Unmarshal([]byte(got), &gotJSON)
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
assert.Equal(t, wantJSON, gotJSON)
})
}
}