Skip to content

Commit 87fe1ae

Browse files
authored
feat: add mcpServer in config map (#1953)
1 parent 386a208 commit 87fe1ae

File tree

4 files changed

+686
-0
lines changed

4 files changed

+686
-0
lines changed

pkg/ingress/kube/configmap/config.go

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type HigressConfig struct {
4040
Upstream *Upstream `json:"upstream,omitempty"`
4141
DisableXEnvoyHeaders bool `json:"disableXEnvoyHeaders,omitempty"`
4242
AddXRealIpHeader bool `json:"addXRealIpHeader,omitempty"`
43+
McpServer *McpServer `json:"mcpServer,omitempty"`
4344
}
4445

4546
func NewDefaultHigressConfig() *HigressConfig {
@@ -51,6 +52,7 @@ func NewDefaultHigressConfig() *HigressConfig {
5152
Upstream: globalOption.Upstream,
5253
DisableXEnvoyHeaders: globalOption.DisableXEnvoyHeaders,
5354
AddXRealIpHeader: globalOption.AddXRealIpHeader,
55+
McpServer: NewDefaultMcpServer(),
5456
}
5557
return higressConfig
5658
}

pkg/ingress/kube/configmap/controller.go

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ func NewConfigmapMgr(XDSUpdater model.XDSUpdater, namespace string, higressConfi
8989
globalOptionController := NewGlobalOptionController(namespace)
9090
configmapMgr.AddItemControllers(globalOptionController)
9191

92+
mcpServerController := NewMcpServerController(namespace)
93+
configmapMgr.AddItemControllers(mcpServerController)
94+
9295
configmapMgr.initEventHandlers()
9396

9497
return configmapMgr
+327
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
// Copyright (c) 2022 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package configmap
16+
17+
import (
18+
"encoding/json"
19+
"errors"
20+
"fmt"
21+
"reflect"
22+
"strings"
23+
"sync/atomic"
24+
25+
"github.com/alibaba/higress/pkg/ingress/kube/util"
26+
. "github.com/alibaba/higress/pkg/ingress/log"
27+
networking "istio.io/api/networking/v1alpha3"
28+
"istio.io/istio/pkg/config"
29+
"istio.io/istio/pkg/config/schema/gvk"
30+
)
31+
32+
// RedisConfig defines the configuration for Redis connection
33+
type RedisConfig struct {
34+
// The address of Redis server in the format of "host:port"
35+
Address string `json:"address,omitempty"`
36+
// The username for Redis authentication
37+
Username string `json:"username,omitempty"`
38+
// The password for Redis authentication
39+
Password string `json:"password,omitempty"`
40+
// The database index to use
41+
DB int `json:"db,omitempty"`
42+
}
43+
44+
// SSEServer defines the configuration for Server-Sent Events (SSE) server
45+
type SSEServer struct {
46+
// The name of the SSE server
47+
Name string `json:"name,omitempty"`
48+
// The path where the SSE server will be mounted, the full path is (PATH + SsePathSuffix)
49+
Path string `json:"path,omitempty"`
50+
// The type of the SSE server
51+
Type string `json:"type,omitempty"`
52+
// Additional Config parameters for the real MCP server implementation
53+
Config map[string]interface{} `json:"config,omitempty"`
54+
}
55+
56+
// McpServer defines the configuration for MCP (Model Context Protocol) server
57+
type McpServer struct {
58+
// Flag to control whether MCP server is enabled
59+
Enable bool `json:"enable,omitempty"`
60+
// Redis Config for MCP server
61+
Redis *RedisConfig `json:"redis,omitempty"`
62+
// The suffix to be appended to SSE paths, default is "/sse"
63+
SsePathSuffix string `json:"sse_path_suffix,omitempty"`
64+
// List of SSE servers Configs
65+
Servers []*SSEServer `json:"servers,omitempty"`
66+
}
67+
68+
func NewDefaultMcpServer() *McpServer {
69+
return &McpServer{Enable: false}
70+
}
71+
72+
const (
73+
higressMcpServerEnvoyFilterName = "higress-config-mcp-server"
74+
)
75+
76+
func validMcpServer(m *McpServer) error {
77+
if m == nil {
78+
return nil
79+
}
80+
81+
if m.Enable && m.Redis == nil {
82+
return errors.New("redis config cannot be empty when mcp server is enabled")
83+
}
84+
85+
return nil
86+
}
87+
88+
func compareMcpServer(old *McpServer, new *McpServer) (Result, error) {
89+
if old == nil && new == nil {
90+
return ResultNothing, nil
91+
}
92+
93+
if new == nil {
94+
return ResultDelete, nil
95+
}
96+
97+
if !reflect.DeepEqual(old, new) {
98+
return ResultReplace, nil
99+
}
100+
101+
return ResultNothing, nil
102+
}
103+
104+
func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
105+
newMcp := NewDefaultMcpServer()
106+
newMcp.Enable = mcp.Enable
107+
108+
if mcp.Redis != nil {
109+
newMcp.Redis = &RedisConfig{
110+
Address: mcp.Redis.Address,
111+
Username: mcp.Redis.Username,
112+
Password: mcp.Redis.Password,
113+
DB: mcp.Redis.DB,
114+
}
115+
}
116+
117+
newMcp.SsePathSuffix = mcp.SsePathSuffix
118+
119+
if len(mcp.Servers) > 0 {
120+
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
121+
for i, server := range mcp.Servers {
122+
newServer := &SSEServer{
123+
Name: server.Name,
124+
Path: server.Path,
125+
Type: server.Type,
126+
}
127+
if server.Config != nil {
128+
newServer.Config = make(map[string]interface{})
129+
for k, v := range server.Config {
130+
newServer.Config[k] = v
131+
}
132+
}
133+
newMcp.Servers[i] = newServer
134+
}
135+
}
136+
137+
return newMcp, nil
138+
}
139+
140+
type McpServerController struct {
141+
Namespace string
142+
mcpServer atomic.Value
143+
Name string
144+
eventHandler ItemEventHandler
145+
}
146+
147+
func NewMcpServerController(namespace string) *McpServerController {
148+
mcpController := &McpServerController{
149+
Namespace: namespace,
150+
mcpServer: atomic.Value{},
151+
Name: "mcpServer",
152+
}
153+
mcpController.SetMcpServer(NewDefaultMcpServer())
154+
return mcpController
155+
}
156+
157+
func (m *McpServerController) GetName() string {
158+
return m.Name
159+
}
160+
161+
func (m *McpServerController) SetMcpServer(mcp *McpServer) {
162+
m.mcpServer.Store(mcp)
163+
}
164+
165+
func (m *McpServerController) GetMcpServer() *McpServer {
166+
value := m.mcpServer.Load()
167+
if value != nil {
168+
if mcp, ok := value.(*McpServer); ok {
169+
return mcp
170+
}
171+
}
172+
return nil
173+
}
174+
175+
func (m *McpServerController) AddOrUpdateHigressConfig(name util.ClusterNamespacedName, old *HigressConfig, new *HigressConfig) error {
176+
if err := validMcpServer(new.McpServer); err != nil {
177+
IngressLog.Errorf("data:%+v convert to mcp server, error: %+v", new.McpServer, err)
178+
return nil
179+
}
180+
181+
result, _ := compareMcpServer(old.McpServer, new.McpServer)
182+
183+
switch result {
184+
case ResultReplace:
185+
if newMcp, err := deepCopyMcpServer(new.McpServer); err != nil {
186+
IngressLog.Infof("mcp server deepcopy error:%v", err)
187+
} else {
188+
m.SetMcpServer(newMcp)
189+
IngressLog.Infof("AddOrUpdate Higress config mcp server")
190+
m.eventHandler(higressMcpServerEnvoyFilterName)
191+
IngressLog.Infof("send event with filter name:%s", higressMcpServerEnvoyFilterName)
192+
}
193+
case ResultDelete:
194+
m.SetMcpServer(NewDefaultMcpServer())
195+
IngressLog.Infof("Delete Higress config mcp server")
196+
m.eventHandler(higressMcpServerEnvoyFilterName)
197+
IngressLog.Infof("send event with filter name:%s", higressMcpServerEnvoyFilterName)
198+
}
199+
200+
return nil
201+
}
202+
203+
func (m *McpServerController) ValidHigressConfig(higressConfig *HigressConfig) error {
204+
if higressConfig == nil {
205+
return nil
206+
}
207+
if higressConfig.McpServer == nil {
208+
return nil
209+
}
210+
211+
return validMcpServer(higressConfig.McpServer)
212+
}
213+
214+
func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHandler) {
215+
m.eventHandler = eventHandler
216+
}
217+
218+
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
219+
configs := make([]*config.Config, 0)
220+
mcpServer := m.GetMcpServer()
221+
namespace := m.Namespace
222+
223+
if mcpServer == nil || !mcpServer.Enable {
224+
return configs, nil
225+
}
226+
227+
mcpStruct := m.constructMcpServerStruct(mcpServer)
228+
if mcpStruct == "" {
229+
return configs, nil
230+
}
231+
232+
config := &config.Config{
233+
Meta: config.Meta{
234+
GroupVersionKind: gvk.EnvoyFilter,
235+
Name: higressMcpServerEnvoyFilterName,
236+
Namespace: namespace,
237+
},
238+
Spec: &networking.EnvoyFilter{
239+
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
240+
{
241+
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
242+
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
243+
Context: networking.EnvoyFilter_GATEWAY,
244+
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
245+
Listener: &networking.EnvoyFilter_ListenerMatch{
246+
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
247+
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
248+
Name: "envoy.filters.network.http_connection_manager",
249+
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
250+
Name: "envoy.filters.http.cors",
251+
},
252+
},
253+
},
254+
},
255+
},
256+
},
257+
Patch: &networking.EnvoyFilter_Patch{
258+
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
259+
Value: util.BuildPatchStruct(mcpStruct),
260+
},
261+
},
262+
},
263+
},
264+
}
265+
266+
configs = append(configs, config)
267+
return configs, nil
268+
}
269+
270+
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
271+
// 构建 servers 配置
272+
servers := "[]"
273+
if len(mcp.Servers) > 0 {
274+
serverConfigs := make([]string, len(mcp.Servers))
275+
for i, server := range mcp.Servers {
276+
serverConfig := fmt.Sprintf(`{
277+
"name": "%s",
278+
"path": "%s",
279+
"type": "%s"`,
280+
server.Name, server.Path, server.Type)
281+
282+
if len(server.Config) > 0 {
283+
config, _ := json.Marshal(server.Config)
284+
serverConfig += fmt.Sprintf(`,
285+
"config": %s`, string(config))
286+
}
287+
288+
serverConfig += "}"
289+
serverConfigs[i] = serverConfig
290+
}
291+
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
292+
}
293+
294+
// 构建完整的配置结构
295+
structFmt := `{
296+
"name": "envoy.filters.http.golang",
297+
"typed_config": {
298+
"@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
299+
"value": {
300+
"library_id": "mcp-server",
301+
"library_path": "/var/lib/istio/envoy/mcp-server.so",
302+
"plugin_name": "mcp-server",
303+
"plugin_config": {
304+
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
305+
"value": {
306+
"redis": {
307+
"address": "%s",
308+
"username": "%s",
309+
"password": "%s",
310+
"db": %d
311+
},
312+
"sse_path_suffix": "%s",
313+
"servers": %s
314+
}
315+
}
316+
}
317+
}
318+
}`
319+
320+
return fmt.Sprintf(structFmt,
321+
mcp.Redis.Address,
322+
mcp.Redis.Username,
323+
mcp.Redis.Password,
324+
mcp.Redis.DB,
325+
mcp.SsePathSuffix,
326+
servers)
327+
}

0 commit comments

Comments
 (0)